-
👋 欢迎加入我们的交流群!
-
在这里你可以:
+
👋 欢迎加入我们的交流群!在这里你可以:
- 💬 与其他创作者交流心得
- 💡 获取最新功能更新和使用技巧
- 🐛 反馈问题和建议
- 📚 分享创作经验和灵感
-
+
扫描下方二维码加入交流群:
@@ -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)',
}}>

) : (
-
+
微信交流群
{!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 {