diff --git a/README.md b/README.md index 4a7dae7..445208c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
-![Version](https://img.shields.io/badge/version-1.2.7-blue.svg) +![Version](https://img.shields.io/badge/version-1.2.8-blue.svg) ![Python](https://img.shields.io/badge/python-3.11-blue.svg) ![FastAPI](https://img.shields.io/badge/FastAPI-0.109.0-green.svg) ![React](https://img.shields.io/badge/react-18.3.1-blue.svg) diff --git a/backend/.env.example b/backend/.env.example index ffcf2af..4484a58 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -8,7 +8,7 @@ # 应用配置 # ========================================== APP_NAME=MuMuAINovel -APP_VERSION=1.2.7 +APP_VERSION=1.2.8 APP_HOST=0.0.0.0 APP_PORT=8000 DEBUG=false diff --git a/frontend/package.json b/frontend/package.json index 97b55f2..1a606af 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "1.2.7", + "version": "1.2.8", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/public/WX.png b/frontend/public/WX.png index 81de823..f5cf776 100644 Binary files a/frontend/public/WX.png and b/frontend/public/WX.png differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c0ae24e..02d1598 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -40,8 +40,8 @@ function App() { } /> } /> - <>} /> - <>} /> + <>} /> + <>} /> } /> } /> } /> diff --git a/frontend/src/components/AnnotatedText.tsx b/frontend/src/components/AnnotatedText.tsx index 8d7ada3..4578b58 100644 --- a/frontend/src/components/AnnotatedText.tsx +++ b/frontend/src/components/AnnotatedText.tsx @@ -14,7 +14,7 @@ export interface MemoryAnnotation { strength?: number; foreshadowType?: 'planted' | 'resolved'; relatedCharacters?: string[]; - [key: string]: any; + [key: string]: unknown; }; } diff --git a/frontend/src/components/AnnouncementModal.tsx b/frontend/src/components/AnnouncementModal.tsx index 6c6b9d2..0dcbb12 100644 --- a/frontend/src/components/AnnouncementModal.tsx +++ b/frontend/src/components/AnnouncementModal.tsx @@ -73,17 +73,17 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday, } - width={800} + width={700} centered styles={{ body: { - padding: '24px', + padding: '20px', background: 'var(--color-bg-container)', }, header: { background: 'linear-gradient(135deg, rgba(77, 128, 136, 0.08) 0%, rgba(248, 246, 241, 0.95) 100%)', borderBottom: '1px solid var(--color-border-secondary)', - padding: '20px 24px', + padding: '16px 24px', }, footer: { background: 'var(--color-bg-container)', @@ -94,25 +94,24 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday, >
-

👋 欢迎加入我们的交流群!

-

在这里你可以:

+

👋 欢迎加入我们的交流群!在这里你可以:

  • 💬 与其他创作者交流心得
  • 💡 获取最新功能更新和使用技巧
  • 🐛 反馈问题和建议
  • 📚 分享创作经验和灵感
-

+

扫描下方二维码加入交流群:

@@ -122,7 +121,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday, justifyContent: 'center', alignItems: 'flex-start', gap: '24px', - padding: '20px', + padding: '16px', background: 'var(--color-bg-layout)', borderRadius: '8px', flexWrap: 'wrap', @@ -132,9 +131,9 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday, display: 'flex', flexDirection: 'column', alignItems: 'center', - minWidth: '280px', + minWidth: '200px', }}> -

+

QQ交流群

{!qqImageError ? ( @@ -144,15 +143,15 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday, alignItems: 'center', background: 'var(--color-bg-container)', borderRadius: '8px', - padding: '8px', + padding: '6px', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', }}> QQ交流群二维码 ) : (
-

+

微信交流群

{!wxImageError ? ( @@ -194,15 +193,15 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday, alignItems: 'center', background: 'var(--color-bg-container)', borderRadius: '8px', - padding: '8px', + padding: '6px', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', }}> 微信交流群二维码 ) : (
💡 提示:选择"今日内不再展示"当天不再显示,选择"永不再展示"将永久隐藏此公告 diff --git a/frontend/src/components/AppFooter.tsx b/frontend/src/components/AppFooter.tsx index f3bda37..d3de61b 100644 --- a/frontend/src/components/AppFooter.tsx +++ b/frontend/src/components/AppFooter.tsx @@ -1,13 +1,19 @@ import { useState, useEffect } from 'react'; -import { Typography, Space, Divider, Badge, Button } from 'antd'; +import { Typography, Space, Divider, Badge, Button, Grid } from 'antd'; import { GithubOutlined, CopyrightOutlined, HeartFilled, ClockCircleOutlined, GiftOutlined } from '@ant-design/icons'; import { VERSION_INFO, getVersionString } from '../config/version'; import { checkLatestVersion } from '../services/versionService'; const { Text, Link } = Typography; +const { useBreakpoint } = Grid; -export default function AppFooter() { - const isMobile = window.innerWidth <= 768; +interface AppFooterProps { + sidebarWidth?: number; +} + +export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) { + const screens = useBreakpoint(); + const isMobile = !screens.md; const [hasUpdate, setHasUpdate] = useState(false); const [latestVersion, setLatestVersion] = useState(''); const [releaseUrl, setReleaseUrl] = useState(''); @@ -20,7 +26,7 @@ export default function AppFooter() { setHasUpdate(result.hasUpdate); setLatestVersion(result.latestVersion); setReleaseUrl(result.releaseUrl); - } catch (error) { + } catch { // 静默失败 } }; @@ -37,12 +43,15 @@ export default function AppFooter() { } }; + // 计算左边距:桌面端有侧边栏时需要偏移 + const leftOffset = isMobile ? 0 : sidebarWidth; + return (
) => { const target = e.currentTarget; - target.style.transform = 'translateY(-10px) scale(1.01)'; - target.style.boxShadow = '0 20px 40px rgba(77, 128, 136, 0.2), 0 8px 16px rgba(0, 0, 0, 0.08)'; + target.style.transform = 'translateY(-6px) rotateY(-2deg)'; // 悬浮时轻微翻起 + + // 统一书本悬浮态 + target.style.boxShadow = ` + -2px 0 4px rgba(0, 0, 0, 0.1), // 书脊阴影加深 + 8px 12px 24px rgba(0, 0, 0, 0.12) + `; + }, onMouseLeave: (e: React.MouseEvent) => { const target = e.currentTarget; - target.style.transform = 'translateY(0) scale(1)'; - target.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04)'; + target.style.transform = 'translateY(0) rotateY(0)'; + + // 统一恢复基础阴影 + target.style.boxShadow = ` + 0 2px 8px rgba(0, 0, 0, 0.04), + 4px 0 8px rgba(0, 0, 0, 0.02) + `; }, }; diff --git a/frontend/src/components/ChangelogFloatingButton.tsx b/frontend/src/components/ChangelogFloatingButton.tsx index 0cd5445..799ed7a 100644 --- a/frontend/src/components/ChangelogFloatingButton.tsx +++ b/frontend/src/components/ChangelogFloatingButton.tsx @@ -1,20 +1,30 @@ import { useState } from 'react'; -import { FloatButton } from 'antd'; +import { FloatButton, Grid } from 'antd'; import { FileTextOutlined } from '@ant-design/icons'; import ChangelogModal from './ChangelogModal'; +const { useBreakpoint } = Grid; + export default function ChangelogFloatingButton() { const [showChangelog, setShowChangelog] = useState(false); + const screens = useBreakpoint(); + const isMobile = !screens.md; return ( -
+ <> } type="primary" tooltip="查看更新日志" style={{ + // 桌面端时,确保按钮在主内容区域内(侧边栏右侧) right: 24, bottom: 100, + // 移动端无侧边栏,不需要额外处理 + ...(isMobile ? {} : { + // 确保 zIndex 低于侧边栏但高于内容 + zIndex: 999, + }), }} onClick={() => setShowChangelog(true)} /> @@ -23,6 +33,6 @@ export default function ChangelogFloatingButton() { visible={showChangelog} onClose={() => setShowChangelog(false)} /> -
+ ); } \ No newline at end of file diff --git a/frontend/src/components/ChapterAnalysis.tsx b/frontend/src/components/ChapterAnalysis.tsx index 2fee5fd..af006f2 100644 --- a/frontend/src/components/ChapterAnalysis.tsx +++ b/frontend/src/components/ChapterAnalysis.tsx @@ -55,6 +55,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter window.removeEventListener('resize', handleResize); // 清除可能存在的轮询 }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible, chapterId]); // 🔧 新增:独立的章节信息加载函数 diff --git a/frontend/src/components/ChapterContentComparison.tsx b/frontend/src/components/ChapterContentComparison.tsx index db87187..267b2b7 100644 --- a/frontend/src/components/ChapterContentComparison.tsx +++ b/frontend/src/components/ChapterContentComparison.tsx @@ -78,8 +78,9 @@ const ChapterContentComparison: React.FC = ({ }, 500); onClose(); - } catch (error: any) { - message.error(error.message || '应用失败'); + } catch (error: unknown) { + const err = error as Error; + message.error(err.message || '应用失败'); } finally { setApplying(false); } diff --git a/frontend/src/components/ChapterRegenerationModal.tsx b/frontend/src/components/ChapterRegenerationModal.tsx index a861142..49df4e3 100644 --- a/frontend/src/components/ChapterRegenerationModal.tsx +++ b/frontend/src/components/ChapterRegenerationModal.tsx @@ -119,7 +119,22 @@ const ChapterRegenerationModal: React.FC = ({ setWordCount(0); // 构建请求数据 - const requestData: any = { + interface RegenerationRequest { + modification_source: string; + custom_instructions?: string; + selected_suggestion_indices: number[]; + preserve_elements: { + preserve_structure: boolean; + preserve_dialogues: string[]; + preserve_plot_points: string[]; + preserve_character_traits: boolean; + }; + style_id?: string; + target_word_count: number; + focus_areas: string[]; + } + + const requestData: RegenerationRequest = { modification_source: values.modification_source, custom_instructions: values.custom_instructions, selected_suggestion_indices: selectedSuggestions, @@ -158,7 +173,7 @@ const ChapterRegenerationModal: React.FC = ({ currentWordCount = accumulatedContent.length; // 不再自己计算进度,完全依赖后端发送的progress消息 }, - onResult: (data: any) => { + onResult: (data: { word_count?: number }) => { // 生成完成,确保使用最新的累积内容 setProgress(100); setStatus('success'); @@ -183,11 +198,12 @@ const ChapterRegenerationModal: React.FC = ({ } ); - } catch (error: any) { + } catch (error: unknown) { console.error('提交失败:', error); setStatus('error'); - setErrorMessage(error.message || '提交失败'); - message.error('操作失败: ' + (error.message || '未知错误')); + const err = error as Error; + setErrorMessage(err.message || '提交失败'); + message.error('操作失败: ' + (err.message || '未知错误')); } finally { setLoading(false); } diff --git a/frontend/src/components/CharacterCareerCard.tsx b/frontend/src/components/CharacterCareerCard.tsx index 25099a8..0176eb0 100644 --- a/frontend/src/components/CharacterCareerCard.tsx +++ b/frontend/src/components/CharacterCareerCard.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { Card, Button, Modal, Form, Select, InputNumber, Input, message, Progress, Tag, Space, Divider, Typography } from 'antd'; import { EditOutlined, PlusOutlined, DeleteOutlined, TrophyOutlined } from '@ant-design/icons'; import axios from 'axios'; @@ -59,14 +59,7 @@ export const CharacterCareerCard: React.FC = ({ const [progressForm] = Form.useForm(); const [modal, contextHolder] = Modal.useModal(); - useEffect(() => { - fetchCharacterCareers(); - if (editable) { - fetchAllCareers(); - } - }, [characterId]); - - const fetchCharacterCareers = async () => { + const fetchCharacterCareers = useCallback(async () => { try { setLoading(true); const response = await axios.get( @@ -75,14 +68,15 @@ export const CharacterCareerCard: React.FC = ({ ); setMainCareer(response.data.main_career || null); setSubCareers(response.data.sub_careers || []); - } catch (error: any) { - message.error(error.response?.data?.detail || '获取职业信息失败'); + } catch (error: unknown) { + const axiosError = error as { response?: { data?: { detail?: string } } }; + message.error(axiosError.response?.data?.detail || '获取职业信息失败'); } finally { setLoading(false); } - }; + }, [characterId]); - const fetchAllCareers = async () => { + const fetchAllCareers = useCallback(async () => { try { const response = await axios.get(`${API_BASE_URL}/api/careers`, { params: { project_id: projectId }, @@ -91,12 +85,19 @@ export const CharacterCareerCard: React.FC = ({ const main = response.data.main_careers || []; const sub = response.data.sub_careers || []; setAllCareers([...main, ...sub]); - } catch (error: any) { + } catch (error: unknown) { console.error('获取职业列表失败:', error); } - }; + }, [projectId]); - const handleSetMainCareer = async (values: any) => { + useEffect(() => { + fetchCharacterCareers(); + if (editable) { + fetchAllCareers(); + } + }, [characterId, editable, fetchCharacterCareers, fetchAllCareers]); + + const handleSetMainCareer = async (values: { career_id: string; current_stage?: number; started_at?: string }) => { try { await axios.post( `${API_BASE_URL}/api/careers/character/${characterId}/careers/main`, @@ -108,12 +109,13 @@ export const CharacterCareerCard: React.FC = ({ mainForm.resetFields(); fetchCharacterCareers(); onUpdate?.(); - } catch (error: any) { - message.error(error.response?.data?.detail || '设置主职业失败'); + } catch (error: unknown) { + const axiosError = error as { response?: { data?: { detail?: string } } }; + message.error(axiosError.response?.data?.detail || '设置主职业失败'); } }; - const handleAddSubCareer = async (values: any) => { + const handleAddSubCareer = async (values: { career_id: string; current_stage?: number; started_at?: string }) => { try { await axios.post( `${API_BASE_URL}/api/careers/character/${characterId}/careers/sub`, @@ -125,12 +127,13 @@ export const CharacterCareerCard: React.FC = ({ subForm.resetFields(); fetchCharacterCareers(); onUpdate?.(); - } catch (error: any) { - message.error(error.response?.data?.detail || '添加副职业失败'); + } catch (error: unknown) { + const axiosError = error as { response?: { data?: { detail?: string } } }; + message.error(axiosError.response?.data?.detail || '添加副职业失败'); } }; - const handleUpdateProgress = async (values: any) => { + const handleUpdateProgress = async (values: { current_stage: number; stage_progress: number; reached_current_stage_at?: string; notes?: string }) => { if (!selectedCareer) return; try { @@ -144,8 +147,9 @@ export const CharacterCareerCard: React.FC = ({ progressForm.resetFields(); fetchCharacterCareers(); onUpdate?.(); - } catch (error: any) { - message.error(error.response?.data?.detail || '更新职业阶段失败'); + } catch (error: unknown) { + const axiosError = error as { response?: { data?: { detail?: string } } }; + message.error(axiosError.response?.data?.detail || '更新职业阶段失败'); } }; @@ -163,8 +167,9 @@ export const CharacterCareerCard: React.FC = ({ message.success('副职业删除成功'); fetchCharacterCareers(); onUpdate?.(); - } catch (error: any) { - message.error(error.response?.data?.detail || '删除副职业失败'); + } catch (error: unknown) { + const axiosError = error as { response?: { data?: { detail?: string } } }; + message.error(axiosError.response?.data?.detail || '删除副职业失败'); } } }); diff --git a/frontend/src/components/ExpansionPlanEditor.tsx b/frontend/src/components/ExpansionPlanEditor.tsx index 94c88c4..2ce975f 100644 --- a/frontend/src/components/ExpansionPlanEditor.tsx +++ b/frontend/src/components/ExpansionPlanEditor.tsx @@ -1,6 +1,6 @@ import { Modal, Form, Input, InputNumber, Select, Tag, Space, Button, message, Divider } from 'antd'; import { PlusOutlined } from '@ant-design/icons'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import type { ExpansionPlanData, Character } from '../types'; import { characterApi } from '../services/api'; @@ -36,13 +36,7 @@ export default function ExpansionPlanEditor({ const [loadingCharacters, setLoadingCharacters] = useState(false); // 加载项目角色列表 - useEffect(() => { - if (visible && projectId) { - loadCharacters(); - } - }, [visible, projectId]); - - const loadCharacters = async () => { + const loadCharacters = useCallback(async () => { try { setLoadingCharacters(true); setAvailableCharacters([]); // 重置为空数组 @@ -53,8 +47,11 @@ export default function ExpansionPlanEditor({ let chars: Character[] = []; if (Array.isArray(response)) { chars = response; - } else if (response && typeof response === 'object' && 'items' in response && Array.isArray((response as any).items)) { - chars = (response as any).items; + } else if (response && typeof response === 'object' && 'items' in response) { + const responseObj = response as { items?: Character[] }; + if (Array.isArray(responseObj.items)) { + chars = responseObj.items; + } } else { console.error('角色API返回格式异常:', response); message.warning('角色数据格式异常'); @@ -62,14 +59,21 @@ export default function ExpansionPlanEditor({ setAvailableCharacters(chars); console.log('设置的角色列表:', chars); - } catch (error: any) { + } catch (error: unknown) { console.error('加载角色列表失败:', error); setAvailableCharacters([]); - message.error('加载角色列表失败: ' + (error?.message || '未知错误')); + const err = error as Error; + message.error('加载角色列表失败: ' + (err?.message || '未知错误')); } finally { setLoadingCharacters(false); } - }; + }, [projectId]); + + useEffect(() => { + if (visible && projectId) { + loadCharacters(); + } + }, [visible, projectId, loadCharacters]); // 当planData或chapterSummary变化时更新状态 useEffect(() => { @@ -325,7 +329,7 @@ export default function ExpansionPlanEditor({ step={100} style={{ width: '100%' }} formatter={(value) => `${value} 字`} - parser={(value) => value?.replace(' 字', '') as any} + parser={(value) => Number(value?.replace(' 字', '')) as 500 | 10000} /> diff --git a/frontend/src/components/UserMenu.tsx b/frontend/src/components/UserMenu.tsx index d5279ab..293dd3d 100644 --- a/frontend/src/components/UserMenu.tsx +++ b/frontend/src/components/UserMenu.tsx @@ -8,7 +8,12 @@ import { useNavigate } from 'react-router-dom'; const { Text } = Typography; -export default function UserMenu() { +interface UserMenuProps { + /** 是否总是显示完整信息(用于移动端侧边栏) */ + showFullInfo?: boolean; +} + +export default function UserMenu({ showFullInfo = false }: UserMenuProps) { const navigate = useNavigate(); const [currentUser, setCurrentUser] = useState(null); const [showChangePassword, setShowChangePassword] = useState(false); @@ -54,9 +59,10 @@ export default function UserMenu() { message.success('密码修改成功'); setShowChangePassword(false); changePasswordForm.resetFields(); - } catch (error: any) { + } catch (error: unknown) { console.error('修改密码失败:', error); - message.error(error.response?.data?.detail || '修改密码失败'); + const err = error as { response?: { data?: { detail?: string } } }; + message.error(err.response?.data?.detail || '修改密码失败'); } finally { setChangingPassword(false); } @@ -171,7 +177,7 @@ export default function UserMenu() {
)}
- + (null); + interface PasswordStatus { + has_password: boolean; + has_custom_password: boolean; + username: string; + default_password: string; + } + const [passwordStatus, setPasswordStatus] = useState(null); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [settingPassword, setSettingPassword] = useState(false); @@ -187,7 +193,7 @@ export default function AuthCallback() { setShowAnnouncement(true); }, 500); } - } catch (error) { + } catch { message.error('密码设置失败,请重试'); } finally { setSettingPassword(false); diff --git a/frontend/src/pages/Careers.tsx b/frontend/src/pages/Careers.tsx index 9ee22c5..cc59fa5 100644 --- a/frontend/src/pages/Careers.tsx +++ b/frontend/src/pages/Careers.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { Button, Modal, Form, Input, Select, message, Row, Col, Empty, Tabs, Card, Tag, Space, Divider, Typography, InputNumber } from 'antd'; import { ThunderboltOutlined, PlusOutlined, EditOutlined, DeleteOutlined, TrophyOutlined } from '@ant-design/icons'; import { useParams } from 'react-router-dom'; @@ -46,26 +46,26 @@ export default function Careers() { const [aiProgress, setAiProgress] = useState(0); const [aiMessage, setAiMessage] = useState(''); - useEffect(() => { - if (projectId) { - fetchCareers(); - } - }, [projectId]); - - const fetchCareers = async () => { + const fetchCareers = useCallback(async () => { try { setLoading(true); - const response: any = await api.get('/careers', { + const response = await api.get('/careers', { params: { project_id: projectId } - }); + }) as { main_careers?: Career[]; sub_careers?: Career[] }; setMainCareers(response.main_careers || []); setSubCareers(response.sub_careers || []); - } catch (error: any) { + } catch (error: unknown) { console.error('获取职业列表失败:', error); } finally { setLoading(false); } - }; + }, [projectId]); + + useEffect(() => { + if (projectId) { + fetchCareers(); + } + }, [projectId, fetchCareers]); const handleOpenModal = (career?: Career) => { if (career) { @@ -81,7 +81,18 @@ export default function Careers() { setIsModalOpen(true); }; - const handleSubmit = async (values: any) => { + interface CareerFormValues { + name: string; + type: 'main' | 'sub'; + description?: string; + category?: string; + stages?: string; + requirements?: string; + special_abilities?: string; + worldview_rules?: string; + } + + const handleSubmit = async (values: CareerFormValues) => { try { // 解析阶段数据 const stagesText = values.stages || ''; @@ -124,8 +135,9 @@ export default function Careers() { setIsModalOpen(false); form.resetFields(); fetchCareers(); - } catch (error: any) { - message.error(error.response?.data?.detail || '操作失败'); + } catch (error: unknown) { + const axiosError = error as { response?: { data?: { detail?: string } } }; + message.error(axiosError.response?.data?.detail || '操作失败'); } }; @@ -139,14 +151,15 @@ export default function Careers() { await api.delete(`/careers/${id}`); message.success('职业删除成功'); fetchCareers(); - } catch (error: any) { - message.error(error.response?.data?.detail || '删除失败'); + } catch (error: unknown) { + const axiosError = error as { response?: { data?: { detail?: string } } }; + message.error(axiosError.response?.data?.detail || '删除失败'); } } }); }; - const handleAIGenerate = async (values: any) => { + const handleAIGenerate = async (values: { main_career_count: number; sub_career_count: number }) => { setIsAIModalOpen(false); setAiGenerating(true); setAiProgress(0); @@ -193,9 +206,10 @@ export default function Careers() { setAiGenerating(false); message.error('连接中断,生成失败'); }; - } catch (err: any) { + } catch (err: unknown) { setAiGenerating(false); - message.error(err.message || '启动生成失败'); + const error = err as Error; + message.error(error.message || '启动生成失败'); } }; diff --git a/frontend/src/pages/ChapterReader.tsx b/frontend/src/pages/ChapterReader.tsx index d733d23..aaa1be3 100644 --- a/frontend/src/pages/ChapterReader.tsx +++ b/frontend/src/pages/ChapterReader.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Card, Spin, Alert, Button, Space, Switch, Drawer, message, Progress } from 'antd'; import { @@ -75,13 +75,7 @@ const ChapterReader: React.FC = () => { const [analysisProgress, setAnalysisProgress] = useState(0); const [navigation, setNavigation] = useState(null); - useEffect(() => { - if (chapterId) { - loadChapterData(); - } - }, [chapterId]); - - const loadChapterData = async () => { + const loadChapterData = useCallback(async () => { try { setLoading(true); setError(null); @@ -130,13 +124,20 @@ const ChapterReader: React.FC = () => { } else { setAnnotationsData(null); } - } catch (err: any) { + } catch (err: unknown) { console.error('加载章节数据失败:', err); - setError(err.response?.data?.detail || err.message || '加载失败'); + const error = err as { response?: { data?: { detail?: string } }; message?: string }; + setError(error.response?.data?.detail || error.message || '加载失败'); } finally { setLoading(false); } - }; + }, [chapterId]); + + useEffect(() => { + if (chapterId) { + loadChapterData(); + } + }, [chapterId, loadChapterData]); const handleAnnotationClick = (annotation: MemoryAnnotation) => { setActiveAnnotationId(annotation.id); @@ -211,10 +212,11 @@ const ChapterReader: React.FC = () => { } }, 30000); - } catch (err: any) { + } catch (err: unknown) { setAnalyzing(false); + const error = err as { response?: { data?: { detail?: string } } }; message.error({ - content: err.response?.data?.detail || '触发分析失败', + content: error.response?.data?.detail || '触发分析失败', key: 'analyze' }); } diff --git a/frontend/src/pages/Inspiration.tsx b/frontend/src/pages/Inspiration.tsx index 57f25ff..1323ae6 100644 --- a/frontend/src/pages/Inspiration.tsx +++ b/frontend/src/pages/Inspiration.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Card, Input, Button, Space, Typography, message, Spin, Modal } from 'antd'; import { SendOutlined, ArrowLeftOutlined, ReloadOutlined } from '@ant-design/icons'; @@ -102,8 +102,18 @@ const Inspiration: React.FC = () => { // ==================== 缓存管理函数 ==================== + // 清除缓存 + const clearCache = useCallback(() => { + try { + localStorage.removeItem(CACHE_KEY); + console.log('🗑️ 缓存已清除'); + } catch (error) { + console.error('清除缓存失败:', error); + } + }, []); + // 保存到缓存 - const saveToCache = () => { + const saveToCache = useCallback(() => { try { // 只在对话阶段保存,生成阶段不保存 if (currentStep === 'generating' || currentStep === 'complete') { @@ -130,10 +140,10 @@ const Inspiration: React.FC = () => { } catch (error) { console.error('保存缓存失败:', error); } - }; + }, [currentStep, messages, wizardData, initialIdea, selectedOptions, lastFailedRequest]); // 从缓存恢复 - const restoreFromCache = (): boolean => { + const restoreFromCache = useCallback((): boolean => { try { const cached = localStorage.getItem(CACHE_KEY); if (!cached) { @@ -174,17 +184,7 @@ const Inspiration: React.FC = () => { clearCache(); return false; } - }; - - // 清除缓存 - const clearCache = () => { - try { - localStorage.removeItem(CACHE_KEY); - console.log('🗑️ 缓存已清除'); - } catch (error) { - console.error('清除缓存失败:', error); - } - }; + }, [clearCache]); // ==================== 组件挂载时恢复缓存 ==================== @@ -193,7 +193,7 @@ const Inspiration: React.FC = () => { restoreFromCache(); setCacheLoaded(true); } - }, []); + }, [cacheLoaded, restoreFromCache]); // ==================== 自动保存:状态变化时保存 ==================== @@ -206,7 +206,7 @@ const Inspiration: React.FC = () => { }, 500); return () => clearTimeout(timer); - }, [messages, currentStep, wizardData, initialIdea, selectedOptions, lastFailedRequest, cacheLoaded]); + }, [messages, currentStep, wizardData, initialIdea, selectedOptions, lastFailedRequest, cacheLoaded, saveToCache]); // 自动滚动到底部 const scrollToBottom = () => { @@ -259,7 +259,7 @@ const Inspiration: React.FC = () => { }; setMessages(prev => [...prev, aiMessage]); setLastFailedRequest(null); - } catch (error: any) { + } catch (error: unknown) { console.error('重试失败:', error); message.error('重试失败,请稍后再试'); } finally { @@ -307,7 +307,7 @@ const Inspiration: React.FC = () => { const step = targetMessage.step as 'title' | 'description' | 'theme' | 'genre'; // 构建上下文 - const context: any = { + const context: Partial & { initial_idea?: string } = { initial_idea: initialIdea, title: wizardData.title, description: wizardData.description, @@ -339,9 +339,11 @@ const Inspiration: React.FC = () => { setMessages(prev => [...prev, aiMessage]); message.success('已根据您的反馈重新生成选项'); - } catch (error: any) { + } catch (error: unknown) { console.error('优化选项失败:', error); - message.error(error.response?.data?.detail || '优化失败,请重试'); + const errMsg = error instanceof Error ? error.message : '优化失败,请重试'; + const axiosError = error as { response?: { data?: { detail?: string } } }; + message.error(axiosError.response?.data?.detail || errMsg); } finally { setRefining(false); } @@ -406,9 +408,11 @@ const Inspiration: React.FC = () => { } else { await handleCustomInput(userInput); } - } catch (error: any) { + } catch (error: unknown) { console.error('发送消息失败:', error); - message.error(error.response?.data?.detail || '生成失败,请重试'); + const errMsg = error instanceof Error ? error.message : '生成失败,请重试'; + const axiosError = error as { response?: { data?: { detail?: string } } }; + message.error(axiosError.response?.data?.detail || errMsg); } finally { setLoading(false); } @@ -575,9 +579,11 @@ const Inspiration: React.FC = () => { setWizardData(updatedData); await generateNextStep(updatedData); - } catch (error: any) { + } catch (error: unknown) { console.error('选择选项失败:', error); - message.error(error.response?.data?.detail || '生成失败,请重试'); + const errMsg = error instanceof Error ? error.message : '生成失败,请重试'; + const axiosError = error as { response?: { data?: { detail?: string } } }; + message.error(axiosError.response?.data?.detail || errMsg); } finally { setLoading(false); } @@ -625,9 +631,11 @@ const Inspiration: React.FC = () => { setWizardData(updatedData); await generateNextStep(updatedData); - } catch (error: any) { + } catch (error: unknown) { console.error('处理自定义输入失败:', error); - message.error(error.response?.data?.detail || '处理失败,请重试'); + const errMsg = error instanceof Error ? error.message : '处理失败,请重试'; + const axiosError = error as { response?: { data?: { detail?: string } } }; + message.error(axiosError.response?.data?.detail || errMsg); } finally { setLoading(false); } @@ -1050,7 +1058,7 @@ const Inspiration: React.FC = () => { return (
{contextHolder} @@ -1118,7 +1126,7 @@ const Inspiration: React.FC = () => { color: '#fff', }} > - {isMobile ? '返回' : '返回项目列表'} + {isMobile ? '返回' : '返回首页'}
@@ -1133,12 +1141,6 @@ const Inspiration: React.FC = () => { > ✨ 灵感模式 - - 通过对话快速创建你的小说项目 -
{/* 重新开始按钮 - 只在对话进行中显示 */} diff --git a/frontend/src/pages/Organizations.tsx b/frontend/src/pages/Organizations.tsx index da0f0d5..bf67b49 100644 --- a/frontend/src/pages/Organizations.tsx +++ b/frontend/src/pages/Organizations.tsx @@ -83,6 +83,7 @@ export default function Organizations() { } finally { setLoading(false); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [projectId]); const loadCharacters = useCallback(async () => { diff --git a/frontend/src/pages/ProjectWizardNew.tsx b/frontend/src/pages/ProjectWizardNew.tsx index 2ce313c..825afdb 100644 --- a/frontend/src/pages/ProjectWizardNew.tsx +++ b/frontend/src/pages/ProjectWizardNew.tsx @@ -39,6 +39,7 @@ export default function ProjectWizardNew() { setResumeProjectId(projectId); handleResumeGeneration(projectId); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams]); // 恢复未完成项目的生成 @@ -320,7 +321,7 @@ export default function ProjectWizardNew() { return (
{/* 顶部标题栏 - 固定不滚动 */} @@ -358,6 +359,7 @@ export default function ProjectWizardNew() { color: '#fff', textShadow: '0 2px 4px rgba(0,0,0,0.1)', }}> + 项目创建向导 diff --git a/frontend/src/pages/Relationships.tsx b/frontend/src/pages/Relationships.tsx index 2e42ee4..d37398f 100644 --- a/frontend/src/pages/Relationships.tsx +++ b/frontend/src/pages/Relationships.tsx @@ -61,6 +61,7 @@ export default function Relationships() { if (projectId) { loadData(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [projectId]); const loadData = async () => { diff --git a/frontend/src/pages/UserManagement.tsx b/frontend/src/pages/UserManagement.tsx index d2ac4df..a74d683 100644 --- a/frontend/src/pages/UserManagement.tsx +++ b/frontend/src/pages/UserManagement.tsx @@ -90,7 +90,16 @@ export default function UserManagement() { }, []); // 添加用户 - const handleCreate = async (values: any) => { + interface CreateUserValues { + username: string; + display_name: string; + password?: string; + avatar_url?: string; + trust_level?: number; + is_admin?: boolean; + } + + const handleCreate = async (values: CreateUserValues) => { try { const res = await adminApi.createUser(values); message.success('用户创建成功'); @@ -134,7 +143,14 @@ export default function UserManagement() { setEditModalVisible(true); }; - const handleUpdate = async (values: any) => { + interface UpdateUserValues { + display_name: string; + avatar_url?: string; + trust_level?: number; + is_admin?: boolean; + } + + const handleUpdate = async (values: UpdateUserValues) => { if (!currentUser) return; try { @@ -290,7 +306,7 @@ export default function UserManagement() { key: 'action', width: isMobile ? 80 : 300, fixed: 'right' as const, - render: (_: any, record: UserWithStatus) => { + render: (_: unknown, record: UserWithStatus) => { const isActive = record.is_active !== false; // 移动端:使用下拉菜单 diff --git a/frontend/src/pages/WorldSetting.tsx b/frontend/src/pages/WorldSetting.tsx index a8cfb21..15eedaf 100644 --- a/frontend/src/pages/WorldSetting.tsx +++ b/frontend/src/pages/WorldSetting.tsx @@ -58,7 +58,7 @@ export default function WorldSetting() { // 可以在这里显示生成的内容片段(可选) console.log('生成片段:', chunk); }, - onResult: (result: any) => { + onResult: (result: { time_period: string; location: string; atmosphere: string; rules: string }) => { // 保存新生成的数据 const newData = { time_period: result.time_period, diff --git a/frontend/src/pages/WritingStyles.tsx b/frontend/src/pages/WritingStyles.tsx index 5652f67..6f5693e 100644 --- a/frontend/src/pages/WritingStyles.tsx +++ b/frontend/src/pages/WritingStyles.tsx @@ -53,6 +53,7 @@ export default function WritingStyles() { // 加载风格列表 - 如果有项目则加载项目风格(包含默认标记),否则加载用户风格 useEffect(() => { loadStyles(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentProject?.id]); const loadStyles = async () => { diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index d43d4cd..ecedf15 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -192,7 +192,7 @@ export const settingsApi = { getAvailableModels: (params: { api_key: string; api_base_url: string; provider: string }) => api.get; count?: number }>('/settings/models', { params }), - testApiConnection: (params: { api_key: string; api_base_url: string; provider: string; llm_model: string }) => + testApiConnection: (params: { api_key: string; api_base_url: string; provider: string; llm_model: string; temperature?: number; max_tokens?: number }) => api.post; + details?: Record; error?: string; error_type?: string; suggestions?: string[]; diff --git a/frontend/src/services/versionService.ts b/frontend/src/services/versionService.ts index 1d88758..a29d7ab 100644 --- a/frontend/src/services/versionService.ts +++ b/frontend/src/services/versionService.ts @@ -69,7 +69,7 @@ export async function checkLatestVersion(): Promise { } throw new Error('无法从 Badge API 解析版本信息'); - } catch (error) { + } catch { // 失败时返回无更新 return { hasUpdate: false, diff --git a/frontend/src/store/eventBus.ts b/frontend/src/store/eventBus.ts index 923e030..48a9164 100644 --- a/frontend/src/store/eventBus.ts +++ b/frontend/src/store/eventBus.ts @@ -112,4 +112,7 @@ export const EventNames = { CHAPTER_UPDATED: 'chapter:updated', CHAPTER_DELETED: 'chapter:deleted', CHAPTER_NEEDS_REFRESH: 'chapter:needsRefresh', + + // 视图切换事件 + SWITCH_TO_MCP_VIEW: 'view:switchToMcp', } as const; \ No newline at end of file diff --git a/frontend/src/utils/sessionManager.ts b/frontend/src/utils/sessionManager.ts index 366769e..20b8d9e 100644 --- a/frontend/src/utils/sessionManager.ts +++ b/frontend/src/utils/sessionManager.ts @@ -113,7 +113,7 @@ class SessionManager { await this.refreshSession(); } } - } catch (error) { + } catch { // 静默处理错误 } } @@ -231,7 +231,7 @@ class SessionManager { try { await this.refreshSession(); return true; - } catch (error) { + } catch { return false; } } diff --git a/frontend/src/utils/sseClient.ts b/frontend/src/utils/sseClient.ts index afd2b37..c64d475 100644 --- a/frontend/src/utils/sseClient.ts +++ b/frontend/src/utils/sseClient.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ export interface SSEMessage { type: 'progress' | 'chunk' | 'result' | 'error' | 'done'; message?: string; @@ -61,7 +62,7 @@ export class SSEClient { }); } - private handleMessage(message: SSEMessage, resolve: Function, reject: Function) { + private handleMessage(message: SSEMessage, resolve: (value: any) => void, reject: (reason?: any) => void) { switch (message.type) { case 'progress': if (this.options.onProgress && message.progress !== undefined) { @@ -129,6 +130,7 @@ export class SSEPostClient { private options: SSEClientOptions; private abortController: AbortController | null = null; private accumulatedContent: string = ''; + private resultData: any = null; constructor(url: string, data: any, options: SSEClientOptions = {}) { this.url = url; @@ -137,7 +139,12 @@ export class SSEPostClient { } async connect(): Promise { - return new Promise(async (resolve, reject) => { + return new Promise((resolve, reject) => { + this.connectInternal(resolve, reject); + }); + } + + private async connectInternal(resolve: (value: any) => void, reject: (reason?: any) => void) { try { this.abortController = new AbortController(); @@ -232,10 +239,9 @@ export class SSEPostClient { reject(error); } } - }); } - private async handleMessage(message: SSEMessage, resolve: Function, reject: Function) { + private async handleMessage(message: SSEMessage, resolve: (value: any) => void, reject: (reason?: any) => void) { switch (message.type) { case 'progress': if (this.options.onProgress && message.progress !== undefined) { @@ -261,7 +267,7 @@ export class SSEPostClient { if (this.options.onResult && message.data) { this.options.onResult(message.data); } - (this as any).resultData = message.data; + this.resultData = message.data; break; case 'error': @@ -275,8 +281,8 @@ export class SSEPostClient { if (this.options.onComplete) { this.options.onComplete(); } - if ((this as any).resultData) { - resolve((this as any).resultData); + if (this.resultData) { + resolve(this.resultData); } else if (this.accumulatedContent) { resolve({ content: this.accumulatedContent }); } else {