fix: 修复多处TypeScript类型错误(Inspiration、ExpansionPlanEditor、sseClient等)

This commit is contained in:
xiamuceer-j
2026-01-14 14:33:43 +08:00
parent aeb78fddd2
commit 7ba2b2e5fa
31 changed files with 347 additions and 192 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
<div align="center"> <div align="center">
![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) ![Python](https://img.shields.io/badge/python-3.11-blue.svg)
![FastAPI](https://img.shields.io/badge/FastAPI-0.109.0-green.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) ![React](https://img.shields.io/badge/react-18.3.1-blue.svg)
+1 -1
View File
@@ -8,7 +8,7 @@
# 应用配置 # 应用配置
# ========================================== # ==========================================
APP_NAME=MuMuAINovel APP_NAME=MuMuAINovel
APP_VERSION=1.2.7 APP_VERSION=1.2.8
APP_HOST=0.0.0.0 APP_HOST=0.0.0.0
APP_PORT=8000 APP_PORT=8000
DEBUG=false DEBUG=false
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "1.2.7", "version": "1.2.8",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 188 KiB

+2 -2
View File
@@ -40,8 +40,8 @@ function App() {
<Route path="/login" element={<><Login /><AppFooter /></>} /> <Route path="/login" element={<><Login /><AppFooter /></>} />
<Route path="/auth/callback" element={<AuthCallback />} /> <Route path="/auth/callback" element={<AuthCallback />} />
<Route path="/" element={<ProtectedRoute><><ProjectList /><AppFooter /></></ProtectedRoute>} /> <Route path="/" element={<ProtectedRoute><><ProjectList /><AppFooter sidebarWidth={220} /></></ProtectedRoute>} />
<Route path="/projects" element={<ProtectedRoute><><ProjectList /><AppFooter /></></ProtectedRoute>} /> <Route path="/projects" element={<ProtectedRoute><><ProjectList /><AppFooter sidebarWidth={220} /></></ProtectedRoute>} />
<Route path="/wizard" element={<ProtectedRoute><ProjectWizardNew /></ProtectedRoute>} /> <Route path="/wizard" element={<ProtectedRoute><ProjectWizardNew /></ProtectedRoute>} />
<Route path="/inspiration" element={<ProtectedRoute><Inspiration /></ProtectedRoute>} /> <Route path="/inspiration" element={<ProtectedRoute><Inspiration /></ProtectedRoute>} />
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} /> <Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
+1 -1
View File
@@ -14,7 +14,7 @@ export interface MemoryAnnotation {
strength?: number; strength?: number;
foreshadowType?: 'planted' | 'resolved'; foreshadowType?: 'planted' | 'resolved';
relatedCharacters?: string[]; relatedCharacters?: string[];
[key: string]: any; [key: string]: unknown;
}; };
} }
+28 -29
View File
@@ -73,17 +73,17 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
</Button> </Button>
</Space> </Space>
} }
width={800} width={700}
centered centered
styles={{ styles={{
body: { body: {
padding: '24px', padding: '20px',
background: 'var(--color-bg-container)', background: 'var(--color-bg-container)',
}, },
header: { header: {
background: 'linear-gradient(135deg, rgba(77, 128, 136, 0.08) 0%, rgba(248, 246, 241, 0.95) 100%)', 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)', borderBottom: '1px solid var(--color-border-secondary)',
padding: '20px 24px', padding: '16px 24px',
}, },
footer: { footer: {
background: 'var(--color-bg-container)', background: 'var(--color-bg-container)',
@@ -94,25 +94,24 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
> >
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<div style={{ <div style={{
marginBottom: '16px', marginBottom: '12px',
fontSize: '16px', fontSize: '15px',
color: 'var(--color-text-secondary)', color: 'var(--color-text-secondary)',
lineHeight: '1.6', lineHeight: '1.5',
}}> }}>
<p>👋 </p> <p style={{ marginBottom: '8px' }}>👋 </p>
<p></p>
<ul style={{ <ul style={{
textAlign: 'left', textAlign: 'left',
marginLeft: '40px', marginLeft: '40px',
marginTop: '12px', marginTop: '0',
marginBottom: '20px', marginBottom: '12px',
}}> }}>
<li>💬 </li> <li>💬 </li>
<li>💡 使</li> <li>💡 使</li>
<li>🐛 </li> <li>🐛 </li>
<li>📚 </li> <li>📚 </li>
</ul> </ul>
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '16px' }}> <p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '12px' }}>
</p> </p>
</div> </div>
@@ -122,7 +121,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'flex-start', alignItems: 'flex-start',
gap: '24px', gap: '24px',
padding: '20px', padding: '16px',
background: 'var(--color-bg-layout)', background: 'var(--color-bg-layout)',
borderRadius: '8px', borderRadius: '8px',
flexWrap: 'wrap', flexWrap: 'wrap',
@@ -132,9 +131,9 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
minWidth: '280px', minWidth: '200px',
}}> }}>
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '12px', fontSize: '15px' }}> <p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '8px', fontSize: '14px' }}>
QQ交流群 QQ交流群
</p> </p>
{!qqImageError ? ( {!qqImageError ? (
@@ -144,15 +143,15 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
alignItems: 'center', alignItems: 'center',
background: 'var(--color-bg-container)', background: 'var(--color-bg-container)',
borderRadius: '8px', borderRadius: '8px',
padding: '8px', padding: '6px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}> }}>
<img <img
src="/qq.jpg" src="/qq.jpg"
alt="QQ交流群二维码" alt="QQ交流群二维码"
style={{ style={{
maxWidth: '280px', maxWidth: '180px',
maxHeight: '280px', maxHeight: '180px',
width: 'auto', width: 'auto',
height: 'auto', height: 'auto',
display: 'block', display: 'block',
@@ -163,8 +162,8 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
</div> </div>
) : ( ) : (
<div style={{ <div style={{
width: '280px', width: '180px',
height: '280px', height: '180px',
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
@@ -182,9 +181,9 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
minWidth: '280px', minWidth: '200px',
}}> }}>
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '12px', fontSize: '15px' }}> <p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '8px', fontSize: '14px' }}>
</p> </p>
{!wxImageError ? ( {!wxImageError ? (
@@ -194,15 +193,15 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
alignItems: 'center', alignItems: 'center',
background: 'var(--color-bg-container)', background: 'var(--color-bg-container)',
borderRadius: '8px', borderRadius: '8px',
padding: '8px', padding: '6px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}> }}>
<img <img
src="/WX.png" src="/WX.png"
alt="微信交流群二维码" alt="微信交流群二维码"
style={{ style={{
maxWidth: '280px', maxWidth: '180px',
maxHeight: '280px', maxHeight: '180px',
width: 'auto', width: 'auto',
height: 'auto', height: 'auto',
display: 'block', display: 'block',
@@ -213,8 +212,8 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
</div> </div>
) : ( ) : (
<div style={{ <div style={{
width: '280px', width: '180px',
height: '280px', height: '180px',
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
@@ -229,12 +228,12 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
</div> </div>
<div style={{ <div style={{
marginTop: '20px', marginTop: '16px',
padding: '12px', padding: '10px',
background: 'var(--color-warning-bg)', background: 'var(--color-warning-bg)',
borderRadius: '8px', borderRadius: '8px',
border: '1px solid var(--color-warning-border)', border: '1px solid var(--color-warning-border)',
fontSize: '14px', fontSize: '13px',
color: 'var(--color-warning)', color: 'var(--color-warning)',
}}> }}>
💡 "今日内不再展示""永不再展示" 💡 "今日内不再展示""永不再展示"
+15 -5
View File
@@ -1,13 +1,19 @@
import { useState, useEffect } from 'react'; 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 { GithubOutlined, CopyrightOutlined, HeartFilled, ClockCircleOutlined, GiftOutlined } from '@ant-design/icons';
import { VERSION_INFO, getVersionString } from '../config/version'; import { VERSION_INFO, getVersionString } from '../config/version';
import { checkLatestVersion } from '../services/versionService'; import { checkLatestVersion } from '../services/versionService';
const { Text, Link } = Typography; const { Text, Link } = Typography;
const { useBreakpoint } = Grid;
export default function AppFooter() { interface AppFooterProps {
const isMobile = window.innerWidth <= 768; sidebarWidth?: number;
}
export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
const screens = useBreakpoint();
const isMobile = !screens.md;
const [hasUpdate, setHasUpdate] = useState(false); const [hasUpdate, setHasUpdate] = useState(false);
const [latestVersion, setLatestVersion] = useState(''); const [latestVersion, setLatestVersion] = useState('');
const [releaseUrl, setReleaseUrl] = useState(''); const [releaseUrl, setReleaseUrl] = useState('');
@@ -20,7 +26,7 @@ export default function AppFooter() {
setHasUpdate(result.hasUpdate); setHasUpdate(result.hasUpdate);
setLatestVersion(result.latestVersion); setLatestVersion(result.latestVersion);
setReleaseUrl(result.releaseUrl); setReleaseUrl(result.releaseUrl);
} catch (error) { } catch {
// 静默失败 // 静默失败
} }
}; };
@@ -37,12 +43,15 @@ export default function AppFooter() {
} }
}; };
// 计算左边距:桌面端有侧边栏时需要偏移
const leftOffset = isMobile ? 0 : sidebarWidth;
return ( return (
<div <div
style={{ style={{
position: 'fixed', position: 'fixed',
bottom: 0, bottom: 0,
left: 0, left: leftOffset,
right: 0, right: 0,
backdropFilter: 'blur(20px) saturate(180%)', backdropFilter: 'blur(20px) saturate(180%)',
WebkitBackdropFilter: 'blur(20px) saturate(180%)', WebkitBackdropFilter: 'blur(20px) saturate(180%)',
@@ -51,6 +60,7 @@ export default function AppFooter() {
zIndex: 100, zIndex: 100,
boxShadow: 'var(--shadow-card)', boxShadow: 'var(--shadow-card)',
backgroundColor: 'rgba(255, 255, 255, 0.8)', // 半透明背景以支持 backdrop-filter backgroundColor: 'rgba(255, 255, 255, 0.8)', // 半透明背景以支持 backdrop-filter
transition: 'left 0.3s ease', // 平滑过渡
}} }}
> >
<div <div
+60 -11
View File
@@ -11,6 +11,7 @@ export const cardStyles = {
// 悬浮效果 // 悬浮效果
hoverable: { hoverable: {
cursor: 'pointer', cursor: 'pointer',
position: 'relative' as const,
} as CSSProperties, } as CSSProperties,
// 角色卡片样式 // 角色卡片样式
@@ -32,15 +33,52 @@ export const cardStyles = {
borderRadius: 12, borderRadius: 12,
} as CSSProperties, } as CSSProperties,
// 项目卡片样式 - 现代化设计 // 项目卡片样式 - 书籍风格 (Book Style)
project: { project: {
height: '100%', height: '100%',
borderRadius: 20, borderRadius: '6px 16px 16px 6px', // 左侧稍直(书脊),右侧圆润
overflow: 'hidden', overflow: 'hidden',
background: 'var(--color-bg-container)', background: '#fff',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04)', // 基础阴影 + 书籍厚度阴影
transition: 'all 0.35s cubic-bezier(0.4, 0, 0.2, 1)', boxShadow: `
border: '1px solid rgba(0, 0, 0, 0.04)', 0 2px 8px rgba(0, 0, 0, 0.04),
4px 0 8px rgba(0, 0, 0, 0.02)
`,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
border: '1px solid rgba(0,0,0,0.02)',
borderLeft: '6px solid var(--color-primary)', // 书脊效果
display: 'flex',
flexDirection: 'column',
position: 'relative',
} as CSSProperties,
// 新建项目卡片样式 - 统一书籍风格
newProjectBook: {
height: '100%',
borderRadius: '6px 16px 16px 6px',
overflow: 'hidden',
background: '#fff',
// 基础阴影 + 书籍厚度阴影 (与普通项目一致)
boxShadow: `
0 2px 8px rgba(0, 0, 0, 0.04),
4px 0 8px rgba(0, 0, 0, 0.02)
`,
border: '1px solid rgba(0,0,0,0.02)',
borderLeft: '6px solid var(--color-primary)', // 与普通项目一致的书脊颜色
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative',
} as CSSProperties,
// 书架风格容器
bookshelf: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
gap: '24px',
padding: '24px 0',
} as CSSProperties, } as CSSProperties,
// 卡片内容区域样式 // 卡片内容区域样式
@@ -74,17 +112,28 @@ export const cardStyles = {
} as CSSProperties), } as CSSProperties),
}; };
// 卡片悬浮动画 - 增强版 // 卡片悬浮动画 - 增强版 (Subtle Lift)
export const cardHoverHandlers = { export const cardHoverHandlers = {
onMouseEnter: (e: React.MouseEvent<HTMLDivElement>) => { onMouseEnter: (e: React.MouseEvent<HTMLDivElement>) => {
const target = e.currentTarget; const target = e.currentTarget;
target.style.transform = 'translateY(-10px) scale(1.01)'; target.style.transform = 'translateY(-6px) rotateY(-2deg)'; // 悬浮时轻微翻起
target.style.boxShadow = '0 20px 40px rgba(77, 128, 136, 0.2), 0 8px 16px rgba(0, 0, 0, 0.08)';
// 统一书本悬浮态
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<HTMLDivElement>) => { onMouseLeave: (e: React.MouseEvent<HTMLDivElement>) => {
const target = e.currentTarget; const target = e.currentTarget;
target.style.transform = 'translateY(0) scale(1)'; target.style.transform = 'translateY(0) rotateY(0)';
target.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04)';
// 统一恢复基础阴影
target.style.boxShadow = `
0 2px 8px rgba(0, 0, 0, 0.04),
4px 0 8px rgba(0, 0, 0, 0.02)
`;
}, },
}; };
@@ -1,20 +1,30 @@
import { useState } from 'react'; import { useState } from 'react';
import { FloatButton } from 'antd'; import { FloatButton, Grid } from 'antd';
import { FileTextOutlined } from '@ant-design/icons'; import { FileTextOutlined } from '@ant-design/icons';
import ChangelogModal from './ChangelogModal'; import ChangelogModal from './ChangelogModal';
const { useBreakpoint } = Grid;
export default function ChangelogFloatingButton() { export default function ChangelogFloatingButton() {
const [showChangelog, setShowChangelog] = useState(false); const [showChangelog, setShowChangelog] = useState(false);
const screens = useBreakpoint();
const isMobile = !screens.md;
return ( return (
<div style={{ position: 'fixed', zIndex: 9999 }}> <>
<FloatButton <FloatButton
icon={<FileTextOutlined />} icon={<FileTextOutlined />}
type="primary" type="primary"
tooltip="查看更新日志" tooltip="查看更新日志"
style={{ style={{
// 桌面端时,确保按钮在主内容区域内(侧边栏右侧)
right: 24, right: 24,
bottom: 100, bottom: 100,
// 移动端无侧边栏,不需要额外处理
...(isMobile ? {} : {
// 确保 zIndex 低于侧边栏但高于内容
zIndex: 999,
}),
}} }}
onClick={() => setShowChangelog(true)} onClick={() => setShowChangelog(true)}
/> />
@@ -23,6 +33,6 @@ export default function ChangelogFloatingButton() {
visible={showChangelog} visible={showChangelog}
onClose={() => setShowChangelog(false)} onClose={() => setShowChangelog(false)}
/> />
</div> </>
); );
} }
@@ -55,6 +55,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
// 清除可能存在的轮询 // 清除可能存在的轮询
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible, chapterId]); }, [visible, chapterId]);
// 🔧 新增:独立的章节信息加载函数 // 🔧 新增:独立的章节信息加载函数
@@ -78,8 +78,9 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
}, 500); }, 500);
onClose(); onClose();
} catch (error: any) { } catch (error: unknown) {
message.error(error.message || '应用失败'); const err = error as Error;
message.error(err.message || '应用失败');
} finally { } finally {
setApplying(false); setApplying(false);
} }
@@ -119,7 +119,22 @@ const ChapterRegenerationModal: React.FC<ChapterRegenerationModalProps> = ({
setWordCount(0); 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, modification_source: values.modification_source,
custom_instructions: values.custom_instructions, custom_instructions: values.custom_instructions,
selected_suggestion_indices: selectedSuggestions, selected_suggestion_indices: selectedSuggestions,
@@ -158,7 +173,7 @@ const ChapterRegenerationModal: React.FC<ChapterRegenerationModalProps> = ({
currentWordCount = accumulatedContent.length; currentWordCount = accumulatedContent.length;
// 不再自己计算进度,完全依赖后端发送的progress消息 // 不再自己计算进度,完全依赖后端发送的progress消息
}, },
onResult: (data: any) => { onResult: (data: { word_count?: number }) => {
// 生成完成,确保使用最新的累积内容 // 生成完成,确保使用最新的累积内容
setProgress(100); setProgress(100);
setStatus('success'); setStatus('success');
@@ -183,11 +198,12 @@ const ChapterRegenerationModal: React.FC<ChapterRegenerationModalProps> = ({
} }
); );
} catch (error: any) { } catch (error: unknown) {
console.error('提交失败:', error); console.error('提交失败:', error);
setStatus('error'); setStatus('error');
setErrorMessage(error.message || '提交失败'); const err = error as Error;
message.error('操作失败: ' + (error.message || '未知错误')); setErrorMessage(err.message || '提交失败');
message.error('操作失败: ' + (err.message || '未知错误'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
+31 -26
View File
@@ -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 { 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 { EditOutlined, PlusOutlined, DeleteOutlined, TrophyOutlined } from '@ant-design/icons';
import axios from 'axios'; import axios from 'axios';
@@ -59,14 +59,7 @@ export const CharacterCareerCard: React.FC<Props> = ({
const [progressForm] = Form.useForm(); const [progressForm] = Form.useForm();
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
useEffect(() => { const fetchCharacterCareers = useCallback(async () => {
fetchCharacterCareers();
if (editable) {
fetchAllCareers();
}
}, [characterId]);
const fetchCharacterCareers = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await axios.get( const response = await axios.get(
@@ -75,14 +68,15 @@ export const CharacterCareerCard: React.FC<Props> = ({
); );
setMainCareer(response.data.main_career || null); setMainCareer(response.data.main_career || null);
setSubCareers(response.data.sub_careers || []); setSubCareers(response.data.sub_careers || []);
} catch (error: any) { } catch (error: unknown) {
message.error(error.response?.data?.detail || '获取职业信息失败'); const axiosError = error as { response?: { data?: { detail?: string } } };
message.error(axiosError.response?.data?.detail || '获取职业信息失败');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [characterId]);
const fetchAllCareers = async () => { const fetchAllCareers = useCallback(async () => {
try { try {
const response = await axios.get(`${API_BASE_URL}/api/careers`, { const response = await axios.get(`${API_BASE_URL}/api/careers`, {
params: { project_id: projectId }, params: { project_id: projectId },
@@ -91,12 +85,19 @@ export const CharacterCareerCard: React.FC<Props> = ({
const main = response.data.main_careers || []; const main = response.data.main_careers || [];
const sub = response.data.sub_careers || []; const sub = response.data.sub_careers || [];
setAllCareers([...main, ...sub]); setAllCareers([...main, ...sub]);
} catch (error: any) { } catch (error: unknown) {
console.error('获取职业列表失败:', error); 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 { try {
await axios.post( await axios.post(
`${API_BASE_URL}/api/careers/character/${characterId}/careers/main`, `${API_BASE_URL}/api/careers/character/${characterId}/careers/main`,
@@ -108,12 +109,13 @@ export const CharacterCareerCard: React.FC<Props> = ({
mainForm.resetFields(); mainForm.resetFields();
fetchCharacterCareers(); fetchCharacterCareers();
onUpdate?.(); onUpdate?.();
} catch (error: any) { } catch (error: unknown) {
message.error(error.response?.data?.detail || '设置主职业失败'); 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 { try {
await axios.post( await axios.post(
`${API_BASE_URL}/api/careers/character/${characterId}/careers/sub`, `${API_BASE_URL}/api/careers/character/${characterId}/careers/sub`,
@@ -125,12 +127,13 @@ export const CharacterCareerCard: React.FC<Props> = ({
subForm.resetFields(); subForm.resetFields();
fetchCharacterCareers(); fetchCharacterCareers();
onUpdate?.(); onUpdate?.();
} catch (error: any) { } catch (error: unknown) {
message.error(error.response?.data?.detail || '添加副职业失败'); 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; if (!selectedCareer) return;
try { try {
@@ -144,8 +147,9 @@ export const CharacterCareerCard: React.FC<Props> = ({
progressForm.resetFields(); progressForm.resetFields();
fetchCharacterCareers(); fetchCharacterCareers();
onUpdate?.(); onUpdate?.();
} catch (error: any) { } catch (error: unknown) {
message.error(error.response?.data?.detail || '更新职业阶段失败'); const axiosError = error as { response?: { data?: { detail?: string } } };
message.error(axiosError.response?.data?.detail || '更新职业阶段失败');
} }
}; };
@@ -163,8 +167,9 @@ export const CharacterCareerCard: React.FC<Props> = ({
message.success('副职业删除成功'); message.success('副职业删除成功');
fetchCharacterCareers(); fetchCharacterCareers();
onUpdate?.(); onUpdate?.();
} catch (error: any) { } catch (error: unknown) {
message.error(error.response?.data?.detail || '删除副职业失败'); const axiosError = error as { response?: { data?: { detail?: string } } };
message.error(axiosError.response?.data?.detail || '删除副职业失败');
} }
} }
}); });
+18 -14
View File
@@ -1,6 +1,6 @@
import { Modal, Form, Input, InputNumber, Select, Tag, Space, Button, message, Divider } from 'antd'; import { Modal, Form, Input, InputNumber, Select, Tag, Space, Button, message, Divider } from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import type { ExpansionPlanData, Character } from '../types'; import type { ExpansionPlanData, Character } from '../types';
import { characterApi } from '../services/api'; import { characterApi } from '../services/api';
@@ -36,13 +36,7 @@ export default function ExpansionPlanEditor({
const [loadingCharacters, setLoadingCharacters] = useState(false); const [loadingCharacters, setLoadingCharacters] = useState(false);
// 加载项目角色列表 // 加载项目角色列表
useEffect(() => { const loadCharacters = useCallback(async () => {
if (visible && projectId) {
loadCharacters();
}
}, [visible, projectId]);
const loadCharacters = async () => {
try { try {
setLoadingCharacters(true); setLoadingCharacters(true);
setAvailableCharacters([]); // 重置为空数组 setAvailableCharacters([]); // 重置为空数组
@@ -53,8 +47,11 @@ export default function ExpansionPlanEditor({
let chars: Character[] = []; let chars: Character[] = [];
if (Array.isArray(response)) { if (Array.isArray(response)) {
chars = response; chars = response;
} else if (response && typeof response === 'object' && 'items' in response && Array.isArray((response as any).items)) { } else if (response && typeof response === 'object' && 'items' in response) {
chars = (response as any).items; const responseObj = response as { items?: Character[] };
if (Array.isArray(responseObj.items)) {
chars = responseObj.items;
}
} else { } else {
console.error('角色API返回格式异常:', response); console.error('角色API返回格式异常:', response);
message.warning('角色数据格式异常'); message.warning('角色数据格式异常');
@@ -62,14 +59,21 @@ export default function ExpansionPlanEditor({
setAvailableCharacters(chars); setAvailableCharacters(chars);
console.log('设置的角色列表:', chars); console.log('设置的角色列表:', chars);
} catch (error: any) { } catch (error: unknown) {
console.error('加载角色列表失败:', error); console.error('加载角色列表失败:', error);
setAvailableCharacters([]); setAvailableCharacters([]);
message.error('加载角色列表失败: ' + (error?.message || '未知错误')); const err = error as Error;
message.error('加载角色列表失败: ' + (err?.message || '未知错误'));
} finally { } finally {
setLoadingCharacters(false); setLoadingCharacters(false);
} }
}; }, [projectId]);
useEffect(() => {
if (visible && projectId) {
loadCharacters();
}
}, [visible, projectId, loadCharacters]);
// 当planData或chapterSummary变化时更新状态 // 当planData或chapterSummary变化时更新状态
useEffect(() => { useEffect(() => {
@@ -325,7 +329,7 @@ export default function ExpansionPlanEditor({
step={100} step={100}
style={{ width: '100%' }} style={{ width: '100%' }}
formatter={(value) => `${value}`} formatter={(value) => `${value}`}
parser={(value) => value?.replace(' 字', '') as any} parser={(value) => Number(value?.replace(' 字', '')) as 500 | 10000}
/> />
</Form.Item> </Form.Item>
+10 -4
View File
@@ -8,7 +8,12 @@ import { useNavigate } from 'react-router-dom';
const { Text } = Typography; const { Text } = Typography;
export default function UserMenu() { interface UserMenuProps {
/** 是否总是显示完整信息(用于移动端侧边栏) */
showFullInfo?: boolean;
}
export default function UserMenu({ showFullInfo = false }: UserMenuProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [currentUser, setCurrentUser] = useState<User | null>(null); const [currentUser, setCurrentUser] = useState<User | null>(null);
const [showChangePassword, setShowChangePassword] = useState(false); const [showChangePassword, setShowChangePassword] = useState(false);
@@ -54,9 +59,10 @@ export default function UserMenu() {
message.success('密码修改成功'); message.success('密码修改成功');
setShowChangePassword(false); setShowChangePassword(false);
changePasswordForm.resetFields(); changePasswordForm.resetFields();
} catch (error: any) { } catch (error: unknown) {
console.error('修改密码失败:', error); console.error('修改密码失败:', error);
message.error(error.response?.data?.detail || '修改密码失败'); const err = error as { response?: { data?: { detail?: string } } };
message.error(err.response?.data?.detail || '修改密码失败');
} finally { } finally {
setChangingPassword(false); setChangingPassword(false);
} }
@@ -171,7 +177,7 @@ export default function UserMenu() {
</div> </div>
)} )}
</div> </div>
<Space direction="vertical" size={0} style={{ display: window.innerWidth <= 768 ? 'none' : 'flex' }}> <Space direction="vertical" size={0} style={{ display: (window.innerWidth <= 768 && !showFullInfo) ? 'none' : 'flex' }}>
<Text strong style={{ <Text strong style={{
color: 'var(--color-text-primary)', color: 'var(--color-text-primary)',
fontSize: 14, fontSize: 14,
+8 -2
View File
@@ -10,7 +10,13 @@ export default function AuthCallback() {
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const [showAnnouncement, setShowAnnouncement] = useState(false); const [showAnnouncement, setShowAnnouncement] = useState(false);
const [showPasswordModal, setShowPasswordModal] = useState(false); const [showPasswordModal, setShowPasswordModal] = useState(false);
const [passwordStatus, setPasswordStatus] = useState<any>(null); interface PasswordStatus {
has_password: boolean;
has_custom_password: boolean;
username: string;
default_password: string;
}
const [passwordStatus, setPasswordStatus] = useState<PasswordStatus | null>(null);
const [newPassword, setNewPassword] = useState(''); const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const [settingPassword, setSettingPassword] = useState(false); const [settingPassword, setSettingPassword] = useState(false);
@@ -187,7 +193,7 @@ export default function AuthCallback() {
setShowAnnouncement(true); setShowAnnouncement(true);
}, 500); }, 500);
} }
} catch (error) { } catch {
message.error('密码设置失败,请重试'); message.error('密码设置失败,请重试');
} finally { } finally {
setSettingPassword(false); setSettingPassword(false);
+34 -20
View File
@@ -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 { 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 { ThunderboltOutlined, PlusOutlined, EditOutlined, DeleteOutlined, TrophyOutlined } from '@ant-design/icons';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
@@ -46,26 +46,26 @@ export default function Careers() {
const [aiProgress, setAiProgress] = useState(0); const [aiProgress, setAiProgress] = useState(0);
const [aiMessage, setAiMessage] = useState(''); const [aiMessage, setAiMessage] = useState('');
useEffect(() => { const fetchCareers = useCallback(async () => {
if (projectId) {
fetchCareers();
}
}, [projectId]);
const fetchCareers = async () => {
try { try {
setLoading(true); setLoading(true);
const response: any = await api.get('/careers', { const response = await api.get('/careers', {
params: { project_id: projectId } params: { project_id: projectId }
}); }) as { main_careers?: Career[]; sub_careers?: Career[] };
setMainCareers(response.main_careers || []); setMainCareers(response.main_careers || []);
setSubCareers(response.sub_careers || []); setSubCareers(response.sub_careers || []);
} catch (error: any) { } catch (error: unknown) {
console.error('获取职业列表失败:', error); console.error('获取职业列表失败:', error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [projectId]);
useEffect(() => {
if (projectId) {
fetchCareers();
}
}, [projectId, fetchCareers]);
const handleOpenModal = (career?: Career) => { const handleOpenModal = (career?: Career) => {
if (career) { if (career) {
@@ -81,7 +81,18 @@ export default function Careers() {
setIsModalOpen(true); 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 { try {
// 解析阶段数据 // 解析阶段数据
const stagesText = values.stages || ''; const stagesText = values.stages || '';
@@ -124,8 +135,9 @@ export default function Careers() {
setIsModalOpen(false); setIsModalOpen(false);
form.resetFields(); form.resetFields();
fetchCareers(); fetchCareers();
} catch (error: any) { } catch (error: unknown) {
message.error(error.response?.data?.detail || '操作失败'); 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}`); await api.delete(`/careers/${id}`);
message.success('职业删除成功'); message.success('职业删除成功');
fetchCareers(); fetchCareers();
} catch (error: any) { } catch (error: unknown) {
message.error(error.response?.data?.detail || '删除失败'); 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); setIsAIModalOpen(false);
setAiGenerating(true); setAiGenerating(true);
setAiProgress(0); setAiProgress(0);
@@ -193,9 +206,10 @@ export default function Careers() {
setAiGenerating(false); setAiGenerating(false);
message.error('连接中断,生成失败'); message.error('连接中断,生成失败');
}; };
} catch (err: any) { } catch (err: unknown) {
setAiGenerating(false); setAiGenerating(false);
message.error(err.message || '启动生成失败'); const error = err as Error;
message.error(error.message || '启动生成失败');
} }
}; };
+15 -13
View File
@@ -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 { useParams, useNavigate } from 'react-router-dom';
import { Card, Spin, Alert, Button, Space, Switch, Drawer, message, Progress } from 'antd'; import { Card, Spin, Alert, Button, Space, Switch, Drawer, message, Progress } from 'antd';
import { import {
@@ -75,13 +75,7 @@ const ChapterReader: React.FC = () => {
const [analysisProgress, setAnalysisProgress] = useState(0); const [analysisProgress, setAnalysisProgress] = useState(0);
const [navigation, setNavigation] = useState<NavigationData | null>(null); const [navigation, setNavigation] = useState<NavigationData | null>(null);
useEffect(() => { const loadChapterData = useCallback(async () => {
if (chapterId) {
loadChapterData();
}
}, [chapterId]);
const loadChapterData = async () => {
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
@@ -130,13 +124,20 @@ const ChapterReader: React.FC = () => {
} else { } else {
setAnnotationsData(null); setAnnotationsData(null);
} }
} catch (err: any) { } catch (err: unknown) {
console.error('加载章节数据失败:', err); 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 { } finally {
setLoading(false); setLoading(false);
} }
}; }, [chapterId]);
useEffect(() => {
if (chapterId) {
loadChapterData();
}
}, [chapterId, loadChapterData]);
const handleAnnotationClick = (annotation: MemoryAnnotation) => { const handleAnnotationClick = (annotation: MemoryAnnotation) => {
setActiveAnnotationId(annotation.id); setActiveAnnotationId(annotation.id);
@@ -211,10 +212,11 @@ const ChapterReader: React.FC = () => {
} }
}, 30000); }, 30000);
} catch (err: any) { } catch (err: unknown) {
setAnalyzing(false); setAnalyzing(false);
const error = err as { response?: { data?: { detail?: string } } };
message.error({ message.error({
content: err.response?.data?.detail || '触发分析失败', content: error.response?.data?.detail || '触发分析失败',
key: 'analyze' key: 'analyze'
}); });
} }
+37 -35
View File
@@ -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 { useNavigate } from 'react-router-dom';
import { Card, Input, Button, Space, Typography, message, Spin, Modal } from 'antd'; import { Card, Input, Button, Space, Typography, message, Spin, Modal } from 'antd';
import { SendOutlined, ArrowLeftOutlined, ReloadOutlined } from '@ant-design/icons'; 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 { try {
// 只在对话阶段保存,生成阶段不保存 // 只在对话阶段保存,生成阶段不保存
if (currentStep === 'generating' || currentStep === 'complete') { if (currentStep === 'generating' || currentStep === 'complete') {
@@ -130,10 +140,10 @@ const Inspiration: React.FC = () => {
} catch (error) { } catch (error) {
console.error('保存缓存失败:', error); console.error('保存缓存失败:', error);
} }
}; }, [currentStep, messages, wizardData, initialIdea, selectedOptions, lastFailedRequest]);
// 从缓存恢复 // 从缓存恢复
const restoreFromCache = (): boolean => { const restoreFromCache = useCallback((): boolean => {
try { try {
const cached = localStorage.getItem(CACHE_KEY); const cached = localStorage.getItem(CACHE_KEY);
if (!cached) { if (!cached) {
@@ -174,17 +184,7 @@ const Inspiration: React.FC = () => {
clearCache(); clearCache();
return false; return false;
} }
}; }, [clearCache]);
// 清除缓存
const clearCache = () => {
try {
localStorage.removeItem(CACHE_KEY);
console.log('🗑️ 缓存已清除');
} catch (error) {
console.error('清除缓存失败:', error);
}
};
// ==================== 组件挂载时恢复缓存 ==================== // ==================== 组件挂载时恢复缓存 ====================
@@ -193,7 +193,7 @@ const Inspiration: React.FC = () => {
restoreFromCache(); restoreFromCache();
setCacheLoaded(true); setCacheLoaded(true);
} }
}, []); }, [cacheLoaded, restoreFromCache]);
// ==================== 自动保存:状态变化时保存 ==================== // ==================== 自动保存:状态变化时保存 ====================
@@ -206,7 +206,7 @@ const Inspiration: React.FC = () => {
}, 500); }, 500);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [messages, currentStep, wizardData, initialIdea, selectedOptions, lastFailedRequest, cacheLoaded]); }, [messages, currentStep, wizardData, initialIdea, selectedOptions, lastFailedRequest, cacheLoaded, saveToCache]);
// 自动滚动到底部 // 自动滚动到底部
const scrollToBottom = () => { const scrollToBottom = () => {
@@ -259,7 +259,7 @@ const Inspiration: React.FC = () => {
}; };
setMessages(prev => [...prev, aiMessage]); setMessages(prev => [...prev, aiMessage]);
setLastFailedRequest(null); setLastFailedRequest(null);
} catch (error: any) { } catch (error: unknown) {
console.error('重试失败:', error); console.error('重试失败:', error);
message.error('重试失败,请稍后再试'); message.error('重试失败,请稍后再试');
} finally { } finally {
@@ -307,7 +307,7 @@ const Inspiration: React.FC = () => {
const step = targetMessage.step as 'title' | 'description' | 'theme' | 'genre'; const step = targetMessage.step as 'title' | 'description' | 'theme' | 'genre';
// 构建上下文 // 构建上下文
const context: any = { const context: Partial<WizardData> & { initial_idea?: string } = {
initial_idea: initialIdea, initial_idea: initialIdea,
title: wizardData.title, title: wizardData.title,
description: wizardData.description, description: wizardData.description,
@@ -339,9 +339,11 @@ const Inspiration: React.FC = () => {
setMessages(prev => [...prev, aiMessage]); setMessages(prev => [...prev, aiMessage]);
message.success('已根据您的反馈重新生成选项'); message.success('已根据您的反馈重新生成选项');
} catch (error: any) { } catch (error: unknown) {
console.error('优化选项失败:', error); 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 { } finally {
setRefining(false); setRefining(false);
} }
@@ -406,9 +408,11 @@ const Inspiration: React.FC = () => {
} else { } else {
await handleCustomInput(userInput); await handleCustomInput(userInput);
} }
} catch (error: any) { } catch (error: unknown) {
console.error('发送消息失败:', error); 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 { } finally {
setLoading(false); setLoading(false);
} }
@@ -575,9 +579,11 @@ const Inspiration: React.FC = () => {
setWizardData(updatedData); setWizardData(updatedData);
await generateNextStep(updatedData); await generateNextStep(updatedData);
} catch (error: any) { } catch (error: unknown) {
console.error('选择选项失败:', error); 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 { } finally {
setLoading(false); setLoading(false);
} }
@@ -625,9 +631,11 @@ const Inspiration: React.FC = () => {
setWizardData(updatedData); setWizardData(updatedData);
await generateNextStep(updatedData); await generateNextStep(updatedData);
} catch (error: any) { } catch (error: unknown) {
console.error('处理自定义输入失败:', error); 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 { } finally {
setLoading(false); setLoading(false);
} }
@@ -1050,7 +1058,7 @@ const Inspiration: React.FC = () => {
return ( return (
<div style={{ <div style={{
minHeight: '100vh', minHeight: '100dvh',
background: 'var(--color-bg-base)', background: 'var(--color-bg-base)',
}}> }}>
{contextHolder} {contextHolder}
@@ -1118,7 +1126,7 @@ const Inspiration: React.FC = () => {
color: '#fff', color: '#fff',
}} }}
> >
{isMobile ? '返回' : '返回项目列表'} {isMobile ? '返回' : '返回首页'}
</Button> </Button>
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
@@ -1133,12 +1141,6 @@ const Inspiration: React.FC = () => {
> >
</Title> </Title>
<Text style={{
color: 'rgba(255,255,255,0.85)',
fontSize: isMobile ? 12 : 14,
}}>
</Text>
</div> </div>
{/* 重新开始按钮 - 只在对话进行中显示 */} {/* 重新开始按钮 - 只在对话进行中显示 */}
+1
View File
@@ -83,6 +83,7 @@ export default function Organizations() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]); }, [projectId]);
const loadCharacters = useCallback(async () => { const loadCharacters = useCallback(async () => {
+3 -1
View File
@@ -39,6 +39,7 @@ export default function ProjectWizardNew() {
setResumeProjectId(projectId); setResumeProjectId(projectId);
handleResumeGeneration(projectId); handleResumeGeneration(projectId);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams]); }, [searchParams]);
// 恢复未完成项目的生成 // 恢复未完成项目的生成
@@ -320,7 +321,7 @@ export default function ProjectWizardNew() {
return ( return (
<div style={{ <div style={{
minHeight: '100vh', minHeight: '100dvh',
background: 'var(--color-bg-base)', background: 'var(--color-bg-base)',
}}> }}>
{/* 顶部标题栏 - 固定不滚动 */} {/* 顶部标题栏 - 固定不滚动 */}
@@ -358,6 +359,7 @@ export default function ProjectWizardNew() {
color: '#fff', color: '#fff',
textShadow: '0 2px 4px rgba(0,0,0,0.1)', textShadow: '0 2px 4px rgba(0,0,0,0.1)',
}}> }}>
<RocketOutlined style={{ marginRight: 8 }} />
</Title> </Title>
+1
View File
@@ -61,6 +61,7 @@ export default function Relationships() {
if (projectId) { if (projectId) {
loadData(); loadData();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]); }, [projectId]);
const loadData = async () => { const loadData = async () => {
+19 -3
View File
@@ -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 { try {
const res = await adminApi.createUser(values); const res = await adminApi.createUser(values);
message.success('用户创建成功'); message.success('用户创建成功');
@@ -134,7 +143,14 @@ export default function UserManagement() {
setEditModalVisible(true); 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; if (!currentUser) return;
try { try {
@@ -290,7 +306,7 @@ export default function UserManagement() {
key: 'action', key: 'action',
width: isMobile ? 80 : 300, width: isMobile ? 80 : 300,
fixed: 'right' as const, fixed: 'right' as const,
render: (_: any, record: UserWithStatus) => { render: (_: unknown, record: UserWithStatus) => {
const isActive = record.is_active !== false; const isActive = record.is_active !== false;
// 移动端:使用下拉菜单 // 移动端:使用下拉菜单
+1 -1
View File
@@ -58,7 +58,7 @@ export default function WorldSetting() {
// 可以在这里显示生成的内容片段(可选) // 可以在这里显示生成的内容片段(可选)
console.log('生成片段:', chunk); console.log('生成片段:', chunk);
}, },
onResult: (result: any) => { onResult: (result: { time_period: string; location: string; atmosphere: string; rules: string }) => {
// 保存新生成的数据 // 保存新生成的数据
const newData = { const newData = {
time_period: result.time_period, time_period: result.time_period,
+1
View File
@@ -53,6 +53,7 @@ export default function WritingStyles() {
// 加载风格列表 - 如果有项目则加载项目风格(包含默认标记),否则加载用户风格 // 加载风格列表 - 如果有项目则加载项目风格(包含默认标记),否则加载用户风格
useEffect(() => { useEffect(() => {
loadStyles(); loadStyles();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentProject?.id]); }, [currentProject?.id]);
const loadStyles = async () => { const loadStyles = async () => {
+2 -2
View File
@@ -192,7 +192,7 @@ export const settingsApi = {
getAvailableModels: (params: { api_key: string; api_base_url: string; provider: string }) => getAvailableModels: (params: { api_key: string; api_base_url: string; provider: string }) =>
api.get<unknown, { provider: string; models: Array<{ value: string; label: string; description: string }>; count?: number }>('/settings/models', { params }), api.get<unknown, { provider: string; models: Array<{ value: string; label: string; description: string }>; 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<unknown, { api.post<unknown, {
success: boolean; success: boolean;
message: string; message: string;
@@ -200,7 +200,7 @@ export const settingsApi = {
provider?: string; provider?: string;
model?: string; model?: string;
response_preview?: string; response_preview?: string;
details?: Record<string, boolean>; details?: Record<string, boolean | number>;
error?: string; error?: string;
error_type?: string; error_type?: string;
suggestions?: string[]; suggestions?: string[];
+1 -1
View File
@@ -69,7 +69,7 @@ export async function checkLatestVersion(): Promise<VersionCheckResult> {
} }
throw new Error('无法从 Badge API 解析版本信息'); throw new Error('无法从 Badge API 解析版本信息');
} catch (error) { } catch {
// 失败时返回无更新 // 失败时返回无更新
return { return {
hasUpdate: false, hasUpdate: false,
+3
View File
@@ -112,4 +112,7 @@ export const EventNames = {
CHAPTER_UPDATED: 'chapter:updated', CHAPTER_UPDATED: 'chapter:updated',
CHAPTER_DELETED: 'chapter:deleted', CHAPTER_DELETED: 'chapter:deleted',
CHAPTER_NEEDS_REFRESH: 'chapter:needsRefresh', CHAPTER_NEEDS_REFRESH: 'chapter:needsRefresh',
// 视图切换事件
SWITCH_TO_MCP_VIEW: 'view:switchToMcp',
} as const; } as const;
+2 -2
View File
@@ -113,7 +113,7 @@ class SessionManager {
await this.refreshSession(); await this.refreshSession();
} }
} }
} catch (error) { } catch {
// 静默处理错误 // 静默处理错误
} }
} }
@@ -231,7 +231,7 @@ class SessionManager {
try { try {
await this.refreshSession(); await this.refreshSession();
return true; return true;
} catch (error) { } catch {
return false; return false;
} }
} }
+13 -7
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface SSEMessage { export interface SSEMessage {
type: 'progress' | 'chunk' | 'result' | 'error' | 'done'; type: 'progress' | 'chunk' | 'result' | 'error' | 'done';
message?: string; 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) { switch (message.type) {
case 'progress': case 'progress':
if (this.options.onProgress && message.progress !== undefined) { if (this.options.onProgress && message.progress !== undefined) {
@@ -129,6 +130,7 @@ export class SSEPostClient {
private options: SSEClientOptions; private options: SSEClientOptions;
private abortController: AbortController | null = null; private abortController: AbortController | null = null;
private accumulatedContent: string = ''; private accumulatedContent: string = '';
private resultData: any = null;
constructor(url: string, data: any, options: SSEClientOptions = {}) { constructor(url: string, data: any, options: SSEClientOptions = {}) {
this.url = url; this.url = url;
@@ -137,7 +139,12 @@ export class SSEPostClient {
} }
async connect(): Promise<any> { async connect(): Promise<any> {
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 { try {
this.abortController = new AbortController(); this.abortController = new AbortController();
@@ -232,10 +239,9 @@ export class SSEPostClient {
reject(error); 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) { switch (message.type) {
case 'progress': case 'progress':
if (this.options.onProgress && message.progress !== undefined) { if (this.options.onProgress && message.progress !== undefined) {
@@ -261,7 +267,7 @@ export class SSEPostClient {
if (this.options.onResult && message.data) { if (this.options.onResult && message.data) {
this.options.onResult(message.data); this.options.onResult(message.data);
} }
(this as any).resultData = message.data; this.resultData = message.data;
break; break;
case 'error': case 'error':
@@ -275,8 +281,8 @@ export class SSEPostClient {
if (this.options.onComplete) { if (this.options.onComplete) {
this.options.onComplete(); this.options.onComplete();
} }
if ((this as any).resultData) { if (this.resultData) {
resolve((this as any).resultData); resolve(this.resultData);
} else if (this.accumulatedContent) { } else if (this.accumulatedContent) {
resolve({ content: this.accumulatedContent }); resolve({ content: this.accumulatedContent });
} else { } else {