style:1.重构整个项目的主题颜色,样式风格采用中国风元素 2.优化更新日志逻辑,不再间隔1h自动刷新过于频繁触发403响应

This commit is contained in:
xiamuceer
2025-12-11 17:01:25 +08:00
parent 02bd2a2529
commit 46d56d9fd8
27 changed files with 2892 additions and 2329 deletions
+35 -35
View File
@@ -44,11 +44,11 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
resumeProjectId
}) => {
const navigate = useNavigate();
// 状态管理
const [loading, setLoading] = useState(false);
const [projectId, setProjectId] = useState<string>('');
// SSE流式进度状态
const [progress, setProgress] = useState(0);
const [progressMessage, setProgressMessage] = useState('');
@@ -58,7 +58,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
characters: 'pending',
outline: 'pending'
});
// 保存生成数据,用于重试
const [generationData, setGenerationData] = useState<GenerationConfig | null>(null);
// 保存世界观生成结果,用于后续步骤
@@ -132,7 +132,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
// 世界观已完成,从角色开始
message.info('世界观已完成,从角色步骤继续...');
setGenerationSteps({ worldBuilding: 'completed', characters: 'processing', outline: 'pending' });
// 获取世界观数据
const worldResult = {
project_id: projectIdParam,
@@ -143,7 +143,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
};
setWorldBuildingResult(worldResult);
setProgress(33);
await resumeFromCharacters(data, worldResult);
} else if (wizardStep === 2) {
// 世界观和角色已完成,从大纲开始
@@ -297,7 +297,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
message.success('项目创建成功!正在进入项目...');
clearStorage();
setLoading(false);
onComplete(pid);
setTimeout(() => {
navigate(`/project/${pid}`);
@@ -319,7 +319,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
// 步骤1: 生成世界观并创建项目
setGenerationSteps(prev => ({ ...prev, worldBuilding: 'processing' }));
setProgressMessage('正在生成世界观...');
const worldResult = await wizardStreamApi.generateWorldBuildingStream(
{
title: data.title,
@@ -367,7 +367,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
// 步骤2: 生成角色
setGenerationSteps(prev => ({ ...prev, characters: 'processing' }));
setProgressMessage('正在生成角色...');
await wizardStreamApi.generateCharactersStream(
{
project_id: createdProjectId,
@@ -406,7 +406,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
// 步骤3: 生成大纲
setGenerationSteps(prev => ({ ...prev, outline: 'processing' }));
setProgressMessage('正在生成大纲...');
await wizardStreamApi.generateCompleteOutlineStream(
{
project_id: createdProjectId,
@@ -441,15 +441,15 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
setProgressMessage('项目创建完成!正在跳转...');
message.success('项目创建成功!正在进入项目...');
clearStorage();
// 调用完成回调
onComplete(createdProjectId);
// 延迟1秒后自动跳转到项目详情页
setTimeout(() => {
navigate(`/project/${createdProjectId}`);
}, 1000);
} catch (error) {
const apiError = error as ApiError;
const errorMsg = apiError.response?.data?.detail || apiError.message || '未知错误';
@@ -634,11 +634,11 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
setProgressMessage('项目创建完成!正在跳转...');
message.success('项目创建成功!正在进入项目...');
setLoading(false);
// 调用完成回调
if (projectId) {
onComplete(projectId);
// 延迟1秒后自动跳转到项目详情页
setTimeout(() => {
navigate(`/project/${projectId}`);
@@ -733,11 +733,11 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
setProgressMessage('项目创建完成!正在跳转...');
message.success('项目创建成功!正在进入项目...');
setLoading(false);
// 调用完成回调
if (projectId) {
onComplete(projectId);
// 延迟1秒后自动跳转到项目详情页
setTimeout(() => {
navigate(`/project/${projectId}`);
@@ -748,15 +748,15 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
// 获取步骤状态图标和颜色
const getStepStatus = (step: GenerationStep) => {
if (step === 'completed') return { icon: <CheckCircleOutlined />, color: '#52c41a' };
if (step === 'processing') return { icon: <LoadingOutlined />, color: '#1890ff' };
if (step === 'error') return { icon: '✗', color: '#ff4d4f' };
return { icon: '○', color: '#d9d9d9' };
if (step === 'completed') return { icon: <CheckCircleOutlined />, color: 'var(--color-success)' };
if (step === 'processing') return { icon: <LoadingOutlined />, color: 'var(--color-primary)' };
if (step === 'error') return { icon: '✗', color: 'var(--color-error)' };
return { icon: '○', color: 'var(--color-text-quaternary)' };
};
const hasError = generationSteps.worldBuilding === 'error' ||
generationSteps.characters === 'error' ||
generationSteps.outline === 'error';
generationSteps.characters === 'error' ||
generationSteps.outline === 'error';
// 渲染生成进度页面
const renderGenerating = () => (
@@ -770,7 +770,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
level={isMobile ? 4 : 3}
style={{
marginBottom: 32,
color: '#fff',
color: 'var(--color-text-primary)',
wordBreak: 'break-word',
whiteSpace: 'normal',
overflowWrap: 'break-word'
@@ -784,8 +784,8 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
percent={progress}
status={hasError ? 'exception' : (progress === 100 ? 'success' : 'active')}
strokeColor={{
'0%': '#667eea',
'100%': '#764ba2',
'0%': 'var(--color-primary)',
'100%': 'var(--color-primary-active)',
}}
style={{ marginBottom: 24 }}
/>
@@ -794,7 +794,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
style={{
fontSize: isMobile ? 14 : 16,
marginBottom: 32,
color: hasError ? '#ff4d4f' : '#666',
color: hasError ? 'var(--color-error)' : 'var(--color-text-secondary)',
wordBreak: 'break-word',
whiteSpace: 'normal',
overflowWrap: 'break-word'
@@ -808,18 +808,18 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
size="small"
style={{
marginBottom: 24,
background: '#fff2f0',
borderColor: '#ffccc7',
background: 'var(--color-error-bg)',
borderColor: 'var(--color-error-border)',
textAlign: 'left',
maxWidth: '100%',
overflow: 'hidden'
}}
>
<Text strong style={{ color: '#ff4d4f' }}></Text>
<Text strong style={{ color: 'var(--color-error)' }}></Text>
<br />
<Text
style={{
color: '#666',
color: 'var(--color-text-secondary)',
fontSize: 14,
wordBreak: 'break-word',
whiteSpace: 'normal',
@@ -855,9 +855,9 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
alignItems: 'center',
justifyContent: 'space-between',
padding: isMobile ? '10px 12px' : '12px 20px',
background: step === 'processing' ? '#f0f5ff' : (step === 'error' ? '#fff2f0' : '#fafafa'),
background: step === 'processing' ? 'var(--color-info-bg)' : (step === 'error' ? 'var(--color-error-bg)' : 'var(--color-bg-layout)'),
borderRadius: 8,
border: `1px solid ${step === 'processing' ? '#d6e4ff' : (step === 'error' ? '#ffccc7' : '#f0f0f0')}`,
border: `1px solid ${step === 'processing' ? 'var(--color-info-border)' : (step === 'error' ? 'var(--color-error-border)' : 'var(--color-border-secondary)')}`,
gap: '8px',
maxWidth: '100%',
overflow: 'hidden'
@@ -894,7 +894,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
<Paragraph
type="secondary"
style={{
color: '#fff',
color: 'var(--color-text-secondary)',
opacity: 0.9,
wordBreak: 'break-word',
whiteSpace: 'normal',
@@ -904,7 +904,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
>
{hasError ? '生成过程中出现错误,请点击重试按钮重新生成' : '请耐心等待,AI正在为您精心创作...'}
</Paragraph>
{hasError && (
<Space style={{ marginTop: 16 }}>
<Button
@@ -918,7 +918,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
</Button>
</Space>
)}
</div>
);
+14 -14
View File
@@ -56,7 +56,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
<div style={{
marginBottom: '16px',
fontSize: '16px',
color: '#666',
color: 'var(--color-text-secondary)',
lineHeight: '1.6',
}}>
<p>👋 </p>
@@ -72,18 +72,18 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
<li>🐛 </li>
<li>📚 </li>
</ul>
<p style={{ fontWeight: 600, color: '#333', marginBottom: '16px' }}>
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '16px' }}>
</p>
</div>
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-start',
gap: '24px',
padding: '20px',
background: '#f5f5f5',
background: 'var(--color-bg-layout)',
borderRadius: '8px',
flexWrap: 'wrap',
}}>
@@ -94,7 +94,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
alignItems: 'center',
minWidth: '280px',
}}>
<p style={{ fontWeight: 600, color: '#333', marginBottom: '12px', fontSize: '15px' }}>
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '12px', fontSize: '15px' }}>
QQ交流群
</p>
{!qqImageError ? (
@@ -102,7 +102,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
background: '#fff',
background: 'var(--color-bg-container)',
borderRadius: '8px',
padding: '8px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
@@ -128,7 +128,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
background: '#fff',
background: 'var(--color-bg-container)',
borderRadius: '8px',
color: '#999',
}}>
@@ -144,7 +144,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
alignItems: 'center',
minWidth: '280px',
}}>
<p style={{ fontWeight: 600, color: '#333', marginBottom: '12px', fontSize: '15px' }}>
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '12px', fontSize: '15px' }}>
</p>
{!wxImageError ? (
@@ -152,7 +152,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
background: '#fff',
background: 'var(--color-bg-container)',
borderRadius: '8px',
padding: '8px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
@@ -178,7 +178,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
background: '#fff',
background: 'var(--color-bg-container)',
borderRadius: '8px',
color: '#999',
}}>
@@ -187,15 +187,15 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
)}
</div>
</div>
<div style={{
marginTop: '20px',
padding: '12px',
background: '#fff7e6',
background: 'var(--color-warning-bg)',
borderRadius: '8px',
border: '1px solid #ffd591',
border: '1px solid var(--color-warning-border)',
fontSize: '14px',
color: '#ad6800',
color: 'var(--color-warning)',
}}>
💡 "今日内不再展示""永不再展示"
</div>
+25 -33
View File
@@ -46,10 +46,11 @@ export default function AppFooter() {
right: 0,
backdropFilter: 'blur(20px) saturate(180%)',
WebkitBackdropFilter: 'blur(20px) saturate(180%)',
borderTop: '1px solid rgba(255, 255, 255, 0.2)',
borderTop: '1px solid var(--color-border)',
padding: isMobile ? '8px 12px' : '10px 16px',
zIndex: 100,
boxShadow: '0 -4px 16px rgba(0, 0, 0, 0.1), 0 -1px 4px rgba(0, 0, 0, 0.06)',
boxShadow: 'var(--shadow-card)',
backgroundColor: 'rgba(255, 255, 255, 0.8)', // 半透明背景以支持 backdrop-filter
}}
>
<div
@@ -77,29 +78,26 @@ export default function AppFooter() {
display: 'flex',
alignItems: 'center',
gap: 4,
color: '#fff',
textShadow: '0 1px 3px rgba(0, 0, 0, 0.3)',
color: 'var(--color-primary)',
cursor: hasUpdate ? 'pointer' : 'default',
}}
>
<strong style={{ color: '#fff' }}>{VERSION_INFO.projectName}</strong>
<strong style={{ color: 'var(--color-text-primary)' }}>{VERSION_INFO.projectName}</strong>
<span>{getVersionString()}</span>
</Text>
</Tooltip>
</Badge>
<Divider type="vertical" style={{ margin: '0 4px', borderColor: 'rgba(255, 255, 255, 0.3)' }} />
<Divider type="vertical" style={{ margin: '0 4px', borderColor: 'var(--color-border)' }} />
<Button
type="primary"
type="text"
size="small"
icon={<GiftOutlined />}
onClick={() => window.open('https://mumuverse.space:1588/', '_blank')}
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.4)',
color: 'var(--color-text-secondary)',
fontSize: 11,
height: 24,
padding: '0 8px',
padding: '0 4px',
display: 'flex',
alignItems: 'center',
gap: 4,
@@ -107,7 +105,7 @@ export default function AppFooter() {
>
</Button>
<Divider type="vertical" style={{ margin: '0 4px', borderColor: 'rgba(255, 255, 255, 0.3)' }} />
<Divider type="vertical" style={{ margin: '0 4px', borderColor: 'var(--color-border)' }} />
<Link
href={VERSION_INFO.githubUrl}
target="_blank"
@@ -117,8 +115,7 @@ export default function AppFooter() {
display: 'flex',
alignItems: 'center',
gap: 4,
color: '#fff',
textShadow: '0 1px 3px rgba(0, 0, 0, 0.3)',
color: 'var(--color-text-secondary)',
}}
>
<GithubOutlined style={{ fontSize: 12 }} />
@@ -126,8 +123,7 @@ export default function AppFooter() {
<Text
style={{
fontSize: 10,
color: 'rgba(255, 255, 255, 0.8)',
textShadow: '0 1px 2px rgba(0, 0, 0, 0.3)',
color: 'var(--color-text-tertiary)',
}}
>
<ClockCircleOutlined style={{ fontSize: 10, marginRight: 4 }} />
@@ -139,7 +135,7 @@ export default function AppFooter() {
<Space
direction="horizontal"
size={12}
split={<Divider type="vertical" style={{ borderColor: 'rgba(255, 255, 255, 0.3)' }} />}
split={<Divider type="vertical" style={{ borderColor: 'var(--color-border)' }} />}
style={{
display: 'flex',
justifyContent: 'center',
@@ -156,8 +152,8 @@ export default function AppFooter() {
display: 'flex',
alignItems: 'center',
gap: 6,
color: '#fff',
textShadow: '0 1px 3px rgba(0, 0, 0, 0.3)',
color: 'var(--color-text-secondary)',
textShadow: 'none',
cursor: hasUpdate ? 'pointer' : 'default',
transition: 'all 0.3s',
}}
@@ -172,7 +168,7 @@ export default function AppFooter() {
}
}}
>
<strong style={{ color: '#fff' }}>{VERSION_INFO.projectName}</strong>
<strong style={{ color: 'var(--color-text-primary)' }}>{VERSION_INFO.projectName}</strong>
<span>{getVersionString()}</span>
</Text>
</Tooltip>
@@ -188,8 +184,7 @@ export default function AppFooter() {
display: 'flex',
alignItems: 'center',
gap: 6,
color: '#fff',
textShadow: '0 1px 3px rgba(0, 0, 0, 0.3)',
color: 'var(--color-text-secondary)',
}}
>
<GithubOutlined style={{ fontSize: 13 }} />
@@ -203,8 +198,7 @@ export default function AppFooter() {
rel="noopener noreferrer"
style={{
fontSize: 12,
color: '#fff',
textShadow: '0 1px 3px rgba(0, 0, 0, 0.3)',
color: 'var(--color-text-secondary)',
}}
>
LinuxDO
@@ -216,9 +210,9 @@ export default function AppFooter() {
icon={<GiftOutlined style={{ fontSize: 14 }} />}
onClick={() => window.open('https://mumuverse.space:1588/', '_blank')}
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
background: 'var(--color-primary)',
border: 'none',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.5)',
boxShadow: '0 4px 12px rgba(77, 128, 136, 0.3)',
fontSize: 13,
height: 32,
padding: '0 20px',
@@ -250,8 +244,7 @@ export default function AppFooter() {
display: 'flex',
alignItems: 'center',
gap: 6,
color: '#fff',
textShadow: '0 1px 3px rgba(0, 0, 0, 0.3)',
color: 'var(--color-text-secondary)',
}}
>
<CopyrightOutlined style={{ fontSize: 11 }} />
@@ -265,8 +258,7 @@ export default function AppFooter() {
display: 'flex',
alignItems: 'center',
gap: 4,
color: 'rgba(255, 255, 255, 0.9)',
textShadow: '0 1px 3px rgba(0, 0, 0, 0.3)',
color: 'var(--color-text-tertiary)',
}}
>
<ClockCircleOutlined style={{ fontSize: 12 }} />
@@ -280,12 +272,12 @@ export default function AppFooter() {
display: 'flex',
alignItems: 'center',
gap: 4,
color: '#fff',
textShadow: '0 1px 3px rgba(0, 0, 0, 0.3)',
color: 'var(--color-text-secondary)',
textShadow: '0 1px 3px rgba(0, 0, 0, 0.05)',
}}
>
<span>Made with</span>
<HeartFilled style={{ color: '#ff4d4f', fontSize: 11 }} />
<HeartFilled style={{ color: 'var(--color-error)', fontSize: 11 }} />
<span>by {VERSION_INFO.author}</span>
</Text>
</Space>
+16 -15
View File
@@ -18,7 +18,7 @@ export const cardStyles = {
// height: 320,
display: 'flex',
flexDirection: 'column',
borderColor: '#1890ff',
borderColor: 'var(--color-info)',
borderRadius: 12,
} as CSSProperties,
@@ -27,19 +27,20 @@ export const cardStyles = {
// height: 320,
display: 'flex',
flexDirection: 'column',
borderColor: '#52c41a',
backgroundColor: '#f6ffed',
borderColor: 'var(--color-success)',
backgroundColor: 'var(--color-bg-base)', // 使用柔和的背景色
borderRadius: 12,
} as CSSProperties,
// 项目卡片样式
// 项目卡片样式 - 现代化设计
project: {
height: '100%',
borderRadius: 16,
borderRadius: 20,
overflow: 'hidden',
background: '#fff',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.08)',
transition: 'all 0.3s ease',
background: 'var(--color-bg-container)',
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)',
border: '1px solid rgba(0, 0, 0, 0.04)',
} as CSSProperties,
// 卡片内容区域样式
@@ -73,17 +74,17 @@ export const cardStyles = {
} as CSSProperties),
};
// 卡片悬浮动画
// 卡片悬浮动画 - 增强版
export const cardHoverHandlers = {
onMouseEnter: (e: React.MouseEvent<HTMLDivElement>) => {
const target = e.currentTarget;
target.style.transform = 'translateY(-8px)';
target.style.boxShadow = '0 12px 32px rgba(0, 0, 0, 0.15)';
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)';
},
onMouseLeave: (e: React.MouseEvent<HTMLDivElement>) => {
const target = e.currentTarget;
target.style.transform = 'translateY(0)';
target.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.08)';
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)';
},
};
@@ -113,12 +114,12 @@ export const textStyles = {
fontSize: 12,
color: 'rgba(0, 0, 0, 0.45)',
} as CSSProperties,
value: {
fontSize: 14,
color: 'rgba(0, 0, 0, 0.85)',
} as CSSProperties,
description: {
fontSize: 12,
color: 'rgba(0, 0, 0, 0.45)',
+47 -67
View File
@@ -16,10 +16,7 @@ import {
import {
fetchChangelog,
groupChangelogByDate,
getCachedChangelog,
cacheChangelog,
markChangelogFetched,
shouldFetchChangelog,
clearChangelogCache,
type ChangelogEntry,
} from '../services/changelogService';
@@ -50,35 +47,15 @@ export default function ChangelogModal({ visible, onClose }: ChangelogModalProps
const [hasMore, setHasMore] = useState(true);
// 加载更新日志
// 每次用户打开窗口时才同步获取最新数据,不自动刷新
const loadChangelog = async (pageNum: number = 1, append: boolean = false) => {
setLoading(true);
setError(null);
try {
// 如果是第一页,先尝试使用缓存
if (pageNum === 1 && !append) {
const cached = getCachedChangelog();
if (cached && cached.length > 0) {
setChangelog(cached);
// 后台刷新
if (shouldFetchChangelog()) {
fetchChangelog(pageNum, 30)
.then(entries => {
setChangelog(entries);
cacheChangelog(entries);
markChangelogFetched();
})
.catch(console.error);
}
setLoading(false);
return;
}
}
// 每次打开都从网络获取最新数据
const entries = await fetchChangelog(pageNum, 30);
if (entries.length === 0) {
setHasMore(false);
} else {
@@ -86,10 +63,9 @@ export default function ChangelogModal({ visible, onClose }: ChangelogModalProps
setChangelog(prev => [...prev, ...entries]);
} else {
setChangelog(entries);
// 缓存第一页数据
// 缓存第一页数据(用于分页加载时的数据持久化)
if (pageNum === 1) {
cacheChangelog(entries);
markChangelogFetched();
}
}
}
@@ -137,7 +113,7 @@ export default function ChangelogModal({ visible, onClose }: ChangelogModalProps
if (diffDays === 0) return '今天';
if (diffDays === 1) return '昨天';
if (diffDays < 7) return `${diffDays} 天前`;
return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' });
};
@@ -180,10 +156,10 @@ export default function ChangelogModal({ visible, onClose }: ChangelogModalProps
<div style={{
padding: '16px',
marginBottom: '16px',
background: '#fff2e8',
border: '1px solid #ffbb96',
background: 'var(--color-error-bg)',
border: '1px solid var(--color-error-border)',
borderRadius: '4px',
color: '#d4380d',
color: 'var(--color-error)',
}}>
{error}
</div>
@@ -199,16 +175,16 @@ export default function ChangelogModal({ visible, onClose }: ChangelogModalProps
<>
{sortedDates.map(date => {
const entries = groupedChangelog.get(date) || [];
return (
<div key={date} style={{ marginBottom: '32px' }}>
<div style={{
fontSize: '16px',
fontWeight: 600,
color: '#1890ff',
color: 'var(--color-primary)',
marginBottom: '16px',
paddingBottom: '8px',
borderBottom: '2px solid #e8e8e8',
borderBottom: '2px solid var(--color-border-secondary)',
}}>
<ClockCircleOutlined style={{ marginRight: '8px' }} />
{formatDate(date)}
@@ -217,7 +193,7 @@ export default function ChangelogModal({ visible, onClose }: ChangelogModalProps
<Timeline>
{entries.map(entry => {
const config = typeConfig[entry.type] || typeConfig.other;
return (
<Timeline.Item
key={entry.id}
@@ -226,8 +202,8 @@ export default function ChangelogModal({ visible, onClose }: ChangelogModalProps
width: '24px',
height: '24px',
borderRadius: '50%',
background: '#fff',
border: `2px solid ${config.color === 'default' ? '#d9d9d9' : config.color}`,
background: 'var(--color-bg-container)',
border: `2px solid ${config.color === 'default' ? 'var(--color-border)' : config.color}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
@@ -245,7 +221,7 @@ export default function ChangelogModal({ visible, onClose }: ChangelogModalProps
{entry.scope && (
<Tag color="blue">{entry.scope}</Tag>
)}
<span style={{ color: '#999', fontSize: '12px' }}>
<span style={{ color: 'var(--color-text-tertiary)', fontSize: '12px' }}>
{formatTime(entry.date)}
</span>
</Space>
@@ -254,7 +230,7 @@ export default function ChangelogModal({ visible, onClose }: ChangelogModalProps
marginTop: '8px',
fontSize: '14px',
lineHeight: '1.6',
color: '#333',
color: 'var(--color-text-primary)',
}}>
{entry.message}
</div>
@@ -263,7 +239,7 @@ export default function ChangelogModal({ visible, onClose }: ChangelogModalProps
{entry.author.avatar && (
<Avatar size="small" src={entry.author.avatar} />
)}
<span style={{ color: '#666', fontSize: '13px' }}>
<span style={{ color: 'var(--color-text-secondary)', fontSize: '13px' }}>
{entry.author.username || entry.author.name}
</span>
<a
@@ -284,42 +260,46 @@ export default function ChangelogModal({ visible, onClose }: ChangelogModalProps
);
})}
{hasMore && (
<div style={{ textAlign: 'center', marginTop: '24px' }}>
<Button
type="default"
onClick={handleLoadMore}
loading={loading}
>
</Button>
</div>
)}
{
hasMore && (
<div style={{ textAlign: 'center', marginTop: '24px' }}>
<Button
type="default"
onClick={handleLoadMore}
loading={loading}
>
</Button>
</div>
)
}
{!hasMore && changelog.length > 0 && (
<div style={{
textAlign: 'center',
color: '#999',
padding: '16px 0',
fontSize: '14px',
}}>
</div>
)}
{
!hasMore && changelog.length > 0 && (
<div style={{
textAlign: 'center',
color: 'var(--color-text-tertiary)',
padding: '16px 0',
fontSize: '14px',
}}>
</div>
)
}
</>
)}
<div style={{
marginTop: '24px',
padding: '12px',
background: '#f0f5ff',
background: 'var(--color-info-bg)',
borderRadius: '4px',
border: '1px solid #adc6ff',
border: '1px solid var(--color-info-border)',
fontSize: '13px',
color: '#1d39c4',
color: 'var(--color-primary)',
}}>
💡 GitHub
💡 GitHub
</div>
</Modal>
</Modal >
);
}
+188 -188
View File
@@ -42,14 +42,14 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
if (visible && chapterId) {
fetchAnalysisStatus();
}
// 监听窗口大小变化
const handleResize = () => {
setIsMobile(isMobileDevice());
};
window.addEventListener('resize', handleResize);
// 清理函数:组件卸载或关闭时清除轮询
return () => {
window.removeEventListener('resize', handleResize);
@@ -79,33 +79,33 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
try {
setLoading(true);
setError(null);
// 🔧 使用独立的章节加载函数
await loadChapterInfo();
const response = await fetch(`/api/chapters/${chapterId}/analysis/status`);
if (response.status === 404) {
setTask(null);
setError('该章节还未进行分析');
return;
}
if (!response.ok) {
throw new Error('获取分析状态失败');
}
const taskData: AnalysisTask = await response.json();
// 如果状态为 none(无任务),设置 task 为 null,让前端显示"开始分析"按钮
if (taskData.status === 'none' || !taskData.has_task) {
setTask(null);
setError(null); // 清除错误,这不是错误状态
return;
}
setTask(taskData);
if (taskData.status === 'completed') {
await fetchAnalysisResult();
} else if (taskData.status === 'running' || taskData.status === 'pending') {
@@ -137,10 +137,10 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
try {
const response = await fetch(`/api/chapters/${chapterId}/analysis/status`);
if (!response.ok) return;
const taskData: AnalysisTask = await response.json();
setTask(taskData);
if (taskData.status === 'completed') {
clearInterval(pollInterval);
await fetchAnalysisResult();
@@ -163,19 +163,19 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
try {
setLoading(true);
setError(null);
// 🔧 触发分析前先刷新章节内容,确保分析的是最新内容
await loadChapterInfo();
const response = await fetch(`/api/chapters/${chapterId}/analyze`, {
method: 'POST'
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || '触发分析失败');
}
// 触发成功后立即关闭Modal,让父组件的状态管理接管
onClose();
} catch (err) {
@@ -188,16 +188,16 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
const renderStatusIcon = () => {
if (!task) return null;
switch (task.status) {
case 'pending':
return <ClockCircleOutlined style={{ color: '#faad14' }} />;
return <ClockCircleOutlined style={{ color: 'var(--color-warning)' }} />;
case 'running':
return <Spin />;
case 'completed':
return <CheckCircleOutlined style={{ color: '#52c41a' }} />;
return <CheckCircleOutlined style={{ color: 'var(--color-success)' }} />;
case 'failed':
return <CloseCircleOutlined style={{ color: '#ff4d4f' }} />;
return <CloseCircleOutlined style={{ color: 'var(--color-error)' }} />;
default:
return null;
}
@@ -205,7 +205,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
const renderProgress = () => {
if (!task || task.status === 'completed') return null;
return (
<div style={{
padding: '40px',
@@ -225,7 +225,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
fontSize: 20,
fontWeight: 'bold',
marginTop: 16,
color: task.status === 'failed' ? '#ff4d4f' : '#262626'
color: task.status === 'failed' ? 'var(--color-error)' : 'var(--color-text-primary)'
}}>
{task.status === 'pending' && '等待分析...'}
{task.status === 'running' && 'AI正在分析中...'}
@@ -241,7 +241,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
}}>
<div style={{
height: 12,
background: '#f0f0f0',
background: 'var(--color-bg-layout)',
borderRadius: 6,
overflow: 'hidden',
marginBottom: 12
@@ -249,10 +249,10 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
<div style={{
height: '100%',
background: task.status === 'failed'
? 'linear-gradient(90deg, #ff4d4f 0%, #ff7875 100%)'
? 'var(--color-error)'
: task.progress === 100
? 'linear-gradient(90deg, #52c41a 0%, #73d13d 100%)'
: 'linear-gradient(90deg, #1890ff 0%, #40a9ff 100%)',
? 'var(--color-success)'
: 'var(--color-primary)',
width: `${task.progress}%`,
transition: 'all 0.3s ease',
borderRadius: 6,
@@ -261,14 +261,14 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
: 'none'
}} />
</div>
{/* 进度百分比 */}
<div style={{
textAlign: 'center',
fontSize: 32,
fontWeight: 'bold',
color: task.status === 'failed' ? '#ff4d4f' :
task.progress === 100 ? '#52c41a' : '#1890ff',
color: task.status === 'failed' ? 'var(--color-error)' :
task.progress === 100 ? 'var(--color-success)' : 'var(--color-primary)',
marginBottom: 8
}}>
{task.progress}%
@@ -279,7 +279,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
<div style={{
textAlign: 'center',
fontSize: 16,
color: '#595959',
color: 'var(--color-text-secondary)',
minHeight: 24,
marginBottom: 16
}}>
@@ -307,7 +307,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
<div style={{
textAlign: 'center',
fontSize: 13,
color: '#8c8c8c',
color: 'var(--color-text-tertiary)',
marginTop: 16
}}>
@@ -320,7 +320,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
// 将分析建议转换为重新生成组件需要的格式
const convertSuggestionsForRegeneration = () => {
if (!analysis?.analysis?.suggestions) return [];
return analysis.analysis.suggestions.map((suggestion, index) => ({
category: '改进建议',
content: suggestion,
@@ -330,9 +330,9 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
const renderAnalysisResult = () => {
if (!analysis) return null;
const { analysis: analysis_data, memories } = analysis;
return (
<Tabs
defaultActiveKey="overview"
@@ -366,7 +366,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
style={{ marginBottom: 16 }}
/>
)}
<Card title="整体评分" style={{ marginBottom: 16 }} size={isMobile ? 'small' : 'default'}>
<Row gutter={isMobile ? 8 : 16}>
<Col span={isMobile ? 12 : 6}>
@@ -374,7 +374,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
title="整体质量"
value={analysis_data.overall_quality_score || 0}
suffix="/ 10"
valueStyle={{ color: '#3f8600' }}
valueStyle={{ color: 'var(--color-success)' }}
/>
</Col>
<Col span={isMobile ? 12 : 6}>
@@ -400,7 +400,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
</Col>
</Row>
</Card>
{analysis_data.analysis_report && (
<Card title="分析摘要" style={{ marginBottom: 16 }} size={isMobile ? 'small' : 'default'}>
<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit', fontSize: isMobile ? 13 : 14 }}>
@@ -408,7 +408,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
</pre>
</Card>
)}
{analysis_data.suggestions && analysis_data.suggestions.length > 0 && (
<Card title={<><BulbOutlined /> </>} size={isMobile ? 'small' : 'default'}>
<List
@@ -431,27 +431,27 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
children: (
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card size={isMobile ? 'small' : 'default'}>
{analysis_data.hooks && analysis_data.hooks.length > 0 ? (
<List
dataSource={analysis_data.hooks}
renderItem={(hook) => (
<List.Item>
<List.Item.Meta
title={
<div>
<Tag color="blue">{hook.type}</Tag>
<Tag color="orange">{hook.position}</Tag>
<Tag color="red">: {hook.strength}/10</Tag>
</div>
}
description={hook.content}
/>
</List.Item>
)}
/>
) : (
<Empty description="暂无钩子" />
)}
{analysis_data.hooks && analysis_data.hooks.length > 0 ? (
<List
dataSource={analysis_data.hooks}
renderItem={(hook) => (
<List.Item>
<List.Item.Meta
title={
<div>
<Tag color="blue">{hook.type}</Tag>
<Tag color="orange">{hook.position}</Tag>
<Tag color="red">: {hook.strength}/10</Tag>
</div>
}
description={hook.content}
/>
</List.Item>
)}
/>
) : (
<Empty description="暂无钩子" />
)}
</Card>
</div>
)
@@ -463,32 +463,32 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
children: (
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card size={isMobile ? 'small' : 'default'}>
{analysis_data.foreshadows && analysis_data.foreshadows.length > 0 ? (
<List
dataSource={analysis_data.foreshadows}
renderItem={(foreshadow) => (
<List.Item>
<List.Item.Meta
title={
<div>
<Tag color={foreshadow.type === 'planted' ? 'green' : 'purple'}>
{foreshadow.type === 'planted' ? '已埋下' : '已回收'}
</Tag>
<Tag>: {foreshadow.strength}/10</Tag>
<Tag>: {foreshadow.subtlety}/10</Tag>
{foreshadow.reference_chapter && (
<Tag color="cyan">{foreshadow.reference_chapter}</Tag>
)}
</div>
}
description={foreshadow.content}
/>
</List.Item>
)}
/>
) : (
<Empty description="暂无伏笔" />
)}
{analysis_data.foreshadows && analysis_data.foreshadows.length > 0 ? (
<List
dataSource={analysis_data.foreshadows}
renderItem={(foreshadow) => (
<List.Item>
<List.Item.Meta
title={
<div>
<Tag color={foreshadow.type === 'planted' ? 'green' : 'purple'}>
{foreshadow.type === 'planted' ? '已埋下' : '已回收'}
</Tag>
<Tag>: {foreshadow.strength}/10</Tag>
<Tag>: {foreshadow.subtlety}/10</Tag>
{foreshadow.reference_chapter && (
<Tag color="cyan">{foreshadow.reference_chapter}</Tag>
)}
</div>
}
description={foreshadow.content}
/>
</List.Item>
)}
/>
) : (
<Empty description="暂无伏笔" />
)}
</Card>
</div>
)
@@ -500,41 +500,41 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
children: (
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card size={isMobile ? 'small' : 'default'}>
{analysis_data.emotional_tone ? (
<div>
<Row gutter={isMobile ? 8 : 16} style={{ marginBottom: isMobile ? 16 : 24 }}>
<Col span={isMobile ? 24 : 12}>
<Statistic
title="主导情绪"
value={analysis_data.emotional_tone}
/>
</Col>
<Col span={isMobile ? 24 : 12}>
<Statistic
title="情感强度"
value={(analysis_data.emotional_intensity * 10).toFixed(1)}
suffix="/ 10"
/>
</Col>
</Row>
<Card type="inner" title="剧情阶段" size="small">
<p><strong></strong>{analysis_data.plot_stage}</p>
<p><strong></strong>{analysis_data.conflict_level} / 10</p>
{analysis_data.conflict_types && analysis_data.conflict_types.length > 0 && (
<div style={{ marginTop: 8 }}>
<strong></strong>
{analysis_data.conflict_types.map((type, idx) => (
<Tag key={idx} color="red" style={{ margin: 4 }}>
{type}
</Tag>
))}
</div>
)}
</Card>
</div>
) : (
<Empty description="暂无情感分析" />
)}
{analysis_data.emotional_tone ? (
<div>
<Row gutter={isMobile ? 8 : 16} style={{ marginBottom: isMobile ? 16 : 24 }}>
<Col span={isMobile ? 24 : 12}>
<Statistic
title="主导情绪"
value={analysis_data.emotional_tone}
/>
</Col>
<Col span={isMobile ? 24 : 12}>
<Statistic
title="情感强度"
value={(analysis_data.emotional_intensity * 10).toFixed(1)}
suffix="/ 10"
/>
</Col>
</Row>
<Card type="inner" title="剧情阶段" size="small">
<p><strong></strong>{analysis_data.plot_stage}</p>
<p><strong></strong>{analysis_data.conflict_level} / 10</p>
{analysis_data.conflict_types && analysis_data.conflict_types.length > 0 && (
<div style={{ marginTop: 8 }}>
<strong></strong>
{analysis_data.conflict_types.map((type, idx) => (
<Tag key={idx} color="red" style={{ margin: 4 }}>
{type}
</Tag>
))}
</div>
)}
</Card>
</div>
) : (
<Empty description="暂无情感分析" />
)}
</Card>
</div>
)
@@ -546,37 +546,37 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
children: (
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card size={isMobile ? 'small' : 'default'}>
{analysis_data.character_states && analysis_data.character_states.length > 0 ? (
<List
dataSource={analysis_data.character_states}
renderItem={(char) => (
<List.Item>
<Card
type="inner"
title={char.character_name}
size="small"
style={{ width: '100%' }}
>
<p><strong></strong>{char.state_before} {char.state_after}</p>
<p><strong></strong>{char.psychological_change}</p>
<p><strong></strong>{char.key_event}</p>
{char.relationship_changes && Object.keys(char.relationship_changes).length > 0 && (
<div>
<strong></strong>
{Object.entries(char.relationship_changes).map(([name, change]) => (
<Tag key={name} color="blue" style={{ margin: 4 }}>
{name}: {change}
</Tag>
))}
</div>
)}
</Card>
</List.Item>
)}
/>
) : (
<Empty description="暂无角色分析" />
)}
{analysis_data.character_states && analysis_data.character_states.length > 0 ? (
<List
dataSource={analysis_data.character_states}
renderItem={(char) => (
<List.Item>
<Card
type="inner"
title={char.character_name}
size="small"
style={{ width: '100%' }}
>
<p><strong></strong>{char.state_before} {char.state_after}</p>
<p><strong></strong>{char.psychological_change}</p>
<p><strong></strong>{char.key_event}</p>
{char.relationship_changes && Object.keys(char.relationship_changes).length > 0 && (
<div>
<strong></strong>
{Object.entries(char.relationship_changes).map(([name, change]) => (
<Tag key={name} color="blue" style={{ margin: 4 }}>
{name}: {change}
</Tag>
))}
</div>
)}
</Card>
</List.Item>
)}
/>
) : (
<Empty description="暂无角色分析" />
)}
</Card>
</div>
)
@@ -588,38 +588,38 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
children: (
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card size={isMobile ? 'small' : 'default'}>
{memories && memories.length > 0 ? (
<List
dataSource={memories}
renderItem={(memory) => (
<List.Item>
<List.Item.Meta
title={
<div>
<Tag color="blue">{memory.type}</Tag>
<Tag color="orange">: {memory.importance.toFixed(1)}</Tag>
{memory.is_foreshadow === 1 && <Tag color="green"></Tag>}
{memory.is_foreshadow === 2 && <Tag color="purple"></Tag>}
<span style={{ marginLeft: 8 }}>{memory.title}</span>
</div>
}
description={
<div>
<p>{memory.content}</p>
{memories && memories.length > 0 ? (
<List
dataSource={memories}
renderItem={(memory) => (
<List.Item>
<List.Item.Meta
title={
<div>
{memory.tags.map((tag, idx) => (
<Tag key={idx} style={{ margin: 2 }}>{tag}</Tag>
))}
<Tag color="blue">{memory.type}</Tag>
<Tag color="orange">: {memory.importance.toFixed(1)}</Tag>
{memory.is_foreshadow === 1 && <Tag color="green"></Tag>}
{memory.is_foreshadow === 2 && <Tag color="purple"></Tag>}
<span style={{ marginLeft: 8 }}>{memory.title}</span>
</div>
</div>
}
/>
</List.Item>
)}
/>
) : (
<Empty description="暂无记忆片段" />
)}
}
description={
<div>
<p>{memory.content}</p>
<div>
{memory.tags.map((tag, idx) => (
<Tag key={idx} style={{ margin: 2 }}>{tag}</Tag>
))}
</div>
</div>
}
/>
</List.Item>
)}
/>
) : (
<Empty description="暂无记忆片段" />
)}
</Card>
</div>
)
@@ -700,7 +700,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
<p style={{ marginTop: 16 }}>...</p>
</div>
)}
{error && (
<Alert
message="错误"
@@ -709,10 +709,10 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
showIcon
/>
)}
{task && task.status !== 'completed' && renderProgress()}
{task && task.status === 'completed' && analysis && renderAnalysisResult()}
{/* 重新生成Modal */}
{chapterInfo && (
<ChapterRegenerationModal
@@ -734,7 +734,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
hasAnalysis={true}
/>
)}
{/* 内容对比组件 */}
{chapterInfo && comparisonModalVisible && (
<ChapterContentComparison
@@ -749,7 +749,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
// 应用新内容后刷新章节信息和分析
setChapterInfo(null);
setAnalysis(null);
// 重新加载章节内容
try {
const chapterResponse = await fetch(`/api/chapters/${chapterId}`);
@@ -764,7 +764,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
} catch (error) {
console.error('重新加载章节失败:', error);
}
// 刷新分析状态
await fetchAnalysisStatus();
}}
@@ -51,10 +51,10 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
}
message.success('新内容已应用!');
// 先调用 onApply 通知父组件刷新
onApply();
// 延迟触发章节分析,给父组件时间刷新
setTimeout(async () => {
try {
@@ -75,7 +75,7 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
message.warning('章节分析触发失败,您可以手动触发分析');
}
}, 500);
onClose();
} catch (error: any) {
message.error(error.message || '应用失败');
@@ -108,9 +108,9 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
centered
style={{ maxWidth: 1600 }}
footer={[
<Button
key="discard"
danger
<Button
key="discard"
danger
icon={<CloseOutlined />}
onClick={handleDiscard}
>
@@ -123,9 +123,9 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
>
</Button>,
<Button
key="apply"
type="primary"
<Button
key="apply"
type="primary"
icon={<CheckOutlined />}
loading={applying}
onClick={handleApply}
@@ -156,7 +156,7 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
title="字数变化"
value={wordCountDiff}
suffix="字"
valueStyle={{ color: wordCountDiff > 0 ? '#3f8600' : '#cf1322' }}
valueStyle={{ color: wordCountDiff > 0 ? 'var(--color-success)' : 'var(--color-error)' }}
prefix={wordCountDiff > 0 ? '+' : ''}
/>
</Col>
@@ -165,7 +165,7 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
title="变化比例"
value={wordCountDiffPercent}
suffix="%"
valueStyle={{ color: Math.abs(parseFloat(wordCountDiffPercent)) < 10 ? '#1890ff' : '#faad14' }}
valueStyle={{ color: Math.abs(parseFloat(wordCountDiffPercent)) < 10 ? 'var(--color-primary)' : 'var(--color-warning)' }}
prefix={wordCountDiff > 0 ? '+' : ''}
/>
</Col>
@@ -173,10 +173,10 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
</Card>
{/* 内容对比 */}
<div style={{
maxHeight: 'calc(90vh - 300px)',
<div style={{
maxHeight: 'calc(90vh - 300px)',
overflow: 'auto',
border: '1px solid #d9d9d9',
border: '1px solid var(--color-border)',
borderRadius: 4
}}>
<ReactDiffViewer
@@ -190,19 +190,19 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
styles={{
variables: {
light: {
diffViewerBackground: '#fff',
addedBackground: '#e6ffed',
addedColor: '#24292e',
removedBackground: '#ffeef0',
removedColor: '#24292e',
wordAddedBackground: '#acf2bd',
wordRemovedBackground: '#fdb8c0',
addedGutterBackground: '#cdffd8',
removedGutterBackground: '#ffdce0',
gutterBackground: '#f6f8fa',
gutterBackgroundDark: '#f3f4f6',
highlightBackground: '#fffbdd',
highlightGutterBackground: '#fff5b1',
diffViewerBackground: '#fff', // Keep white for diff viewer readability
addedBackground: 'var(--color-success-bg)',
addedColor: 'var(--color-text-primary)',
removedBackground: 'var(--color-error-bg)',
removedColor: 'var(--color-text-primary)',
wordAddedBackground: 'var(--color-success-border)',
wordRemovedBackground: 'var(--color-error-border)',
addedGutterBackground: 'var(--color-success-bg)',
removedGutterBackground: 'var(--color-error-bg)',
gutterBackground: 'var(--color-bg-layout)',
gutterBackgroundDark: 'var(--color-bg-container)',
highlightBackground: 'var(--color-warning-bg)',
highlightGutterBackground: 'var(--color-warning-border)',
},
},
line: {
+10 -10
View File
@@ -41,14 +41,14 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
textAlign: 'center',
marginBottom: 24
}}>
<Spin
indicator={<LoadingOutlined style={{ fontSize: 48, color: '#1890ff' }} spin />}
<Spin
indicator={<LoadingOutlined style={{ fontSize: 48, color: 'var(--color-primary)' }} spin />}
/>
<div style={{
fontSize: 20,
fontWeight: 'bold',
marginTop: 16,
color: '#262626'
color: 'var(--color-text-primary)'
}}>
AI生成中...
</div>
@@ -60,29 +60,29 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
}}>
<div style={{
height: 12,
background: '#f0f0f0',
background: 'var(--color-bg-layout)',
borderRadius: 6,
overflow: 'hidden',
marginBottom: 12
}}>
<div style={{
height: '100%',
background: progress === 100
? 'linear-gradient(90deg, #52c41a 0%, #73d13d 100%)'
: 'linear-gradient(90deg, #1890ff 0%, #40a9ff 100%)',
background: progress === 100
? 'linear-gradient(90deg, var(--color-success) 0%, var(--color-success-active) 100%)'
: 'linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-active) 100%)',
width: `${progress}%`,
transition: 'all 0.3s ease',
borderRadius: 6,
boxShadow: progress > 0 ? '0 0 10px rgba(24, 144, 255, 0.3)' : 'none'
boxShadow: progress > 0 ? 'var(--shadow-card)' : 'none'
}} />
</div>
{/* 进度百分比 */}
<div style={{
textAlign: 'center',
fontSize: 32,
fontWeight: 'bold',
color: progress === 100 ? '#52c41a' : '#1890ff',
color: progress === 100 ? 'var(--color-success)' : 'var(--color-primary)',
marginBottom: 8
}}>
{progress}%
+12 -12
View File
@@ -52,14 +52,14 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
textAlign: 'center',
marginBottom: 24
}}>
<Spin
indicator={<LoadingOutlined style={{ fontSize: 48, color: '#1890ff' }} spin />}
<Spin
indicator={<LoadingOutlined style={{ fontSize: 48, color: 'var(--color-primary)' }} spin />}
/>
<div style={{
fontSize: 20,
fontWeight: 'bold',
marginTop: 16,
color: '#262626'
color: 'var(--color-text-primary)'
}}>
{title}
</div>
@@ -72,30 +72,30 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
}}>
<div style={{
height: 12,
background: '#f0f0f0',
background: 'var(--color-bg-layout)',
borderRadius: 6,
overflow: 'hidden',
marginBottom: showPercentage ? 12 : 0
}}>
<div style={{
height: '100%',
background: progress === 100
? 'linear-gradient(90deg, #52c41a 0%, #73d13d 100%)'
: 'linear-gradient(90deg, #1890ff 0%, #40a9ff 100%)',
background: progress === 100
? 'linear-gradient(90deg, var(--color-success) 0%, var(--color-success-active) 100%)'
: 'linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-active) 100%)',
width: `${progress}%`,
transition: 'all 0.3s ease',
borderRadius: 6,
boxShadow: progress > 0 ? '0 0 10px rgba(24, 144, 255, 0.3)' : 'none'
boxShadow: progress > 0 ? 'var(--shadow-card)' : 'none'
}} />
</div>
{/* 进度百分比 */}
{showPercentage && (
<div style={{
textAlign: 'center',
fontSize: 32,
fontWeight: 'bold',
color: progress === 100 ? '#52c41a' : '#1890ff',
color: progress === 100 ? 'var(--color-success)' : 'var(--color-primary)',
marginBottom: 8
}}>
{progress}%
@@ -107,7 +107,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
<div style={{
textAlign: 'center',
fontSize: 16,
color: '#595959',
color: 'var(--color-text-secondary)',
minHeight: 24,
padding: '0 20px',
marginBottom: 16
@@ -119,7 +119,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
<div style={{
textAlign: 'center',
fontSize: 13,
color: '#8c8c8c',
color: 'var(--color-text-tertiary)',
marginBottom: onCancel ? 16 : 0
}}>
+11 -11
View File
@@ -122,23 +122,23 @@ export default function UserMenu() {
alignItems: 'center',
gap: 12,
padding: '8px 16px',
background: 'rgba(255, 255, 255, 0.95)',
background: 'rgba(255, 255, 255, 0.6)', // 保持半透明以配合 Backdrop
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
borderRadius: 24,
border: '1px solid rgba(102, 126, 234, 0.2)',
border: '1px solid var(--color-border)',
transition: 'all 0.3s ease',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
boxShadow: 'var(--shadow-card)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 1)';
e.currentTarget.style.background = 'var(--color-bg-container)'; // 悬浮时变实
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)';
e.currentTarget.style.boxShadow = 'var(--shadow-elevated)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.95)';
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.6)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)';
e.currentTarget.style.boxShadow = 'var(--shadow-card)';
}}
>
<div style={{ position: 'relative' }}>
@@ -147,9 +147,9 @@ export default function UserMenu() {
icon={<UserOutlined />}
size={40}
style={{
backgroundColor: '#1890ff',
backgroundColor: 'var(--color-primary)',
border: '3px solid #fff',
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.3)',
boxShadow: 'var(--shadow-card)',
}}
/>
{currentUser.is_admin && (
@@ -173,14 +173,14 @@ export default function UserMenu() {
</div>
<Space direction="vertical" size={0} style={{ display: window.innerWidth <= 768 ? 'none' : 'flex' }}>
<Text strong style={{
color: '#262626',
color: 'var(--color-text-primary)',
fontSize: 14,
lineHeight: '20px',
}}>
{currentUser.display_name || currentUser.username}
</Text>
<Text style={{
color: '#8c8c8c',
color: 'var(--color-text-secondary)',
fontSize: 12,
lineHeight: '18px',
}}>
+279 -10
View File
@@ -1,17 +1,69 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
/* --- 中国风配色方案 (Chinese Style Palette) --- */
/* 主色调:天青 (Cerulean / Azure) - 类似汝窑 */
--color-primary: #4D8088;
--color-primary-hover: #5F9EA8;
--color-primary-active: #3A666C;
/* 辅助色 */
--color-success: #52C41A;
--color-warning: #FAAD14;
--color-error: #FF4D4F;
--color-info: #1890FF;
/* 功能色背景/边框 */
--color-success-bg: #F6FFED;
--color-success-border: #B7EB8F;
--color-warning-bg: #FFFBE6;
--color-warning-border: #FFE58F;
--color-error-bg: #FFF2F0;
--color-error-border: #FFCCC7;
--color-info-bg: #E6F7FF;
--color-info-border: #91D5FF;
/* 背景色 */
--color-bg-base: #F8F6F1;
/* 米汤色 (Rice Soup / Cream) - 用于页面背景 */
--color-bg-container: #FFFFFF;
/* 纯白 - 用于卡片/容器 */
--color-bg-layout: #F0F2F5;
--color-bg-spotlight: #3A666C;
--color-bg-mask: rgba(0, 0, 0, 0.45);
/* 文本色 */
--color-text-base: #2B2B2B;
/* 墨色 (Ink) */
--color-text-primary: #2B2B2B;
--color-text-secondary: #595959;
/* 此时 (Secondary Text) */
--color-text-tertiary: #8C8C8C;
--color-text-quaternary: #BFBFBF;
/* 边框色 */
--color-border: #D9D9D9;
--color-border-secondary: #F0F0F0;
/* 阴影 */
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.06);
--shadow-elevated: 0 8px 24px rgba(77, 128, 136, 0.15);
/* 带一点主色调的阴影 */
--shadow-primary: 0 4px 16px rgba(77, 128, 136, 0.25);
--shadow-header: 0 2px 8px rgba(0, 0, 0, 0.05);
font-family: "PingFang SC", "Microsoft YaHei", "Heiti SC", Inter, system-ui, sans-serif;
line-height: 1.5715;
font-weight: 400;
color-scheme: light dark;
color: rgba(0, 0, 0, 0.87);
background-color: #f0f2f5;
color-scheme: light;
color: var(--color-text-base);
background-color: var(--color-bg-base);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* 移动端视口适配 */
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
@@ -64,15 +116,17 @@ body {
:root {
font-size: 14px;
}
/* 移动端隐藏滚动条 */
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
/* 移动端优化触摸区域 */
button, a, [role="button"] {
button,
a,
[role="button"] {
min-height: 44px;
min-width: 44px;
}
@@ -95,7 +149,9 @@ body {
/* 移动端禁止长按选择 */
@media (max-width: 768px) {
img, button {
img,
button {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@@ -131,3 +187,216 @@ body {
.ant-tabs-dropdown {
z-index: 2000 !important;
}
/* ===== 现代化侧边栏样式 (Modern Sidebar Styles) ===== */
/* 侧边栏容器 - 毛玻璃效果 */
.modern-sider {
background: linear-gradient(180deg,
rgba(255, 255, 255, 0.95) 0%,
rgba(248, 246, 241, 0.98) 100%) !important;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-right: 1px solid rgba(77, 128, 136, 0.12);
box-shadow:
4px 0 24px rgba(77, 128, 136, 0.08),
1px 0 0 rgba(255, 255, 255, 0.8) inset;
}
/* 侧边栏菜单整体样式 */
.modern-sider .ant-menu {
background: transparent !important;
border-right: none !important;
}
/* 菜单项基础样式 */
.modern-sider .ant-menu-item {
margin: 6px 12px !important;
padding: 0 16px !important;
border-radius: 12px !important;
height: 48px !important;
line-height: 48px !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
position: relative;
overflow: hidden;
}
/* 折叠状态下的菜单项 */
.modern-sider.ant-layout-sider-collapsed .ant-menu-item {
margin: 6px 8px !important;
padding: 0 !important;
display: flex;
align-items: center;
justify-content: center;
}
/* 菜单项悬停效果 - 仅未选中项 */
.modern-sider .ant-menu-item:not(.ant-menu-item-selected):hover {
background: linear-gradient(135deg,
rgba(77, 128, 136, 0.15) 0%,
rgba(95, 158, 168, 0.22) 100%) !important;
transform: translateX(6px);
box-shadow:
0 2px 8px rgba(77, 128, 136, 0.15),
inset 0 0 0 1px rgba(77, 128, 136, 0.2);
}
/* 折叠状态悬停效果 - 仅未选中项 */
.modern-sider.ant-layout-sider-collapsed .ant-menu-item:not(.ant-menu-item-selected):hover {
transform: translateX(0) scale(1.08);
box-shadow:
0 4px 12px rgba(77, 128, 136, 0.2),
inset 0 0 0 1px rgba(77, 128, 136, 0.25);
}
/* 菜单项选中状态 - 增强版 */
.modern-sider .ant-menu-item-selected {
background: linear-gradient(135deg,
var(--color-primary) 0%,
#5A9BA5 50%,
var(--color-primary-hover) 100%) !important;
box-shadow:
0 6px 20px rgba(77, 128, 136, 0.45),
0 3px 8px rgba(77, 128, 136, 0.25),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.15);
}
/* 选中项悬停 - 微发光效果 */
.modern-sider .ant-menu-item-selected:hover {
transform: translateX(0) !important;
box-shadow:
0 8px 24px rgba(77, 128, 136, 0.5),
0 4px 10px rgba(77, 128, 136, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.25);
filter: brightness(1.05);
}
.modern-sider.ant-layout-sider-collapsed .ant-menu-item-selected:hover {
transform: scale(1.05) !important;
}
/* 选中状态文字和图标颜色 */
.modern-sider .ant-menu-item-selected,
.modern-sider .ant-menu-item-selected a,
.modern-sider .ant-menu-item-selected .anticon {
color: #fff !important;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
/* 未选中状态颜色 */
.modern-sider .ant-menu-item:not(.ant-menu-item-selected),
.modern-sider .ant-menu-item:not(.ant-menu-item-selected) a {
color: var(--color-text-secondary) !important;
}
.modern-sider .ant-menu-item:not(.ant-menu-item-selected) .anticon {
color: var(--color-primary) !important;
}
/* 菜单项悬停时文字和图标颜色 */
.modern-sider .ant-menu-item:not(.ant-menu-item-selected):hover,
.modern-sider .ant-menu-item:not(.ant-menu-item-selected):hover a {
color: var(--color-primary-active) !important;
font-weight: 600;
}
.modern-sider .ant-menu-item:not(.ant-menu-item-selected):hover .anticon {
color: var(--color-primary-active) !important;
}
/* 图标样式优化 */
.modern-sider .ant-menu-item .anticon {
font-size: 18px !important;
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) !important;
}
.modern-sider.ant-layout-sider-collapsed .ant-menu-item .anticon {
font-size: 22px !important;
}
/* 悬停时图标微动效 */
.modern-sider .ant-menu-item:not(.ant-menu-item-selected):hover .anticon {
transform: scale(1.15);
}
/* 选中项左侧指示条 */
.modern-sider .ant-menu-item-selected::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 24px;
background: rgba(255, 255, 255, 0.9);
border-radius: 0 4px 4px 0;
opacity: 0;
}
/* 折叠状态隐藏指示条 */
.modern-sider.ant-layout-sider-collapsed .ant-menu-item-selected::before {
display: none;
}
/* 链接文字样式 */
.modern-sider .ant-menu-item a {
font-weight: 500;
letter-spacing: 0.5px;
transition: all 0.3s ease;
}
/* 侧边栏顶部装饰线 */
.modern-sider::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg,
var(--color-primary) 0%,
var(--color-primary-hover) 50%,
var(--color-primary) 100%);
opacity: 0.8;
}
/* 侧边栏底部渐变遮罩 */
.modern-sider::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background: linear-gradient(to top,
rgba(248, 246, 241, 0.95) 0%,
transparent 100%);
pointer-events: none;
}
/* 菜单项波纹效果 */
.modern-sider .ant-menu-item::after {
display: none !important;
}
/* 菜单标题样式 (如果有分组) */
.modern-sider .ant-menu-item-group-title {
color: var(--color-text-tertiary) !important;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
padding: 16px 24px 8px !important;
}
/* Tooltip 样式优化 (折叠状态) */
.ant-tooltip .ant-tooltip-inner {
background: linear-gradient(135deg,
var(--color-primary) 0%,
var(--color-primary-hover) 100%);
border-radius: 8px;
padding: 8px 16px;
font-weight: 500;
box-shadow: 0 4px 12px rgba(77, 128, 136, 0.3);
}
+28 -1
View File
@@ -8,7 +8,34 @@ import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ConfigProvider locale={zhCN}>
<ConfigProvider
locale={zhCN}
theme={{
token: {
colorPrimary: '#4D8088', // 天青
colorBgBase: '#F8F6F1', // 米汤色
colorTextBase: '#2B2B2B', // 墨色
borderRadius: 6,
wireframe: false,
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif",
},
components: {
Layout: {
bodyBg: '#F8F6F1',
headerBg: '#FFFFFF',
siderBg: '#FFFFFF',
},
Card: {
colorBgContainer: '#FFFFFF',
boxShadowTertiary: '0 4px 12px rgba(0, 0, 0, 0.05)', // 更柔和的阴影
},
Button: {
borderRadius: 6,
controlHeight: 36,
}
}
}}
>
<App />
</ConfigProvider>
</StrictMode>,
+28 -26
View File
@@ -15,6 +15,7 @@ const { TextArea } = Input;
export default function Chapters() {
const { currentProject, chapters, setCurrentChapter, setCurrentProject } = useStore();
const [modal, contextHolder] = Modal.useModal();
const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditorOpen, setIsEditorOpen] = useState(false);
const [isContinuing, setIsContinuing] = useState(false);
@@ -433,7 +434,7 @@ export default function Chapters() {
const selectedStyle = writingStyles.find(s => s.id === selectedStyleId);
const modal = Modal.confirm({
const instance = modal.confirm({
title: 'AI创作章节内容',
width: 700,
centered: true,
@@ -455,11 +456,11 @@ export default function Chapters() {
<div style={{
marginTop: 16,
padding: 12,
background: '#f0f5ff',
background: 'var(--color-info-bg)',
borderRadius: 4,
border: '1px solid #adc6ff'
border: '1px solid var(--color-info-border)'
}}>
<div style={{ marginBottom: 8, fontWeight: 500, color: '#1890ff' }}>
<div style={{ marginBottom: 8, fontWeight: 500, color: 'var(--color-primary)' }}>
📚 {previousChapters.length}
</div>
<div style={{ maxHeight: 150, overflowY: 'auto' }}>
@@ -484,7 +485,7 @@ export default function Chapters() {
okButtonProps: { danger: true },
cancelText: '取消',
onOk: async () => {
modal.update({
instance.update({
okButtonProps: { danger: true, loading: true },
cancelButtonProps: { disabled: true },
closable: false,
@@ -495,7 +496,7 @@ export default function Chapters() {
try {
if (!selectedStyleId) {
message.error('请先选择写作风格');
modal.update({
instance.update({
okButtonProps: { danger: true, loading: false },
cancelButtonProps: { disabled: false },
closable: true,
@@ -505,9 +506,9 @@ export default function Chapters() {
return;
}
await handleGenerate();
modal.destroy();
instance.destroy();
} catch (error) {
modal.update({
instance.update({
okButtonProps: { danger: true, loading: false },
cancelButtonProps: { disabled: false },
closable: true,
@@ -579,7 +580,7 @@ export default function Chapters() {
return;
}
Modal.confirm({
modal.confirm({
title: '导出项目章节',
content: `确定要将《${currentProject.title}》的所有章节导出为TXT文件吗?`,
centered: true,
@@ -840,7 +841,7 @@ export default function Chapters() {
? Math.max(...chapters.map(c => c.chapter_number)) + 1
: 1;
Modal.confirm({
modal.confirm({
title: '手动创建章节',
width: 600,
centered: true,
@@ -937,7 +938,7 @@ export default function Chapters() {
if (conflictChapter) {
// 显示冲突提示Modal
Modal.confirm({
modal.confirm({
title: '章节序号冲突',
icon: <InfoCircleOutlined style={{ color: '#ff4d4f' }} />,
width: 500,
@@ -1074,10 +1075,10 @@ export default function Chapters() {
try {
const planData: ExpansionPlanData = JSON.parse(chapter.expansion_plan);
Modal.info({
modal.info({
title: (
<Space style={{ flexWrap: 'wrap' }}>
<InfoCircleOutlined style={{ color: '#1890ff' }} />
<InfoCircleOutlined style={{ color: 'var(--color-primary)' }} />
<span style={{ wordBreak: 'break-word' }}>{chapter.chapter_number}</span>
</Space>
),
@@ -1366,11 +1367,12 @@ export default function Chapters() {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{contextHolder}
<div style={{
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: '#fff',
backgroundColor: 'var(--color-bg-container)',
padding: isMobile ? '12px 0' : '16px 0',
marginBottom: isMobile ? 12 : 16,
borderBottom: '1px solid #f0f0f0',
@@ -1486,7 +1488,7 @@ export default function Chapters() {
>
<div style={{ width: '100%' }}>
<List.Item.Meta
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: '#1890ff' }} />}
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: 'var(--color-primary)' }} />}
title={
<div style={{
display: 'flex',
@@ -1500,7 +1502,7 @@ export default function Chapters() {
</span>
<Space wrap size={isMobile ? 4 : 8}>
<Tag color={getStatusColor(item.status)}>{getStatusText(item.status)}</Tag>
<Badge count={`${item.word_count || 0}`} style={{ backgroundColor: '#52c41a' }} />
<Badge count={`${item.word_count || 0}`} style={{ backgroundColor: 'var(--color-success)' }} />
{renderAnalysisStatus(item.id)}
{!canGenerateChapter(item) && (
<Tooltip title={getGenerateDisabledReason(item)}>
@@ -1591,11 +1593,11 @@ export default function Chapters() {
</span>
<Badge
count={`${group.chapters.length}`}
style={{ backgroundColor: '#52c41a' }}
style={{ backgroundColor: 'var(--color-success)' }}
/>
<Badge
count={`${group.chapters.reduce((sum, ch) => sum + (ch.word_count || 0), 0)}`}
style={{ backgroundColor: '#1890ff' }}
style={{ backgroundColor: 'var(--color-primary)' }}
/>
</div>
}
@@ -1681,7 +1683,7 @@ export default function Chapters() {
>
<div style={{ width: '100%' }}>
<List.Item.Meta
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: '#1890ff' }} />}
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: 'var(--color-primary)' }} />}
title={
<div style={{
display: 'flex',
@@ -1695,7 +1697,7 @@ export default function Chapters() {
</span>
<Space wrap size={isMobile ? 4 : 8}>
<Tag color={getStatusColor(item.status)}>{getStatusText(item.status)}</Tag>
<Badge count={`${item.word_count || 0}`} style={{ backgroundColor: '#52c41a' }} />
<Badge count={`${item.word_count || 0}`} style={{ backgroundColor: 'var(--color-success)' }} />
{renderAnalysisStatus(item.id)}
{!canGenerateChapter(item) && (
<Tooltip title={getGenerateDisabledReason(item)}>
@@ -1708,7 +1710,7 @@ export default function Chapters() {
{item.expansion_plan && (
<Tooltip title="查看展开详情">
<InfoCircleOutlined
style={{ color: '#1890ff', cursor: 'pointer', fontSize: 16 }}
style={{ color: 'var(--color-primary)', cursor: 'pointer', fontSize: 16 }}
onClick={(e) => {
e.stopPropagation();
showExpansionPlanModal(item);
@@ -1718,7 +1720,7 @@ export default function Chapters() {
)}
<Tooltip title={item.expansion_plan ? "编辑规划信息" : "创建规划信息"}>
<FormOutlined
style={{ color: '#52c41a', cursor: 'pointer', fontSize: 16 }}
style={{ color: 'var(--color-success)', cursor: 'pointer', fontSize: 16 }}
onClick={(e) => {
e.stopPropagation();
handleOpenPlanEditor(item);
@@ -1992,7 +1994,7 @@ export default function Chapters() {
<Select.Option value="omniscient"></Select.Option>
</Select>
{temporaryNarrativePerspective && (
<div style={{ color: '#52c41a', fontSize: 12, marginTop: 4 }}>
<div style={{ color: 'var(--color-success)', fontSize: 12, marginTop: 4 }}>
{getNarrativePerspectiveText(temporaryNarrativePerspective)}
</div>
)}
@@ -2169,7 +2171,7 @@ export default function Chapters() {
open={batchGenerateVisible}
onCancel={() => {
if (batchGenerating) {
Modal.confirm({
modal.confirm({
title: '确认取消',
content: '批量生成正在进行中,确定要取消吗?',
okText: '确定取消',
@@ -2374,7 +2376,7 @@ export default function Chapters() {
danger
icon={<StopOutlined />}
onClick={() => {
Modal.confirm({
modal.confirm({
title: '确认取消',
content: '确定要取消批量生成吗?已生成的章节将保留。',
okText: '确定取消',
@@ -2409,7 +2411,7 @@ export default function Chapters() {
}
title="批量生成章节"
onCancel={() => {
Modal.confirm({
modal.confirm({
title: '确认取消',
content: '确定要取消批量生成吗?已生成的章节将保留。',
okText: '确定取消',
+54 -52
View File
@@ -40,6 +40,7 @@ export default function Characters() {
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentProject?.id]);
const [modal, contextHolder] = Modal.useModal();
if (!currentProject) return null;
@@ -200,7 +201,7 @@ export default function Characters() {
const handleUpdateCharacter = async (values: CharacterUpdate) => {
if (!editingCharacter) return;
try {
await characterApi.updateCharacter(editingCharacter.id, values);
message.success('更新成功');
@@ -218,7 +219,7 @@ export default function Characters() {
};
const showGenerateModal = () => {
Modal.confirm({
modal.confirm({
title: 'AI生成角色',
width: 600,
centered: true,
@@ -256,7 +257,7 @@ export default function Characters() {
};
const showGenerateOrgModal = () => {
Modal.confirm({
modal.confirm({
title: 'AI生成组织',
width: 600,
centered: true,
@@ -306,14 +307,15 @@ export default function Characters() {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{contextHolder}
<div style={{
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: '#fff',
backgroundColor: 'var(--color-bg-container)',
padding: isMobile ? '12px 0' : '16px 0',
marginBottom: isMobile ? 12 : 16,
borderBottom: '1px solid #f0f0f0',
borderBottom: '1px solid var(--color-border-secondary)',
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
gap: isMobile ? 12 : 0,
@@ -370,45 +372,45 @@ export default function Characters() {
position: 'sticky',
top: isMobile ? 60 : 72,
zIndex: 9,
backgroundColor: '#fff',
backgroundColor: 'var(--color-bg-container)',
paddingBottom: 8,
borderBottom: '1px solid #f0f0f0',
borderBottom: '1px solid var(--color-border-secondary)',
}}>
<Tabs
activeKey={activeTab}
onChange={(key) => setActiveTab(key as 'all' | 'character' | 'organization')}
items={[
{
key: 'all',
label: `全部 (${characters.length})`,
},
{
key: 'character',
label: (
<span>
<UserOutlined /> ({characterList.length})
</span>
),
},
{
key: 'organization',
label: (
<span>
<TeamOutlined /> ({organizationList.length})
</span>
),
},
]}
/>
</div>
)}
<div style={{ flex: 1, overflowY: 'auto' }}>
{characters.length === 0 ? (
<Empty description="还没有角色或组织,开始创建吧!" />
) : (
<>
<Row gutter={isMobile ? [8, 8] : characterGridConfig.gutter}>
{
key: 'all',
label: `全部 (${characters.length})`,
},
{
key: 'character',
label: (
<span>
<UserOutlined /> ({characterList.length})
</span>
),
},
{
key: 'organization',
label: (
<span>
<TeamOutlined /> ({organizationList.length})
</span>
),
},
]}
/>
</div>
)}
<div style={{ flex: 1, overflowY: 'auto' }}>
{characters.length === 0 ? (
<Empty description="还没有角色或组织,开始创建吧!" />
) : (
<>
<Row gutter={isMobile ? [8, 8] : characterGridConfig.gutter}>
{activeTab === 'all' && (
<>
{characterList.length > 0 && (
@@ -440,7 +442,7 @@ export default function Characters() {
))}
</>
)}
{organizationList.length > 0 && (
<>
<Col span={24}>
@@ -472,7 +474,7 @@ export default function Characters() {
)}
</>
)}
{activeTab === 'character' && characterList.map((character) => (
<Col
xs={24}
@@ -490,7 +492,7 @@ export default function Characters() {
/>
</Col>
))}
{activeTab === 'organization' && organizationList.map((org) => (
<Col
xs={24}
@@ -511,14 +513,14 @@ export default function Characters() {
</Row>
{displayList.length === 0 && (
<Empty
<Empty
description={
activeTab === 'character'
? '暂无角色'
: activeTab === 'organization'
? '暂无组织'
: '暂无数据'
}
activeTab === 'character'
? '暂无角色'
: activeTab === 'organization'
? '暂无组织'
: '暂无数据'
}
/>
)}
</>
@@ -550,7 +552,7 @@ export default function Characters() {
<Input placeholder={`输入${editingCharacter?.is_organization ? '组织' : '角色'}名称`} />
</Form.Item>
</Col>
{!editingCharacter?.is_organization && (
<Col span={12}>
<Form.Item label="角色定位" name="role_type">
@@ -615,7 +617,7 @@ export default function Characters() {
</Form.Item>
</Col>
</Row>
<Form.Item
label="组织目的"
name="organization_purpose"
@@ -689,7 +691,7 @@ export default function Characters() {
<Input placeholder={`输入${createType === 'character' ? '角色' : '组织'}名称`} />
</Form.Item>
</Col>
{createType === 'character' && (
<Col span={12}>
<Form.Item label="角色定位" name="role_type" initialValue="supporting">
@@ -761,7 +763,7 @@ export default function Characters() {
</Form.Item>
</Col>
</Row>
<Form.Item
label="组织目的"
name="organization_purpose"
+100 -80
View File
@@ -45,6 +45,16 @@ const CACHE_EXPIRY = 24 * 60 * 60 * 1000;
const Inspiration: React.FC = () => {
const navigate = useNavigate();
const [currentStep, setCurrentStep] = useState<Step>('idea');
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const [messages, setMessages] = useState<Message[]>([
{
type: 'ai',
@@ -54,15 +64,15 @@ const Inspiration: React.FC = () => {
const [inputValue, setInputValue] = useState('');
const [loading, setLoading] = useState(false);
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
// 收集的数据
const [wizardData, setWizardData] = useState<Partial<WizardData>>({});
// 保存用户的原始想法,用于保持上下文一致性
const [initialIdea, setInitialIdea] = useState<string>('');
// 生成配置
const [generationConfig, setGenerationConfig] = useState<GenerationConfig | null>(null);
// 滚动容器引用
const messagesEndRef = useRef<HTMLDivElement>(null);
const chatContainerRef = useRef<HTMLDivElement>(null);
@@ -77,7 +87,7 @@ const Inspiration: React.FC = () => {
const [cacheLoaded, setCacheLoaded] = useState(false);
// ==================== 缓存管理函数 ====================
// 保存到缓存
const saveToCache = () => {
try {
@@ -85,7 +95,7 @@ const Inspiration: React.FC = () => {
if (currentStep === 'generating' || currentStep === 'complete') {
return;
}
// 只有用户有输入时才保存(至少两条消息:AI问候+用户回复)
if (messages.length <= 1) {
return;
@@ -99,7 +109,7 @@ const Inspiration: React.FC = () => {
selectedOptions,
timestamp: Date.now()
};
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
console.log('💾 对话已自动保存');
} catch (error) {
@@ -158,7 +168,7 @@ const Inspiration: React.FC = () => {
};
// ==================== 组件挂载时恢复缓存 ====================
useEffect(() => {
if (!cacheLoaded) {
restoreFromCache();
@@ -167,7 +177,7 @@ const Inspiration: React.FC = () => {
}, []);
// ==================== 自动保存:状态变化时保存 ====================
useEffect(() => {
// 防抖保存
const timer = setTimeout(() => {
@@ -199,7 +209,7 @@ const Inspiration: React.FC = () => {
// 重试生成
const handleRetry = async () => {
if (!lastFailedRequest) return;
setLoading(true);
try {
const response = await inspirationApi.generateOptions({
@@ -215,8 +225,8 @@ const Inspiration: React.FC = () => {
setMessages(prev => {
const newMessages = [...prev];
if (newMessages[newMessages.length - 1].type === 'ai' &&
(newMessages[newMessages.length - 1].content.includes('生成失败') ||
newMessages[newMessages.length - 1].content.includes('出错了'))) {
(newMessages[newMessages.length - 1].content.includes('生成失败') ||
newMessages[newMessages.length - 1].content.includes('出错了'))) {
newMessages.pop();
}
return newMessages;
@@ -252,7 +262,7 @@ const Inspiration: React.FC = () => {
content: inputValue,
};
setMessages(prev => [...prev, userMessage]);
const userInput = inputValue;
setInputValue('');
setLoading(true);
@@ -260,7 +270,7 @@ const Inspiration: React.FC = () => {
try {
if (currentStep === 'idea') {
setInitialIdea(userInput);
const requestData = {
step: 'title' as const,
context: {
@@ -268,7 +278,7 @@ const Inspiration: React.FC = () => {
description: userInput
}
};
const response = await inspirationApi.generateOptions(requestData);
if (response.error || !response.options || response.options.length < 3) {
@@ -308,12 +318,12 @@ const Inspiration: React.FC = () => {
await handleRetry();
return;
}
if (option === '我自己输入书名' || option === '我自己输入') {
message.info('请在下方输入框中输入您的内容');
return;
}
// 对于多选类型,不立即禁用选项
if (currentStep === 'genre') {
const newSelected = selectedOptions.includes(option)
@@ -322,7 +332,7 @@ const Inspiration: React.FC = () => {
setSelectedOptions(newSelected);
return;
}
// 立即禁用当前消息的选项(单选场景)
setMessages(prev => {
const newMessages = [...prev];
@@ -335,17 +345,17 @@ const Inspiration: React.FC = () => {
}
return newMessages;
});
if (currentStep === 'perspective') {
const userMessage: Message = {
type: 'user',
content: option,
};
setMessages(prev => [...prev, userMessage]);
const updatedData = { ...wizardData, narrative_perspective: option };
setWizardData(updatedData);
// 询问大纲模式
const aiMessage: Message = {
type: 'ai',
@@ -362,25 +372,25 @@ const Inspiration: React.FC = () => {
setCurrentStep('outline_mode');
return;
}
if (currentStep === 'outline_mode') {
const userMessage: Message = {
type: 'user',
content: option,
};
setMessages(prev => [...prev, userMessage]);
// 将选项转换为实际的模式值
const modeValue: 'one-to-one' | 'one-to-many' =
option === '📋 一对一模式' ? 'one-to-one' : 'one-to-many';
const updatedData = {
...wizardData,
outline_mode: modeValue,
genre: wizardData.genre || []
} as WizardData;
setWizardData(updatedData);
// 显示摘要
const modeText = modeValue === 'one-to-one' ? '一对一模式' : '一对多模式';
const summary = `
@@ -405,7 +415,7 @@ const Inspiration: React.FC = () => {
setCurrentStep('confirm');
return;
}
if (currentStep === 'confirm') {
if (option === '✅ 确认创建') {
const userMessage: Message = {
@@ -413,16 +423,16 @@ const Inspiration: React.FC = () => {
content: '确认创建',
};
setMessages(prev => [...prev, userMessage]);
const aiMessage: Message = {
type: 'ai',
content: '好的!正在为你创建项目,这可能需要几分钟时间...'
};
setMessages(prev => [...prev, aiMessage]);
// 清除缓存(对话完成,进入生成阶段)
clearCache();
// 开始生成项目
const data = wizardData as WizardData;
const config: GenerationConfig = {
@@ -476,7 +486,7 @@ const Inspiration: React.FC = () => {
setLoading(true);
try {
const updatedData = { ...wizardData };
if (currentStep === 'title') {
updatedData.title = input;
} else if (currentStep === 'description') {
@@ -493,7 +503,7 @@ const Inspiration: React.FC = () => {
setLoading(false);
return;
}
setWizardData(updatedData);
await generateNextStep(updatedData);
} catch (error: any) {
@@ -532,7 +542,7 @@ const Inspiration: React.FC = () => {
const updatedData = { ...wizardData, genre: selectedOptions };
setWizardData(updatedData);
setSelectedOptions([]);
setLoading(true);
try {
const aiMessage: Message = {
@@ -657,7 +667,7 @@ const Inspiration: React.FC = () => {
const handleRestart = () => {
// 清除缓存
clearCache();
setCurrentStep('idea');
setMessages([
{
@@ -697,7 +707,7 @@ const Inspiration: React.FC = () => {
<Card
ref={chatContainerRef}
style={{
height: window.innerWidth <= 768 ? 'calc(100vh - 280px)' : 600,
height: isMobile ? 'calc(100vh - 280px)' : 600,
overflowY: 'auto',
marginBottom: 16,
boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
@@ -721,22 +731,22 @@ const Inspiration: React.FC = () => {
maxWidth: '80%',
padding: '12px 16px',
borderRadius: 12,
background: msg.type === 'ai' ? '#f0f0f0' : '#1890ff',
color: msg.type === 'ai' ? '#000' : '#fff',
background: msg.type === 'ai' ? 'var(--color-bg-container)' : 'var(--color-primary)',
color: msg.type === 'ai' ? 'var(--color-text-primary)' : '#fff',
boxShadow: msg.type === 'ai'
? '0 2px 8px rgba(0,0,0,0.08)'
: '0 2px 8px rgba(24,144,255,0.3)',
? 'var(--shadow-card)'
: 'var(--shadow-primary)',
}}>
<Paragraph
style={{
margin: 0,
color: msg.type === 'ai' ? '#000' : '#fff',
<Paragraph
style={{
margin: 0,
color: msg.type === 'ai' ? 'var(--color-text-primary)' : '#fff',
whiteSpace: 'pre-wrap'
}}
>
{msg.content}
</Paragraph>
{msg.options && msg.options.length > 0 && (
<Space
direction="vertical"
@@ -752,13 +762,13 @@ const Inspiration: React.FC = () => {
style={{
cursor: msg.optionsDisabled ? 'not-allowed' : 'pointer',
border: msg.isMultiSelect && selectedOptions.includes(option)
? '2px solid #1890ff'
: '1px solid #d9d9d9',
? '2px solid var(--color-primary)'
: '1px solid var(--color-border)',
background: msg.optionsDisabled
? '#f5f5f5'
? 'var(--color-bg-layout)'
: msg.isMultiSelect && selectedOptions.includes(option)
? '#e6f7ff'
: '#fff',
? 'var(--color-bg-spotlight)' // Need to ensure this exists or use safe fallback
: 'var(--color-bg-container)',
opacity: msg.optionsDisabled ? 0.6 : 1,
animation: 'floatIn 0.6s ease-out',
animationDelay: `${optIndex * 0.1}s`,
@@ -768,7 +778,7 @@ const Inspiration: React.FC = () => {
onMouseEnter={(e) => {
if (!msg.optionsDisabled) {
e.currentTarget.style.transform = 'translateY(-2px) scale(1.02)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(24,144,255,0.2)';
e.currentTarget.style.boxShadow = 'var(--shadow-elevated)';
}
}}
onMouseLeave={(e) => {
@@ -781,7 +791,7 @@ const Inspiration: React.FC = () => {
{option}
</Card>
))}
{msg.isMultiSelect && (
<Button
type="primary"
@@ -797,7 +807,7 @@ const Inspiration: React.FC = () => {
</div>
</div>
))}
{loading && (
<div style={{
textAlign: 'center',
@@ -807,7 +817,7 @@ const Inspiration: React.FC = () => {
<Spin tip="AI思考中..." />
</div>
)}
<div ref={messagesEndRef} />
</Space>
</Card>
@@ -821,7 +831,7 @@ const Inspiration: React.FC = () => {
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder={
currentStep === 'idea'
currentStep === 'idea'
? '例如:我想写一本关于时间旅行的科幻小说...'
: '输入自定义内容,或点击上方选项卡片...'
}
@@ -854,8 +864,7 @@ const Inspiration: React.FC = () => {
return (
<div style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: window.innerWidth <= 768 ? '12px' : '24px'
background: 'var(--color-bg-base)',
}}>
<style>
{`
@@ -894,53 +903,65 @@ const Inspiration: React.FC = () => {
}
`}
</style>
<div style={{ maxWidth: 800, margin: '0 auto' }}>
{/* 顶部标题栏 - 固定不滚动 */}
<div style={{
position: 'sticky',
top: 0,
zIndex: 100,
background: 'var(--color-primary)',
boxShadow: 'var(--shadow-header)',
}}>
<div style={{
marginBottom: window.innerWidth <= 768 ? 12 : 24,
position: 'relative'
maxWidth: 1200,
margin: '0 auto',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: isMobile ? '12px 16px' : '16px 24px',
}}>
<Button
icon={<ArrowLeftOutlined />}
onClick={handleBack}
type="text"
size={window.innerWidth <= 768 ? 'small' : 'middle'}
size={isMobile ? 'middle' : 'large'}
style={{
background: 'rgba(255,255,255,0.2)',
borderColor: 'rgba(255,255,255,0.3)',
color: '#fff',
padding: window.innerWidth <= 768 ? '4px 8px' : '4px 15px',
height: window.innerWidth <= 768 ? 32 : 'auto',
position: window.innerWidth <= 768 ? 'absolute' : 'static',
left: 0,
top: 0,
zIndex: 1
}}
>
{window.innerWidth <= 768 ? '返回' : '返回项目列表'}
{isMobile ? '返回' : '返回项目列表'}
</Button>
<div style={{
textAlign: 'center',
paddingTop: window.innerWidth <= 768 ? 0 : 0
}}>
<div style={{ textAlign: 'center' }}>
<Title
level={window.innerWidth <= 768 ? 4 : 2}
level={isMobile ? 4 : 2}
style={{
color: '#fff',
margin: 0,
marginBottom: window.innerWidth <= 768 ? 4 : 8
color: '#fff',
textShadow: '0 2px 4px rgba(0,0,0,0.1)',
lineHeight: 1.2
}}
>
</Title>
<Text style={{
color: '#fff',
display: 'block',
fontSize: window.innerWidth <= 768 ? 12 : 14,
opacity: 0.9
color: 'rgba(255,255,255,0.85)',
fontSize: isMobile ? 12 : 14,
}}>
</Text>
</div>
</div>
<div style={{ width: isMobile ? 60 : 120 }}></div>
</div>
</div>
<div style={{
maxWidth: 800,
margin: '0 auto',
padding: isMobile ? '16px 12px' : '24px 24px',
}}>
{(currentStep === 'idea' || currentStep === 'title' || currentStep === 'description' ||
currentStep === 'theme' || currentStep === 'genre' || currentStep === 'perspective' ||
currentStep === 'outline_mode' || currentStep === 'confirm') && renderChat()}
@@ -950,7 +971,7 @@ const Inspiration: React.FC = () => {
storagePrefix="inspiration"
onComplete={handleComplete}
onBack={handleBackToChat}
isMobile={window.innerWidth <= 768}
isMobile={isMobile}
/>
)}
</div>
@@ -959,4 +980,3 @@ const Inspiration: React.FC = () => {
};
export default Inspiration;
+146 -147
View File
@@ -46,15 +46,15 @@ export default function Login() {
try {
setLoading(true);
const response = await authApi.localLogin(values.username, values.password);
if (response.success) {
message.success('登录成功!');
// 检查是否永久隐藏公告
const hideForever = localStorage.getItem('announcement_hide_forever');
const hideToday = localStorage.getItem('announcement_hide_today');
const today = new Date().toDateString();
// 如果永久隐藏或今日已隐藏,则不显示公告
if (hideForever === 'true' || hideToday === today) {
const redirect = searchParams.get('redirect') || '/';
@@ -73,13 +73,13 @@ export default function Login() {
try {
setLoading(true);
const response = await authApi.getLinuxDOAuthUrl();
// 保存重定向地址到 sessionStorage
const redirect = searchParams.get('redirect');
if (redirect) {
sessionStorage.setItem('login_redirect', redirect);
}
// 跳转到 LinuxDO 授权页面
window.location.href = response.auth_url;
} catch (error) {
@@ -96,9 +96,9 @@ export default function Login() {
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
background: 'var(--color-bg-base)',
}}>
<Spin size="large" style={{ color: '#fff' }} />
<Spin size="large" style={{ color: 'var(--color-primary)' }} />
</div>
);
}
@@ -141,10 +141,10 @@ export default function Login() {
height: 48,
fontSize: 16,
fontWeight: 600,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
background: 'var(--color-primary)',
border: 'none',
borderRadius: '12px',
boxShadow: '0 4px 16px rgba(102, 126, 234, 0.4)',
boxShadow: 'var(--shadow-primary)',
}}
>
@@ -178,19 +178,19 @@ export default function Login() {
height: 52,
fontSize: 16,
fontWeight: 600,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
background: 'var(--color-primary)',
border: 'none',
borderRadius: '12px',
boxShadow: '0 4px 16px rgba(102, 126, 234, 0.4)',
boxShadow: 'var(--shadow-primary)',
transition: 'all 0.3s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 6px 24px rgba(102, 126, 234, 0.5)';
e.currentTarget.style.boxShadow = 'var(--shadow-elevated)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 4px 16px rgba(102, 126, 234, 0.4)';
e.currentTarget.style.boxShadow = 'var(--shadow-primary)';
}}
>
使 LinuxDO
@@ -224,144 +224,143 @@ export default function Login() {
onNeverShow={handleNeverShow}
/>
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: '20px',
position: 'relative',
overflow: 'hidden',
}}>
{/* 装饰性背景元素 */}
<div style={{
position: 'absolute',
top: '-10%',
right: '-5%',
width: '400px',
height: '400px',
background: 'rgba(255, 255, 255, 0.1)',
borderRadius: '50%',
filter: 'blur(60px)',
}} />
<div style={{
position: 'absolute',
bottom: '-10%',
left: '-5%',
width: '350px',
height: '350px',
background: 'rgba(255, 255, 255, 0.08)',
borderRadius: '50%',
filter: 'blur(60px)',
}} />
<Card
style={{
width: '100%',
maxWidth: 420,
background: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.2)',
border: '1px solid rgba(255, 255, 255, 0.3)',
borderRadius: '16px',
position: 'relative',
zIndex: 1,
}}
bodyStyle={{
padding: '40px 32px',
}}
>
<Space direction="vertical" size="large" style={{ width: '100%', textAlign: 'center' }}>
{/* Logo区域 */}
<div style={{ marginBottom: '8px' }}>
<div style={{
width: '72px',
height: '72px',
margin: '0 auto 20px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 8px 24px rgba(102, 126, 234, 0.4)',
}}>
<img
src="/logo.svg"
alt="Logo"
style={{
width: '48px',
height: '48px',
filter: 'brightness(0) invert(1)',
}}
/>
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
background: 'var(--color-bg-base)',
padding: '20px',
position: 'relative',
overflow: 'hidden',
}}>
{/* 装饰性背景元素 */}
<div style={{
position: 'absolute',
top: '-10%',
right: '-5%',
width: '400px',
height: '400px',
background: 'var(--color-primary)',
opacity: 0.1,
borderRadius: '50%',
filter: 'blur(60px)',
}} />
<div style={{
position: 'absolute',
bottom: '-10%',
left: '-5%',
width: '350px',
height: '350px',
background: 'var(--color-success)',
opacity: 0.08,
borderRadius: '50%',
filter: 'blur(60px)',
}} />
<Card
style={{
width: '100%',
maxWidth: 420,
background: 'var(--color-bg-container)',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
boxShadow: 'var(--shadow-card)',
border: '1px solid var(--color-border)',
borderRadius: '16px',
position: 'relative',
zIndex: 1,
}}
bodyStyle={{
padding: '40px 32px',
}}
>
<Space direction="vertical" size="large" style={{ width: '100%', textAlign: 'center' }}>
{/* Logo区域 */}
<div style={{ marginBottom: '8px' }}>
<div style={{
width: '72px',
height: '72px',
margin: '0 auto 20px',
background: 'var(--color-primary)',
borderRadius: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: 'var(--shadow-primary)',
}}>
<img
src="/logo.svg"
alt="Logo"
style={{
width: '48px',
height: '48px',
filter: 'brightness(0) invert(1)',
}}
/>
</div>
<Title level={2} style={{
marginBottom: 8,
color: 'var(--color-primary)',
fontWeight: 700,
}}>
AI小说创作助手
</Title>
<Paragraph style={{
color: 'var(--color-text-secondary)',
fontSize: '14px',
marginBottom: 0,
}}>
{localAuthEnabled && linuxdoEnabled ? '选择登录方式' :
localAuthEnabled ? '使用账户密码登录' :
'使用 LinuxDO 账号登录'}
</Paragraph>
</div>
<Title level={2} style={{
marginBottom: 8,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
fontWeight: 700,
}}>
AI小说创作助手
</Title>
<Paragraph style={{
color: '#666',
fontSize: '14px',
marginBottom: 0,
}}>
{localAuthEnabled && linuxdoEnabled ? '选择登录方式' :
localAuthEnabled ? '使用账户密码登录' :
'使用 LinuxDO 账号登录'}
</Paragraph>
</div>
{/* 登录方式 */}
{localAuthEnabled && linuxdoEnabled ? (
<Tabs
defaultActiveKey="local"
centered
items={[
{
key: 'local',
label: '账户密码',
children: renderLocalLogin(),
},
{
key: 'linuxdo',
label: 'LinuxDO',
children: renderLinuxDOLogin(),
},
]}
/>
) : localAuthEnabled ? (
renderLocalLogin()
) : (
renderLinuxDOLogin()
)}
{/* 登录方式 */}
{localAuthEnabled && linuxdoEnabled ? (
<Tabs
defaultActiveKey="local"
centered
items={[
{
key: 'local',
label: '账户密码',
children: renderLocalLogin(),
},
{
key: 'linuxdo',
label: 'LinuxDO',
children: renderLinuxDOLogin(),
},
]}
/>
) : localAuthEnabled ? (
renderLocalLogin()
) : (
renderLinuxDOLogin()
)}
{/* 提示信息 */}
<div style={{
padding: '16px',
background: 'rgba(102, 126, 234, 0.08)',
borderRadius: '12px',
border: '1px solid rgba(102, 126, 234, 0.1)',
}}>
<Paragraph style={{
fontSize: 13,
color: '#666',
marginBottom: 0,
lineHeight: 1.6,
{/* 提示信息 */}
<div style={{
padding: '16px',
background: 'rgba(77, 128, 136, 0.08)',
borderRadius: '12px',
border: '1px solid var(--color-border)',
}}>
🎉
<br />
🔒
</Paragraph>
</div>
</Space>
</Card>
</div>
<Paragraph style={{
fontSize: 13,
color: 'var(--color-text-secondary)',
marginBottom: 0,
lineHeight: 1.6,
}}>
🎉
<br />
🔒
</Paragraph>
</div>
</Space>
</Card>
</div>
</>
);
}
+281 -255
View File
@@ -17,7 +17,8 @@ import {
Empty,
Alert,
Descriptions,
Layout,
Row,
Col,
} from 'antd';
import {
PlusOutlined,
@@ -35,7 +36,6 @@ import type { MCPPlugin, MCPTool } from '../types';
const { Paragraph, Text, Title } = Typography;
const { TextArea } = Input;
const { Header, Content } = Layout;
export default function MCPPluginsPage() {
const navigate = useNavigate();
@@ -85,7 +85,7 @@ export default function MCPPluginsPage() {
const handleEdit = (plugin: MCPPlugin) => {
setEditingPlugin(plugin);
// 重构为标准MCP配置格式
const mcpConfig: any = {
mcpServers: {
@@ -94,7 +94,7 @@ export default function MCPPluginsPage() {
}
}
};
if (plugin.plugin_type === 'http') {
mcpConfig.mcpServers[plugin.plugin_name].url = plugin.server_url;
mcpConfig.mcpServers[plugin.plugin_name].headers = plugin.headers || {};
@@ -103,7 +103,7 @@ export default function MCPPluginsPage() {
mcpConfig.mcpServers[plugin.plugin_name].args = plugin.args || [];
mcpConfig.mcpServers[plugin.plugin_name].env = plugin.env || {};
}
form.setFieldsValue({
config_json: JSON.stringify(mcpConfig, null, 2),
enabled: plugin.enabled,
@@ -145,10 +145,10 @@ export default function MCPPluginsPage() {
setTestingPluginId(pluginId);
try {
const result = await mcpPluginApi.testPlugin(pluginId);
// 测试完成后,无论成功失败都刷新插件列表以更新状态
await loadPlugins();
if (result.success) {
Modal.success({
title: '✅ 测试成功',
@@ -156,7 +156,7 @@ export default function MCPPluginsPage() {
content: (
<div>
<p style={{ marginBottom: 16, fontSize: '15px', fontWeight: 500 }}>{result.message}</p>
{/* 显示详细的测试结果 */}
{result.suggestions && result.suggestions.length > 0 && (
<div style={{ marginTop: 16 }}>
@@ -184,21 +184,21 @@ export default function MCPPluginsPage() {
</div>
</div>
)}
{/* 显示工具数量 */}
{result.tools_count !== undefined && (
<div style={{ marginTop: 12 }}>
<Text type="secondary">🔧 : <strong>{result.tools_count}</strong></Text>
</div>
)}
{/* 显示响应时间 */}
{result.response_time_ms !== undefined && (
<div style={{ marginTop: 4 }}>
<Text type="secondary"> : <strong>{result.response_time_ms}ms</strong></Text>
</div>
)}
<div style={{
marginTop: 16,
padding: 12,
@@ -220,7 +220,7 @@ export default function MCPPluginsPage() {
content: (
<div>
<p style={{ marginBottom: 16, fontSize: '15px', fontWeight: 500 }}>{result.message}</p>
{/* 显示错误信息 */}
{result.error && (
<div style={{ marginTop: 16 }}>
@@ -228,9 +228,9 @@ export default function MCPPluginsPage() {
<div style={{
marginTop: 8,
padding: 12,
background: '#fff2f0',
background: 'var(--color-error-bg)',
borderRadius: 4,
color: '#cf1322',
color: 'var(--color-error)',
fontSize: '13px',
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
@@ -242,7 +242,7 @@ export default function MCPPluginsPage() {
</div>
</div>
)}
{/* 显示建议 */}
{result.suggestions && result.suggestions.length > 0 && (
<div style={{ marginTop: 16 }}>
@@ -256,12 +256,12 @@ export default function MCPPluginsPage() {
</ul>
</div>
)}
<div style={{
marginTop: 16,
padding: 12,
background: '#fff7e6',
border: '1px solid #ffd591',
background: 'var(--color-warning-bg)',
border: '1px solid var(--color-warning-border)',
borderRadius: 4
}}>
<Text style={{ fontSize: '13px', color: '#ad6800' }}>
@@ -340,267 +340,293 @@ export default function MCPPluginsPage() {
};
return (
<Layout style={{ minHeight: '100vh', background: '#f0f2f5' }}>
{/* 顶部导航栏 */}
<Header style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: isMobile ? '0 16px' : '0 24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
position: 'fixed',
top: 0,
left: 0,
right: 0,
zIndex: 1000,
height: isMobile ? 56 : 64
}}>
<Space size={isMobile ? 12 : 16}>
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/')}
style={{
color: '#fff',
fontSize: isMobile ? 16 : 18,
display: 'flex',
alignItems: 'center'
}}
>
{!isMobile && '返回'}
</Button>
<Title level={isMobile ? 4 : 3} style={{
margin: 0,
color: '#fff',
fontSize: isMobile ? 18 : 24
}}>
MCP插件管理
</Title>
</Space>
</Header>
{/* 主内容区 */}
<Content style={{
marginTop: isMobile ? 56 : 64,
padding: isMobile ? '16px' : '24px',
<div style={{
minHeight: '100vh',
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)',
padding: isMobile ? '20px 16px' : '40px 24px',
display: 'flex',
flexDirection: 'column',
}}>
<div style={{
maxWidth: 1400,
margin: '0 auto',
width: '100%',
margin: `${isMobile ? 56 : 64}px auto 0`,
flex: 1,
display: 'flex',
flexDirection: 'column',
}}>
{/* 顶部导航卡片 */}
<Card
variant="borderless"
style={{
borderRadius: isMobile ? 8 : 12,
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
marginBottom: isMobile ? 16 : 24
background: 'linear-gradient(135deg, var(--color-primary) 0%, #5A9BA5 50%, var(--color-primary-hover) 100%)',
borderRadius: isMobile ? 16 : 24,
boxShadow: '0 12px 40px rgba(77, 128, 136, 0.25), 0 4px 12px rgba(0, 0, 0, 0.06)',
marginBottom: isMobile ? 20 : 24,
border: 'none',
position: 'relative',
overflow: 'hidden'
}}
>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: isMobile ? 16 : 20
}}>
<Title level={isMobile ? 5 : 4} style={{ margin: 0 }}>
</Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
size={isMobile ? 'middle' : 'large'}
style={{
borderRadius: 8,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none'
}}
>
</Button>
</div>
{/* 装饰性背景元素 */}
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.08)', pointerEvents: 'none' }} />
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.05)', pointerEvents: 'none' }} />
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.06)', pointerEvents: 'none' }} />
<Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
<Col xs={24} sm={12}>
<Space direction="vertical" size={4}>
<Space align="center">
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: '#fff', textShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
<ToolOutlined style={{ color: 'rgba(255,255,255,0.9)', marginRight: 8 }} />
MCP插件管理
</Title>
</Space>
<Text style={{ fontSize: isMobile ? 12 : 14, color: 'rgba(255,255,255,0.85)', marginLeft: isMobile ? 40 : 48 }}>
AI能力
</Text>
</Space>
</Col>
<Col xs={24} sm={12}>
<Space size={12} style={{ display: 'flex', justifyContent: isMobile ? 'flex-start' : 'flex-end', width: '100%' }}>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/')}
style={{
borderRadius: 12,
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.3)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
color: '#fff',
backdropFilter: 'blur(10px)',
transition: 'all 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
e.currentTarget.style.transform = 'translateY(-1px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
e.currentTarget.style.transform = 'none';
}}
>
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
style={{
borderRadius: 12,
background: 'rgba(255, 193, 7, 0.95)',
border: '1px solid rgba(255, 255, 255, 0.3)',
boxShadow: '0 4px 16px rgba(255, 193, 7, 0.4)',
color: '#fff',
fontWeight: 600
}}
>
</Button>
</Space>
</Col>
</Row>
{/* 使用提示 */}
<Alert
message="什么是 MCP 插件?"
message={
<Space align="center">
<InfoCircleOutlined style={{ fontSize: 16, color: 'var(--color-primary)' }} />
<Text strong style={{ fontSize: isMobile ? 13 : 14, color: 'var(--color-text-primary)' }}> MCP </Text>
</Space>
}
description={
<div style={{ fontSize: isMobile ? '12px' : '14px' }}>
<p style={{ margin: '8px 0' }}>
MCP (Model Context Protocol) AI
</p>
<p style={{ margin: '8px 0 0 0' }}>
MCP AI 访API
</p>
<div>
<Text style={{ fontSize: isMobile ? 12 : 13, display: 'block', marginBottom: 8 }}>
<strong>MCP (Model Context Protocol)</strong> AI
</Text>
<Text style={{ fontSize: isMobile ? 12 : 13, display: 'block' }}>
MCP AI 访API
</Text>
</div>
}
type="info"
showIcon
icon={<InfoCircleOutlined />}
style={{ marginBottom: isMobile ? 16 : 20 }}
showIcon={false}
style={{
marginTop: isMobile ? 16 : 24,
borderRadius: 12,
background: 'rgba(230, 247, 255, 0.6)',
border: '1px solid rgba(145, 213, 255, 0.6)',
backdropFilter: 'blur(5px)'
}}
/>
</Card>
{/* 插件列表 */}
<Spin spinning={loading}>
{plugins.length === 0 ? (
<Empty
description="还没有添加任何插件"
image={Empty.PRESENTED_IMAGE_SIMPLE}
style={{ padding: isMobile ? '40px 0' : '60px 0' }}
>
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
</Button>
</Empty>
) : (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
{plugins.map((plugin) => (
<Card
key={plugin.id}
size="small"
style={{
borderRadius: 8,
border: '1px solid #f0f0f0',
}}
>
<div
{/* 主内容区 */}
<div style={{ flex: 1 }}>
{/* 插件列表 */}
<Spin spinning={loading}>
{plugins.length === 0 ? (
<Empty
description="还没有添加任何插件"
image={Empty.PRESENTED_IMAGE_SIMPLE}
style={{ padding: isMobile ? '40px 0' : '60px 0' }}
>
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
</Button>
</Empty>
) : (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
{plugins.map((plugin) => (
<Card
key={plugin.id}
size="small"
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: '16px',
flexWrap: isMobile ? 'wrap' : 'nowrap',
borderRadius: 8,
border: '1px solid #f0f0f0',
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<Space direction="vertical" size="small" style={{ width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
<Text strong style={{ fontSize: isMobile ? '14px' : '16px' }}>
{plugin.display_name || plugin.plugin_name}
</Text>
{getStatusTag(plugin)}
<Tag color={plugin.plugin_type === 'http' ? 'blue' : 'cyan'}>
{plugin.plugin_type?.toUpperCase() || 'UNKNOWN'}
</Tag>
{plugin.category && plugin.category !== 'general' && (
<Tag color="purple">{plugin.category}</Tag>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: '16px',
flexWrap: isMobile ? 'wrap' : 'nowrap',
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<Space direction="vertical" size="small" style={{ width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
<Text strong style={{ fontSize: isMobile ? '14px' : '16px' }}>
{plugin.display_name || plugin.plugin_name}
</Text>
{getStatusTag(plugin)}
<Tag color={plugin.plugin_type === 'http' ? 'blue' : 'cyan'}>
{plugin.plugin_type?.toUpperCase() || 'UNKNOWN'}
</Tag>
{plugin.category && plugin.category !== 'general' && (
<Tag color="purple">{plugin.category}</Tag>
)}
</div>
{plugin.description && (
<Paragraph
type="secondary"
style={{
margin: 0,
fontSize: isMobile ? '12px' : '13px',
}}
ellipsis={{ rows: 2 }}
>
{plugin.description}
</Paragraph>
)}
</div>
{plugin.description && (
<Paragraph
type="secondary"
{/* 只显示有值的URL或命令,脱敏处理敏感信息 */}
{plugin.plugin_type === 'http' && plugin.server_url && (
<div style={{ fontSize: isMobile ? '11px' : '12px' }}>
<Text type="secondary" code>
{(() => {
// 脱敏处理:隐藏URL中的API Key
const url = plugin.server_url;
try {
const urlObj = new URL(url);
// 替换查询参数中的敏感信息
const params = new URLSearchParams(urlObj.search);
let maskedUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;
const sensitiveKeys = ['apiKey', 'api_key', 'key', 'token', 'secret', 'password', 'auth'];
let hasParams = false;
params.forEach((value, key) => {
const isSensitive = sensitiveKeys.some(k => key.toLowerCase().includes(k.toLowerCase()));
const maskedValue = isSensitive ? '***' : value;
maskedUrl += (hasParams ? '&' : '?') + `${key}=${maskedValue}`;
hasParams = true;
});
return maskedUrl;
} catch {
// 如果URL解析失败,尝试简单替换
return url.replace(/([?&])(apiKey|api_key|key|token|secret|password|auth)=([^&]+)/gi, '$1$2=***');
}
})()}
</Text>
</div>
)}
{plugin.plugin_type === 'stdio' && plugin.command && (
<div style={{ fontSize: isMobile ? '11px' : '12px' }}>
<Text type="secondary" code>
{plugin.command} {plugin.args?.join(' ')}
</Text>
</div>
)}
{/* 显示最后错误信息 */}
{plugin.last_error && (
<Text type="danger" style={{ fontSize: isMobile ? '11px' : '12px' }}>
: {plugin.last_error}
</Text>
)}
</Space>
</div>
<Space size="small" wrap>
<Tooltip title={plugin.enabled ? '禁用插件' : '启用插件'}>
<Switch
checked={plugin.enabled}
onChange={(checked) => handleToggle(plugin, checked)}
size={isMobile ? 'small' : 'default'}
style={{
margin: 0,
fontSize: isMobile ? '12px' : '13px',
flexShrink: 0,
height: isMobile ? 16 : 22,
minHeight: isMobile ? 16 : 22,
lineHeight: isMobile ? '16px' : '22px'
}}
ellipsis={{ rows: 2 }}
>
{plugin.description}
</Paragraph>
)}
{/* 只显示有值的URL或命令,脱敏处理敏感信息 */}
{plugin.plugin_type === 'http' && plugin.server_url && (
<div style={{ fontSize: isMobile ? '11px' : '12px' }}>
<Text type="secondary" code>
{(() => {
// 脱敏处理:隐藏URL中的API Key
const url = plugin.server_url;
try {
const urlObj = new URL(url);
// 替换查询参数中的敏感信息
const params = new URLSearchParams(urlObj.search);
let maskedUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;
const sensitiveKeys = ['apiKey', 'api_key', 'key', 'token', 'secret', 'password', 'auth'];
let hasParams = false;
params.forEach((value, key) => {
const isSensitive = sensitiveKeys.some(k => key.toLowerCase().includes(k.toLowerCase()));
const maskedValue = isSensitive ? '***' : value;
maskedUrl += (hasParams ? '&' : '?') + `${key}=${maskedValue}`;
hasParams = true;
});
return maskedUrl;
} catch {
// 如果URL解析失败,尝试简单替换
return url.replace(/([?&])(apiKey|api_key|key|token|secret|password|auth)=([^&]+)/gi, '$1$2=***');
}
})()}
</Text>
</div>
)}
{plugin.plugin_type === 'stdio' && plugin.command && (
<div style={{ fontSize: isMobile ? '11px' : '12px' }}>
<Text type="secondary" code>
{plugin.command} {plugin.args?.join(' ')}
</Text>
</div>
)}
{/* 显示最后错误信息 */}
{plugin.last_error && (
<Text type="danger" style={{ fontSize: isMobile ? '11px' : '12px' }}>
: {plugin.last_error}
</Text>
)}
/>
</Tooltip>
<Tooltip title="测试连接">
<Button
icon={<ThunderboltOutlined />}
onClick={() => handleTest(plugin.id)}
loading={testingPluginId === plugin.id}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
<Tooltip title="查看工具">
<Button
icon={<ToolOutlined />}
onClick={() => handleViewTools(plugin.id)}
disabled={!plugin.enabled || plugin.status !== 'active'}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
<Tooltip title="编辑">
<Button
icon={<EditOutlined />}
onClick={() => handleEdit(plugin)}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
<Tooltip title="删除">
<Button
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(plugin)}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
</Space>
</div>
<Space size="small" wrap>
<Tooltip title={plugin.enabled ? '禁用插件' : '启用插件'}>
<Switch
checked={plugin.enabled}
onChange={(checked) => handleToggle(plugin, checked)}
size={isMobile ? 'small' : 'default'}
style={{
flexShrink: 0,
height: isMobile ? 16 : 22,
minHeight: isMobile ? 16 : 22,
lineHeight: isMobile ? '16px' : '22px'
}}
/>
</Tooltip>
<Tooltip title="测试连接">
<Button
icon={<ThunderboltOutlined />}
onClick={() => handleTest(plugin.id)}
loading={testingPluginId === plugin.id}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
<Tooltip title="查看工具">
<Button
icon={<ToolOutlined />}
onClick={() => handleViewTools(plugin.id)}
disabled={!plugin.enabled || plugin.status !== 'active'}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
<Tooltip title="编辑">
<Button
icon={<EditOutlined />}
onClick={() => handleEdit(plugin)}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
<Tooltip title="删除">
<Button
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(plugin)}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
</Space>
</div>
</Card>
))}
</Space>
)}
</Spin>
</Content>
</Card>
))}
</Space>
)}
</Spin>
</div>
</div>
{/* 创建/编辑插件模态框 */}
<Modal
@@ -710,6 +736,6 @@ export default function MCPPluginsPage() {
</Space>
)}
</Modal>
</Layout>
</div>
);
}
+4 -2
View File
@@ -54,6 +54,7 @@ export default function Organizations() {
const [editMemberForm] = Form.useForm();
const [editOrgForm] = Form.useForm();
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const [modal, contextHolder] = Modal.useModal();
useEffect(() => {
const handleResize = () => {
@@ -129,7 +130,7 @@ export default function Organizations() {
};
const handleRemoveMember = async (memberId: string) => {
Modal.confirm({
modal.confirm({
title: '确认移除',
content: '确定要移除该成员吗?',
centered: true,
@@ -294,6 +295,7 @@ export default function Organizations() {
return (
<div>
{contextHolder}
<Card
title={
<Space wrap>
@@ -326,7 +328,7 @@ export default function Organizations() {
hoverable
style={{
cursor: 'pointer',
border: selectedOrg?.id === org.id ? '2px solid #1890ff' : '1px solid #d9d9d9'
border: selectedOrg?.id === org.id ? '2px solid var(--color-primary)' : '1px solid var(--color-border-secondary)'
}}
onClick={() => handleSelectOrganization(org)}
>
+47 -44
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
import { Button, List, Modal, Form, Input, message, Empty, Space, Popconfirm, Card, Select, Radio, Tag, InputNumber, Tooltip, Tabs } from 'antd';
import { EditOutlined, DeleteOutlined, ThunderboltOutlined, BranchesOutlined, AppstoreAddOutlined, CheckCircleOutlined, ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { useStore } from '../store';
@@ -41,6 +41,7 @@ export default function Outline() {
const [editForm] = Form.useForm();
const [generateForm] = Form.useForm();
const [expansionForm] = Form.useForm();
const [modalApi, contextHolder] = Modal.useModal();
const [batchExpansionForm] = Form.useForm();
const [manualCreateForm] = Form.useForm();
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
@@ -135,7 +136,7 @@ export default function Outline() {
const outline = outlines.find(o => o.id === id);
if (outline) {
editForm.setFieldsValue(outline);
Modal.confirm({
modalApi.confirm({
title: '编辑大纲',
width: 600,
centered: true,
@@ -335,7 +336,7 @@ export default function Outline() {
}
}
Modal.confirm({
modalApi.confirm({
title: hasOutlines ? (
<Space>
<span>AI生成/</span>
@@ -504,7 +505,7 @@ export default function Outline() {
console.log('已同步到Form,当前Form值:', generateForm.getFieldsValue());
}}
/>
<div style={{ color: '#666', fontSize: 12, marginTop: 4 }}>
<div style={{ color: 'var(--color-text-tertiary)', fontSize: 12, marginTop: 4 }}>
{defaultModel ? `当前默认模型: ${loadedModels.find(m => m.value === defaultModel)?.label || defaultModel}` : '未配置默认模型'}
</div>
</Form.Item>
@@ -526,7 +527,7 @@ export default function Outline() {
? Math.max(...outlines.map(o => o.order_index)) + 1
: 1;
Modal.confirm({
modalApi.confirm({
title: '手动创建大纲',
width: 600,
centered: true,
@@ -574,26 +575,26 @@ export default function Outline() {
// 校验序号是否重复
const existingOutline = outlines.find(o => o.order_index === values.order_index);
if (existingOutline) {
Modal.warning({
modalApi.warning({
title: '序号冲突',
content: (
<div>
<p> <strong>{values.order_index}</strong> 使</p>
<div style={{
padding: 12,
background: '#fff7e6',
background: 'var(--color-warning-bg)',
borderRadius: 4,
border: '1px solid #ffd591',
border: '1px solid var(--color-warning-border)',
marginTop: 8
}}>
<div style={{ fontWeight: 500, color: '#fa8c16' }}>
<div style={{ fontWeight: 500, color: 'var(--color-warning)' }}>
{currentProject?.outline_mode === 'one-to-one'
? `${existingOutline.order_index}`
: `${existingOutline.order_index}`
}{existingOutline.title}
</div>
</div>
<p style={{ marginTop: 12, color: '#666' }}>
<p style={{ marginTop: 12, color: 'var(--color-text-secondary)' }}>
💡 使 <strong>{nextOrderIndex}</strong>使
</p>
</div>
@@ -644,7 +645,7 @@ export default function Outline() {
if (!prevChapters.has_chapters) {
// 如果前面有未展开的大纲,显示提示并阻止操作
setIsExpanding(false);
Modal.warning({
modalApi.warning({
title: '请按顺序展开大纲',
width: 600,
centered: true,
@@ -655,18 +656,18 @@ export default function Outline() {
</p>
<div style={{
padding: 12,
background: '#fff7e6',
background: 'var(--color-warning-bg)',
borderRadius: 4,
border: '1px solid #ffd591'
border: '1px solid var(--color-warning-border)'
}}>
<div style={{ fontWeight: 500, marginBottom: 8, color: '#fa8c16' }}>
<div style={{ fontWeight: 500, marginBottom: 8, color: 'var(--color-warning)' }}>
</div>
<div style={{ color: '#666' }}>
<div style={{ color: 'var(--color-text-secondary)' }}>
{prevOutline.order_index}{prevOutline.title}
</div>
</div>
<p style={{ marginTop: 12, color: '#666', fontSize: 13 }}>
<p style={{ marginTop: 12, color: 'var(--color-text-secondary)', fontSize: 13 }}>
💡 使
</p>
</div>
@@ -694,7 +695,7 @@ export default function Outline() {
// 如果没有章节,显示展开表单
setIsExpanding(false);
Modal.confirm({
modalApi.confirm({
title: (
<Space>
<BranchesOutlined />
@@ -705,9 +706,9 @@ export default function Outline() {
centered: true,
content: (
<div>
<div style={{ marginBottom: 16, padding: 12, background: '#f5f5f5', borderRadius: 4 }}>
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-bg-layout)', borderRadius: 4 }}>
<div style={{ fontWeight: 500, marginBottom: 4 }}></div>
<div style={{ color: '#666' }}>{outlineTitle}</div>
<div style={{ color: 'var(--color-text-secondary)' }}>{outlineTitle}</div>
</div>
<Form
form={expansionForm}
@@ -855,10 +856,10 @@ export default function Outline() {
}> | null;
}
) => {
const modal = Modal.info({
modalApi.info({
title: (
<Space style={{ flexWrap: 'wrap' }}>
<CheckCircleOutlined style={{ color: '#52c41a' }} />
<CheckCircleOutlined style={{ color: 'var(--color-success)' }} />
<span></span>
</Space>
),
@@ -876,20 +877,20 @@ export default function Outline() {
overflowY: 'auto'
}
},
footer: (_, { OkBtn }) => (
footer: (_: any, { OkBtn }: any) => (
<Space wrap style={{ width: '100%', justifyContent: isMobile ? 'center' : 'flex-end' }}>
<Button
danger
icon={<DeleteOutlined />}
onClick={() => {
modal.destroy();
Modal.confirm({
Modal.destroyAll();
modalApi.confirm({
title: '确认删除',
icon: <ExclamationCircleOutlined />,
content: (
<div>
<p>{outlineTitle} <strong>{data.chapter_count}</strong> </p>
<p style={{ color: '#1890ff', marginTop: 8 }}>
<p style={{ color: 'var(--color-primary)', marginTop: 8 }}>
📝
</p>
<p style={{ color: '#ff4d4f', marginTop: 8 }}>
@@ -1077,13 +1078,14 @@ export default function Outline() {
))}
</Space>
</Card>
)}
)
}
</Space>
</div>
</div >
)
}))}
/>
</div>
</div >
),
});
};
@@ -1093,10 +1095,10 @@ export default function Outline() {
// 缓存AI生成的规划数据
const cachedPlans = response.chapter_plans;
Modal.confirm({
modalApi.confirm({
title: (
<Space>
<CheckCircleOutlined style={{ color: '#52c41a' }} />
<CheckCircleOutlined style={{ color: 'var(--color-success)' }} />
<span></span>
</Space>
),
@@ -1222,7 +1224,7 @@ export default function Outline() {
return;
}
Modal.confirm({
modalApi.confirm({
title: (
<Space>
<AppstoreAddOutlined />
@@ -1233,7 +1235,7 @@ export default function Outline() {
centered: true,
content: (
<div>
<div style={{ marginBottom: 16, padding: 12, background: '#fff3cd', borderRadius: 4 }}>
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-warning-bg)', borderRadius: 4 }}>
<div style={{ color: '#856404' }}>
{outlines.length}
</div>
@@ -1361,11 +1363,11 @@ export default function Outline() {
<div style={{
marginBottom: 16,
padding: 12,
background: '#fffbe6',
background: 'var(--color-warning-bg)',
borderRadius: 4,
border: '1px solid #ffe58f'
}}>
<div style={{ fontWeight: 500, marginBottom: 8, color: '#faad14' }}>
<div style={{ fontWeight: 500, marginBottom: 8, color: 'var(--color-warning)' }}>
</div>
<Space direction="vertical" size="small" style={{ width: '100%' }}>
@@ -1404,7 +1406,7 @@ export default function Outline() {
background: selectedOutlineIdx === idx ? '#e6f7ff' : 'transparent',
borderRadius: 4,
marginBottom: 4,
border: selectedOutlineIdx === idx ? '1px solid #1890ff' : '1px solid transparent'
border: selectedOutlineIdx === idx ? '1px solid var(--color-primary)' : '1px solid transparent'
}}
>
<div style={{ width: '100%' }}>
@@ -1445,7 +1447,7 @@ export default function Outline() {
background: selectedChapterIdx === idx ? '#e6f7ff' : 'transparent',
borderRadius: 4,
marginBottom: 4,
border: selectedChapterIdx === idx ? '1px solid #1890ff' : '1px solid transparent'
border: selectedChapterIdx === idx ? '1px solid var(--color-primary)' : '1px solid transparent'
}}
>
<div style={{ width: '100%' }}>
@@ -1719,7 +1721,7 @@ export default function Outline() {
<Modal
title={
<Space>
<ExclamationCircleOutlined style={{ color: '#faad14' }} />
<ExclamationCircleOutlined style={{ color: 'var(--color-warning)' }} />
<span></span>
</Space>
}
@@ -1731,7 +1733,7 @@ export default function Outline() {
handleConfirmCharacters(selectedCharacters);
}}
onCancel={() => {
Modal.confirm({
modalApi.confirm({
title: '确认操作',
content: '是否跳过角色创建,直接续写大纲?',
okText: '跳过角色,继续续写',
@@ -1745,7 +1747,7 @@ export default function Outline() {
cancelText="跳过角色创建"
>
<div>
<div style={{ marginBottom: 16, padding: 12, background: '#fffbe6', borderRadius: 4, border: '1px solid #ffe58f' }}>
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-warning-bg)', borderRadius: 4, border: '1px solid var(--color-warning-border)' }}>
<div style={{ fontWeight: 500, marginBottom: 8, color: '#d48806' }}>
AI
</div>
@@ -1785,7 +1787,7 @@ export default function Outline() {
padding: 12,
borderRadius: 4,
marginBottom: 8,
border: selectedCharacterIndices.includes(index) ? '1px solid #1890ff' : '1px solid #f0f0f0',
border: selectedCharacterIndices.includes(index) ? '1px solid var(--color-primary)' : '1px solid var(--color-border-secondary)',
cursor: 'pointer'
}}
onClick={() => {
@@ -1859,7 +1861,7 @@ export default function Outline() {
<Modal
title={
<Space>
<CheckCircleOutlined style={{ color: '#52c41a' }} />
<CheckCircleOutlined style={{ color: 'var(--color-success)' }} />
<span></span>
</Space>
}
@@ -1875,6 +1877,7 @@ export default function Outline() {
{renderBatchPreviewContent()}
</Modal>
{contextHolder}
{/* SSE进度Modal - 使用统一组件 */}
<SSEProgressModal
visible={sseModalVisible}
@@ -1889,7 +1892,7 @@ export default function Outline() {
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: '#fff',
backgroundColor: 'var(--color-bg-container)',
padding: isMobile ? '12px 0' : '16px 0',
marginBottom: isMobile ? 12 : 16,
borderBottom: '1px solid #f0f0f0',
@@ -1992,7 +1995,7 @@ export default function Outline() {
<List.Item.Meta
title={
<Space size="small" style={{ fontSize: isMobile ? 14 : 16, flexWrap: 'wrap' }}>
<span style={{ color: '#1890ff', fontWeight: 'bold' }}>
<span style={{ color: 'var(--color-primary)', fontWeight: 'bold' }}>
{currentProject?.outline_mode === 'one-to-one'
? `${item.order_index || '?'}`
: `${item.order_index || '?'}`
@@ -2058,4 +2061,4 @@ export default function Outline() {
</div>
</>
);
}
}
+19 -20
View File
@@ -193,7 +193,7 @@ export default function ProjectDetail() {
return (
<Layout style={{ minHeight: '100vh', height: '100vh', overflow: 'hidden' }}>
<Header style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
background: 'var(--color-primary)',
padding: mobile ? '0 12px' : '0 24px',
display: 'flex',
alignItems: 'center',
@@ -203,7 +203,7 @@ export default function ProjectDetail() {
left: 0,
right: 0,
zIndex: 1000,
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
boxShadow: 'var(--shadow-header)',
height: mobile ? 56 : 70
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', zIndex: 1 }}>
@@ -279,7 +279,7 @@ export default function ProjectDetail() {
<Card
size="small"
style={{
background: 'rgba(255,255,255,0.95)',
background: 'var(--color-bg-container)',
borderRadius: '6px',
border: 'none',
minWidth: '80px',
@@ -289,10 +289,10 @@ export default function ProjectDetail() {
styles={{ body: { padding: '8px' } }}
>
<Statistic
title={<span style={{ fontSize: '11px', color: '#666' }}></span>}
title={<span style={{ fontSize: '11px', color: 'var(--color-text-secondary)' }}></span>}
value={outlines.length}
suffix="条"
valueStyle={{ fontSize: '16px', fontWeight: 600, color: '#667eea' }}
valueStyle={{ fontSize: '16px', fontWeight: 600, color: 'var(--color-primary)' }}
/>
</Card>
</Col>
@@ -300,7 +300,7 @@ export default function ProjectDetail() {
<Card
size="small"
style={{
background: 'rgba(255,255,255,0.95)',
background: 'var(--color-bg-container)',
borderRadius: '6px',
border: 'none',
minWidth: '80px',
@@ -310,10 +310,10 @@ export default function ProjectDetail() {
styles={{ body: { padding: '8px' } }}
>
<Statistic
title={<span style={{ fontSize: '11px', color: '#666' }}></span>}
title={<span style={{ fontSize: '11px', color: 'var(--color-text-secondary)' }}></span>}
value={characters.length}
suffix="个"
valueStyle={{ fontSize: '16px', fontWeight: 600, color: '#52c41a' }}
valueStyle={{ fontSize: '16px', fontWeight: 600, color: 'var(--color-success)' }}
/>
</Card>
</Col>
@@ -321,7 +321,7 @@ export default function ProjectDetail() {
<Card
size="small"
style={{
background: 'rgba(255,255,255,0.95)',
background: 'var(--color-bg-container)',
borderRadius: '6px',
border: 'none',
minWidth: '80px',
@@ -331,10 +331,10 @@ export default function ProjectDetail() {
styles={{ body: { padding: '8px' } }}
>
<Statistic
title={<span style={{ fontSize: '11px', color: '#666' }}></span>}
title={<span style={{ fontSize: '11px', color: 'var(--color-text-secondary)' }}></span>}
value={chapters.length}
suffix="章"
valueStyle={{ fontSize: '16px', fontWeight: 600, color: '#1890ff' }}
valueStyle={{ fontSize: '16px', fontWeight: 600, color: 'var(--color-info)' }}
/>
</Card>
</Col>
@@ -342,7 +342,7 @@ export default function ProjectDetail() {
<Card
size="small"
style={{
background: 'rgba(255,255,255,0.95)',
background: 'var(--color-bg-container)',
borderRadius: '6px',
border: 'none',
minWidth: '80px',
@@ -352,10 +352,10 @@ export default function ProjectDetail() {
styles={{ body: { padding: '8px' } }}
>
<Statistic
title={<span style={{ fontSize: '11px', color: '#666' }}></span>}
title={<span style={{ fontSize: '11px', color: 'var(--color-text-secondary)' }}></span>}
value={currentProject.current_words}
suffix="字"
valueStyle={{ fontSize: '16px', fontWeight: 600, color: '#fa8c16' }}
valueStyle={{ fontSize: '16px', fontWeight: 600, color: 'var(--color-warning)' }}
/>
</Card>
</Col>
@@ -384,15 +384,14 @@ export default function ProjectDetail() {
trigger={null}
width={220}
collapsedWidth={60}
className="modern-sider"
style={{
background: '#fff',
position: 'fixed',
left: 0,
top: 70,
bottom: 0,
overflow: 'hidden',
boxShadow: '2px 0 12px rgba(0,0,0,0.08)',
transition: 'all 0.2s',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
height: 'calc(100vh - 70px)'
}}
>
@@ -412,7 +411,7 @@ export default function ProjectDetail() {
}}>
<Content
style={{
background: '#f5f7fa',
background: 'var(--color-bg-base)',
padding: mobile ? 12 : 24,
height: mobile ? 'calc(100vh - 56px)' : 'calc(100vh - 70px)',
overflow: 'hidden',
@@ -421,10 +420,10 @@ export default function ProjectDetail() {
}}
>
<div style={{
background: '#fff',
background: 'var(--color-bg-container)',
padding: mobile ? 12 : 24,
borderRadius: mobile ? '8px' : '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
boxShadow: 'var(--shadow-card)',
height: '100%',
overflow: 'hidden',
display: 'flex',
File diff suppressed because it is too large Load Diff
+14 -16
View File
@@ -18,7 +18,7 @@ export default function ProjectWizardNew() {
const [searchParams] = useSearchParams();
const [form] = Form.useForm();
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
// 状态管理
const [currentStep, setCurrentStep] = useState<'form' | 'generating'>('form');
const [generationConfig, setGenerationConfig] = useState<GenerationConfig | null>(null);
@@ -51,7 +51,7 @@ export default function ProjectWizardNew() {
throw new Error('获取项目信息失败');
}
const project = await response.json();
const config: GenerationConfig = {
title: project.title,
description: project.description || '',
@@ -62,7 +62,7 @@ export default function ProjectWizardNew() {
chapter_count: 3,
character_count: project.character_count || 5,
};
setGenerationConfig(config);
setCurrentStep('generating');
} catch (error) {
@@ -85,7 +85,7 @@ export default function ProjectWizardNew() {
character_count: values.character_count || 5,
outline_mode: values.outline_mode || 'one-to-many', // 添加大纲模式
};
setGenerationConfig(config);
setCurrentStep('generating');
};
@@ -195,7 +195,7 @@ export default function ProjectWizardNew() {
<Card
hoverable
style={{
borderColor: form.getFieldValue('outline_mode') === 'one-to-one' ? '#1890ff' : '#d9d9d9',
borderColor: form.getFieldValue('outline_mode') === 'one-to-one' ? 'var(--color-primary)' : 'var(--color-border)',
borderWidth: 2,
height: '100%',
}}
@@ -204,7 +204,7 @@ export default function ProjectWizardNew() {
<Radio value="one-to-one" style={{ width: '100%' }}>
<Space direction="vertical" size={4} style={{ width: '100%' }}>
<div style={{ fontSize: 16, fontWeight: 'bold' }}>
<CheckCircleOutlined style={{ marginRight: 8, color: '#52c41a' }} />
<CheckCircleOutlined style={{ marginRight: 8, color: 'var(--color-success)' }} />
(11)
</div>
<div style={{ fontSize: 12, color: '#666' }}>
@@ -217,12 +217,12 @@ export default function ProjectWizardNew() {
</Radio>
</Card>
</Col>
<Col xs={24} sm={12}>
<Card
hoverable
style={{
borderColor: form.getFieldValue('outline_mode') === 'one-to-many' ? '#1890ff' : '#d9d9d9',
borderColor: form.getFieldValue('outline_mode') === 'one-to-many' ? 'var(--color-primary)' : 'var(--color-border)',
borderWidth: 2,
height: '100%',
}}
@@ -231,7 +231,7 @@ export default function ProjectWizardNew() {
<Radio value="one-to-many" style={{ width: '100%' }}>
<Space direction="vertical" size={4} style={{ width: '100%' }}>
<div style={{ fontSize: 16, fontWeight: 'bold' }}>
<CheckCircleOutlined style={{ marginRight: 8, color: '#52c41a' }} />
<CheckCircleOutlined style={{ marginRight: 8, color: 'var(--color-success)' }} />
(1N)
</div>
<div style={{ fontSize: 12, color: '#666' }}>
@@ -321,17 +321,15 @@ export default function ProjectWizardNew() {
return (
<div style={{
minHeight: '100vh',
background: currentStep === 'generating'
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
: '#f5f7fa',
background: 'var(--color-bg-base)',
}}>
{/* 顶部标题栏 - 固定不滚动 */}
<div style={{
position: 'sticky',
top: 0,
zIndex: 100,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
background: 'var(--color-primary)',
boxShadow: 'var(--shadow-header)',
}}>
<div style={{
maxWidth: 1200,
@@ -354,7 +352,7 @@ export default function ProjectWizardNew() {
>
{isMobile ? '返回' : '返回首页'}
</Button>
<Title level={isMobile ? 4 : 2} style={{
margin: 0,
color: '#fff',
@@ -362,7 +360,7 @@ export default function ProjectWizardNew() {
}}>
</Title>
<div style={{ width: isMobile ? 60 : 120 }}></div>
</div>
</div>
+205 -161
View File
@@ -193,50 +193,87 @@ export default function PromptTemplates() {
return (
<div style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: isMobile ? '20px 16px' : '40px 24px'
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)',
padding: isMobile ? '20px 16px' : '40px 24px',
display: 'flex',
flexDirection: 'column',
}}>
{/* 头部卡片 */}
<div style={{
maxWidth: 1400,
margin: '0 auto',
marginBottom: isMobile ? 20 : 40
width: '100%',
flex: 1,
display: 'flex',
flexDirection: 'column',
}}>
{/* 顶部导航卡片 */}
<Card
variant="borderless"
style={{
background: 'rgba(255, 255, 255, 0.95)',
borderRadius: isMobile ? 12 : 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
background: 'linear-gradient(135deg, var(--color-primary) 0%, #5A9BA5 50%, var(--color-primary-hover) 100%)',
borderRadius: isMobile ? 16 : 24,
boxShadow: '0 12px 40px rgba(77, 128, 136, 0.25), 0 4px 12px rgba(0, 0, 0, 0.06)',
marginBottom: isMobile ? 20 : 24,
border: 'none',
position: 'relative',
overflow: 'hidden'
}}
>
<Row align="middle" justify="space-between" gutter={[16, 16]}>
{/* 装饰性背景元素 */}
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.08)', pointerEvents: 'none' }} />
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.05)', pointerEvents: 'none' }} />
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.06)', pointerEvents: 'none' }} />
<Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
<Col xs={24} sm={12} md={14}>
<Space direction="vertical" size={4}>
<Space align="center">
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/projects')}
size={isMobile ? 'small' : 'middle'}
/>
<Title level={isMobile ? 3 : 2} style={{ margin: 0 }}>
<FileSearchOutlined style={{ color: '#667eea', marginRight: 8 }} />
</Title>
</Space>
<Text type="secondary" style={{ fontSize: isMobile ? 12 : 14, marginLeft: isMobile ? 40 : 48 }}>
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: '#fff', textShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
<FileSearchOutlined style={{ color: 'rgba(255,255,255,0.9)', marginRight: 8 }} />
</Title>
<Text style={{ fontSize: isMobile ? 12 : 14, color: 'rgba(255,255,255,0.85)', marginLeft: isMobile ? 40 : 48 }}>
AI生成提示词
</Text>
</Space>
</Col>
<Col xs={24} sm={12} md={10}>
<Space wrap style={{ justifyContent: isMobile ? 'flex-start' : 'flex-end', width: '100%' }}>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/projects')}
style={{
borderRadius: 12,
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.3)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
color: '#fff',
backdropFilter: 'blur(10px)',
transition: 'all 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
e.currentTarget.style.transform = 'translateY(-1px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
e.currentTarget.style.transform = 'none';
}}
>
</Button>
<Button
icon={<DownloadOutlined />}
onClick={handleExport}
size={isMobile ? 'small' : 'middle'}
style={{ borderRadius: 8 }}
style={{
borderRadius: 12,
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.3)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
color: '#fff',
backdropFilter: 'blur(10px)',
transition: 'all 0.3s ease'
}}
>
</Button>
@@ -248,7 +285,14 @@ export default function PromptTemplates() {
<Button
icon={<UploadOutlined />}
size={isMobile ? 'small' : 'middle'}
style={{ borderRadius: 8 }}
style={{
borderRadius: 12,
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.3)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
color: '#fff',
backdropFilter: 'blur(10px)',
}}
>
</Button>
@@ -261,7 +305,7 @@ export default function PromptTemplates() {
<Alert
message={
<Space align="center">
<InfoCircleOutlined style={{ fontSize: 16, color: '#1890ff' }} />
<InfoCircleOutlined style={{ fontSize: 16, color: 'var(--color-primary)' }} />
<Text strong style={{ fontSize: isMobile ? 13 : 14 }}>使</Text>
</Space>
}
@@ -280,151 +324,151 @@ export default function PromptTemplates() {
style={{
marginTop: isMobile ? 16 : 24,
borderRadius: 12,
background: 'linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%)',
border: '1px solid #91d5ff'
background: 'var(--color-info-bg)',
border: '1px solid var(--color-info-border)'
}}
/>
</Card>
</div>
{/* 主内容区 */}
<div style={{ maxWidth: 1400, margin: '0 auto' }}>
<Spin spinning={loading}>
{/* 分类标签 */}
{categories.length > 0 && (
<Card
variant="borderless"
style={{
background: 'rgba(255, 255, 255, 0.95)',
borderRadius: isMobile ? 12 : 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
marginBottom: isMobile ? 16 : 24
}}
styles={{ body: { padding: isMobile ? '12px' : '16px' } }}
>
<Tabs
activeKey={selectedCategory}
onChange={setSelectedCategory}
items={[
{ key: '0', label: `全部 (${categories.reduce((sum, cat) => sum + cat.count, 0)})` },
...categories.map((cat, index) => ({
key: (index + 1).toString(),
label: `${cat.category} (${cat.count})`
}))
]}
/>
</Card>
)}
{/* 主内容区 */}
<div style={{ flex: 1 }}>
<Spin spinning={loading}>
{/* 分类标签 */}
{categories.length > 0 && (
<Card
variant="borderless"
style={{
background: 'rgba(255, 255, 255, 0.95)',
borderRadius: isMobile ? 12 : 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
marginBottom: isMobile ? 16 : 24
}}
styles={{ body: { padding: isMobile ? '12px' : '16px' } }}
>
<Tabs
activeKey={selectedCategory}
onChange={setSelectedCategory}
items={[
{ key: '0', label: `全部 (${categories.reduce((sum, cat) => sum + cat.count, 0)})` },
...categories.map((cat, index) => ({
key: (index + 1).toString(),
label: `${cat.category} (${cat.count})`
}))
]}
/>
</Card>
)}
{/* 模板列表 */}
{currentTemplates.length === 0 ? (
<Card
variant="borderless"
style={{
background: 'rgba(255, 255, 255, 0.95)',
borderRadius: 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
}}
>
<Empty
description="暂无模板数据"
style={{ padding: '80px 0' }}
/>
</Card>
) : (
<Row gutter={[16, 16]}>
{currentTemplates.map(template => (
<Col {...gridConfig} key={template.id}>
<Card
hoverable
variant="borderless"
style={cardStyles.project}
styles={{ body: { padding: 0, overflow: 'hidden' } }}
{...cardHoverHandlers}
>
{/* 头部 */}
<div style={{
background: template.is_system_default
? 'linear-gradient(135deg, #a8a8a8 0%, #636363 100%)'
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: isMobile ? '16px' : '20px',
position: 'relative'
}}>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Title level={isMobile ? 5 : 4} style={{ margin: 0, color: '#fff', flex: 1 }} ellipsis>
{template.template_name}
</Title>
{!template.is_system_default && (
<Switch
checked={template.is_active}
onChange={(checked) => handleToggleActive(template, checked)}
size={isMobile ? 'small' : 'default'}
style={{ marginLeft: 8 }}
/>
)}
</div>
<Space wrap>
<Tag color="rgba(255,255,255,0.3)" style={{ color: '#fff', border: 'none' }}>
{template.category}
</Tag>
<Tag color="rgba(255,255,255,0.3)" style={{ color: '#fff', border: 'none' }}>
{template.is_system_default ? '系统默认' : '已自定义'}
{/* 模板列表 */}
{currentTemplates.length === 0 ? (
<Card
variant="borderless"
style={{
background: 'rgba(255, 255, 255, 0.95)',
borderRadius: isMobile ? 12 : 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
}}
>
<Empty
description="暂无模板数据"
style={{ padding: '80px 0' }}
/>
</Card>
) : (
<Row gutter={[16, 16]}>
{currentTemplates.map(template => (
<Col {...gridConfig} key={template.id}>
<Card
hoverable
variant="borderless"
style={cardStyles.project}
styles={{ body: { padding: 0, overflow: 'hidden' } }}
{...cardHoverHandlers}
>
{/* 头部 */}
<div style={{
background: template.is_system_default
? 'var(--color-bg-layout)'
: 'var(--color-primary)',
padding: isMobile ? '16px' : '20px',
position: 'relative'
}}>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Title level={isMobile ? 5 : 4} style={{ margin: 0, color: template.is_system_default ? 'var(--color-text-primary)' : '#fff', flex: 1 }} ellipsis>
{template.template_name}
</Title>
{!template.is_system_default && (
<Switch
checked={template.is_active}
onChange={(checked) => handleToggleActive(template, checked)}
size={isMobile ? 'small' : 'default'}
style={{ marginLeft: 8 }}
/>
)}
</div>
<Space wrap>
<Tag color={template.is_system_default ? 'default' : 'rgba(255,255,255,0.3)'} style={{ color: template.is_system_default ? 'var(--color-text-secondary)' : '#fff', border: 'none' }}>
{template.category}
</Tag>
<Tag color={template.is_system_default ? 'default' : 'rgba(255,255,255,0.3)'} style={{ color: template.is_system_default ? 'var(--color-text-secondary)' : '#fff', border: 'none' }}>
{template.is_system_default ? '系统默认' : '已自定义'}
</Tag>
</Space>
</Space>
</div>
{/* 内容 */}
<div style={{ padding: isMobile ? '16px' : '20px' }}>
<Paragraph
type="secondary"
ellipsis={{ rows: 3 }}
style={{ minHeight: 66, marginBottom: 16 }}
>
{template.description || '暂无描述'}
</Paragraph>
<Space wrap style={{ marginBottom: 16 }}>
<Tag
icon={<CheckCircleOutlined />}
color={template.is_system_default || template.is_active ? 'success' : 'default'}
>
{template.is_system_default ? '始终启用' : (template.is_active ? '已启用' : '已禁用')}
</Tag>
</Space>
</Space>
</div>
{/* 内容 */}
<div style={{ padding: isMobile ? '16px' : '20px' }}>
<Paragraph
type="secondary"
ellipsis={{ rows: 3 }}
style={{ minHeight: 66, marginBottom: 16 }}
>
{template.description || '暂无描述'}
</Paragraph>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 16 }}>
: {template.template_key}
</Text>
<Space wrap style={{ marginBottom: 16 }}>
<Tag
icon={<CheckCircleOutlined />}
color={template.is_system_default || template.is_active ? 'success' : 'default'}
>
{template.is_system_default ? '始终启用' : (template.is_active ? '已启用' : '已禁用')}
</Tag>
</Space>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 16 }}>
: {template.template_key}
</Text>
{/* 操作按钮 */}
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Button
type="primary"
icon={<EditOutlined />}
onClick={() => handleEdit(template)}
size={isMobile ? 'small' : 'middle'}
style={{ borderRadius: 6 }}
>
</Button>
<Button
icon={<ReloadOutlined />}
onClick={() => handleReset(template.template_key)}
size={isMobile ? 'small' : 'middle'}
style={{ borderRadius: 6 }}
>
</Button>
</Space>
</div>
</Card>
</Col>
))}
</Row>
)}
</Spin>
{/* 操作按钮 */}
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Button
type="primary"
icon={<EditOutlined />}
onClick={() => handleEdit(template)}
size={isMobile ? 'small' : 'middle'}
style={{ borderRadius: 6 }}
>
</Button>
<Button
icon={<ReloadOutlined />}
onClick={() => handleReset(template.template_key)}
size={isMobile ? 'small' : 'middle'}
style={{ borderRadius: 6 }}
>
</Button>
</Space>
</div>
</Card>
</Col>
))}
</Row>
)}
</Spin>
</div>
</div>
{/* 编辑对话框 */}
@@ -486,6 +530,6 @@ export default function PromptTemplates() {
/>
</Space>
</Modal>
</div>
</div >
);
}
+71 -71
View File
@@ -42,7 +42,7 @@ export default function SettingsPage() {
try {
const settings = await settingsApi.getSettings();
form.setFieldsValue(settings);
// 判断是否为默认设置(id='0'表示来自.env的默认配置)
if (settings.id === '0' || !settings.id) {
setIsDefaultSettings(true);
@@ -164,7 +164,7 @@ export default function SettingsPage() {
api_base_url: apiBaseUrl,
provider: provider || 'openai'
});
setModelOptions(response.models);
setModelsFetched(true);
if (!silent) {
@@ -202,7 +202,7 @@ export default function SettingsPage() {
setTestingApi(true);
setTestResult(null);
try {
const result = await settingsApi.testApiConnection({
api_key: apiKey,
@@ -210,10 +210,10 @@ export default function SettingsPage() {
provider: provider,
llm_model: modelName
});
setTestResult(result);
setShowTestResult(true);
if (result.success) {
message.success(`测试成功!响应时间: ${result.response_time_ms}ms`);
} else {
@@ -238,7 +238,7 @@ export default function SettingsPage() {
return (
<div style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
background: 'var(--color-bg-base)',
padding: isMobile ? '16px 12px' : '40px 24px'
}}>
<div style={{
@@ -248,9 +248,9 @@ export default function SettingsPage() {
<Card
variant="borderless"
style={{
background: 'rgba(255, 255, 255, 0.95)',
background: 'var(--color-bg-container)',
borderRadius: isMobile ? 12 : 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
boxShadow: 'var(--shadow-card)',
}}
styles={{
body: {
@@ -281,7 +281,7 @@ export default function SettingsPage() {
fontSize: isMobile ? '18px' : undefined
}}
>
<SettingOutlined style={{ marginRight: 8, color: '#667eea' }} />
<SettingOutlined style={{ marginRight: 8, color: 'var(--color-primary)' }} />
{isMobile ? 'API 设置' : 'AI API 设置'}
</Title>
</Space>
@@ -341,7 +341,7 @@ export default function SettingsPage() {
<Space size={4}>
<span>API </span>
<Tooltip title="选择你的AI服务提供商">
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
</Tooltip>
</Space>
}
@@ -362,7 +362,7 @@ export default function SettingsPage() {
<Space size={4}>
<span>API </span>
<Tooltip title="你的API密钥,将加密存储">
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
</Tooltip>
</Space>
}
@@ -381,7 +381,7 @@ export default function SettingsPage() {
<Space size={4}>
<span>API </span>
<Tooltip title="API的基础URL地址">
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
</Tooltip>
</Space>
}
@@ -402,7 +402,7 @@ export default function SettingsPage() {
<Space size={4}>
<span></span>
<Tooltip title="AI模型的名称,如 gpt-4, gpt-3.5-turbo">
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
</Tooltip>
</Space>
}
@@ -424,7 +424,7 @@ export default function SettingsPage() {
<>
{menu}
{fetchingModels && (
<div style={{ padding: '8px 12px', color: '#8c8c8c', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
<Spin size="small" /> ...
</div>
)}
@@ -434,7 +434,7 @@ export default function SettingsPage() {
</div>
)}
{!fetchingModels && modelOptions.length === 0 && !modelsFetched && (
<div style={{ padding: '8px 12px', color: '#8c8c8c', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
</div>
)}
@@ -446,7 +446,7 @@ export default function SettingsPage() {
<Spin size="small" /> ...
</div>
) : (
<div style={{ padding: '8px 12px', color: '#8c8c8c', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
</div>
)
@@ -506,7 +506,7 @@ export default function SettingsPage() {
<Space size={4}>
<span></span>
<Tooltip title="控制输出的随机性,值越高越随机(0.0-2.0)">
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
</Tooltip>
</Space>
}
@@ -530,7 +530,7 @@ export default function SettingsPage() {
<Space size={4}>
<span> Token </span>
<Tooltip title="单次请求的最大token数量">
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
</Tooltip>
</Space>
}
@@ -554,9 +554,9 @@ export default function SettingsPage() {
message={
<Space>
{testResult.success ? (
<CheckCircleOutlined style={{ color: '#52c41a', fontSize: isMobile ? '16px' : '18px' }} />
<CheckCircleOutlined style={{ color: 'var(--color-success)', fontSize: isMobile ? '16px' : '18px' }} />
) : (
<CloseCircleOutlined style={{ color: '#ff4d4f', fontSize: isMobile ? '16px' : '18px' }} />
<CloseCircleOutlined style={{ color: 'var(--color-error)', fontSize: isMobile ? '16px' : '18px' }} />
)}
<span style={{ fontSize: isMobile ? '14px' : '16px', fontWeight: 500 }}>
{testResult.message}
@@ -585,7 +585,7 @@ export default function SettingsPage() {
<div style={{ color: '#595959' }}>{testResult.response_preview}</div>
</div>
)}
<div style={{ color: '#52c41a', fontSize: isMobile ? '12px' : '13px', marginTop: '4px' }}>
<div style={{ color: 'var(--color-success)', fontSize: isMobile ? '12px' : '13px', marginTop: '4px' }}>
API 使
</div>
</Space>
@@ -604,7 +604,7 @@ export default function SettingsPage() {
</div>
)}
{testResult.error_type && (
<div style={{ fontSize: isMobile ? '11px' : '12px', color: '#8c8c8c' }}>
<div style={{ fontSize: isMobile ? '11px' : '12px', color: 'var(--color-text-secondary)' }}>
: {testResult.error_type}
</div>
)}
@@ -639,61 +639,61 @@ export default function SettingsPage() {
{/* 操作按钮 */}
<Form.Item style={{ marginBottom: 0, marginTop: isMobile ? 24 : 32 }}>
{isMobile ? (
// 移动端:垂直堆叠布局
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Button
type="primary"
size="large"
icon={<SaveOutlined />}
htmlType="submit"
loading={loading}
block
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
height: '44px'
}}
>
</Button>
// 移动端:垂直堆叠布局
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Button
type="primary"
size="large"
icon={<SaveOutlined />}
htmlType="submit"
loading={loading}
block
style={{
background: 'var(--color-primary)',
border: 'none',
height: '44px'
}}
>
</Button>
<Button
size="large"
icon={<ThunderboltOutlined />}
onClick={handleTestConnection}
loading={testingApi}
block
style={{
borderColor: 'var(--color-success)',
color: 'var(--color-success)',
fontWeight: 500,
height: '44px'
}}
>
{testingApi ? '测试中...' : '测试连接'}
</Button>
<Space size="middle" style={{ width: '100%' }}>
<Button
size="large"
icon={<ThunderboltOutlined />}
onClick={handleTestConnection}
loading={testingApi}
block
style={{
borderColor: '#52c41a',
color: '#52c41a',
fontWeight: 500,
height: '44px'
}}
icon={<ReloadOutlined />}
onClick={handleReset}
style={{ flex: 1, height: '44px' }}
>
{testingApi ? '测试中...' : '测试连接'}
</Button>
<Space size="middle" style={{ width: '100%' }}>
{hasSettings && (
<Button
danger
size="large"
icon={<ReloadOutlined />}
onClick={handleReset}
icon={<DeleteOutlined />}
onClick={handleDelete}
loading={loading}
style={{ flex: 1, height: '44px' }}
>
</Button>
{hasSettings && (
<Button
danger
size="large"
icon={<DeleteOutlined />}
onClick={handleDelete}
loading={loading}
style={{ flex: 1, height: '44px' }}
>
</Button>
)}
</Space>
)}
</Space>
</Space>
) : (
// 桌面端:删除在左边,测试、重置和保存在右边
<div style={{
@@ -720,7 +720,7 @@ export default function SettingsPage() {
) : (
<div /> // 占位符,保持右侧按钮位置
)}
{/* 右侧:测试、重置和保存按钮组 */}
<Space size="middle">
<Button
@@ -729,8 +729,8 @@ export default function SettingsPage() {
onClick={handleTestConnection}
loading={testingApi}
style={{
borderColor: '#52c41a',
color: '#52c41a',
borderColor: 'var(--color-success)',
color: 'var(--color-success)',
fontWeight: 500,
minWidth: '100px'
}}
@@ -754,7 +754,7 @@ export default function SettingsPage() {
htmlType="submit"
loading={loading}
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
background: 'var(--color-primary)',
border: 'none',
minWidth: '120px',
fontWeight: 500
+12 -12
View File
@@ -28,17 +28,17 @@ const sponsorOptions: SponsorOption[] = [
const benefits = [
{
icon: <FileTextOutlined style={{ fontSize: '32px', color: '#1890ff' }} />,
icon: <FileTextOutlined style={{ fontSize: '32px', color: 'var(--color-primary)' }} />,
title: '优先需求响应',
description: '您的功能需求和问题反馈将获得优先处理'
},
{
icon: <RocketOutlined style={{ fontSize: '32px', color: '#52c41a' }} />,
icon: <RocketOutlined style={{ fontSize: '32px', color: 'var(--color-success)' }} />,
title: 'Windows一键启动',
description: '获取免安装EXE程序,双击即可使用'
},
{
icon: <MessageOutlined style={{ fontSize: '32px', color: '#fa8c16' }} />,
icon: <MessageOutlined style={{ fontSize: '32px', color: 'var(--color-warning)' }} />,
title: '专属技术支持',
description: '加入赞助者群,获得远程协助和配置指导'
}
@@ -75,7 +75,7 @@ export default function Sponsor() {
<div style={{
marginTop: '16px',
padding: '16px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
background: 'var(--color-primary)',
borderRadius: '12px',
color: '#fff'
}}>
@@ -91,7 +91,7 @@ export default function Sponsor() {
{/* 赞助专属权益 */}
<div style={{ marginBottom: '32px' }}>
<Title level={3} style={{ textAlign: 'center', marginBottom: '20px' }}>
<CheckCircleOutlined style={{ color: '#52c41a', marginRight: '8px' }} />
<CheckCircleOutlined style={{ color: 'var(--color-success)', marginRight: '8px' }} />
</Title>
@@ -139,27 +139,27 @@ export default function Sponsor() {
style={{
textAlign: 'center',
borderRadius: '10px',
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
boxShadow: 'var(--shadow-card)',
cursor: 'pointer',
transition: 'all 0.3s',
border: '2px solid #f0f0f0'
border: '2px solid var(--color-border)'
}}
styles={{
body: { padding: '20px 12px' }
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-8px)';
e.currentTarget.style.boxShadow = '0 8px 24px rgba(0,0,0,0.15)';
e.currentTarget.style.borderColor = '#1890ff';
e.currentTarget.style.boxShadow = 'var(--shadow-elevated)';
e.currentTarget.style.borderColor = 'var(--color-primary)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.08)';
e.currentTarget.style.borderColor = '#f0f0f0';
e.currentTarget.style.boxShadow = 'var(--shadow-card)';
e.currentTarget.style.borderColor = 'var(--color-border)';
}}
>
<Title level={3} style={{
color: '#1890ff',
color: 'var(--color-primary)',
marginBottom: '4px',
fontSize: '28px',
fontWeight: 'bold'
+61 -46
View File
@@ -56,9 +56,10 @@ export default function UserManagement() {
const [pageSize, setPageSize] = useState(20);
const [currentPage, setCurrentPage] = useState(1);
const [searchText, setSearchText] = useState('');
const [form] = Form.useForm();
const [editForm] = Form.useForm();
const [modal, contextHolder] = Modal.useModal();
// 过滤用户列表
const filteredUsers = users.filter(user => {
@@ -94,10 +95,10 @@ export default function UserManagement() {
try {
const res = await adminApi.createUser(values);
message.success('用户创建成功');
// 如果有默认密码,显示给管理员
if (res.default_password) {
Modal.info({
modal.info({
title: '用户创建成功',
content: (
<div>
@@ -112,7 +113,7 @@ export default function UserManagement() {
centered: true,
});
}
setModalVisible(false);
form.resetFields();
loadUsers();
@@ -136,7 +137,7 @@ export default function UserManagement() {
const handleUpdate = async (values: any) => {
if (!currentUser) return;
try {
await adminApi.updateUser(currentUser.user_id, values);
message.success('用户信息更新成功');
@@ -153,7 +154,7 @@ export default function UserManagement() {
const handleToggleStatus = async (user: UserWithStatus) => {
const isActive = user.is_active !== false;
const action = isActive ? '禁用' : '启用';
try {
await adminApi.toggleUserStatus(user.user_id, !isActive);
message.success(`用户已${action}`);
@@ -173,14 +174,14 @@ export default function UserManagement() {
const handleResetPasswordConfirm = async () => {
if (!currentUser) return;
try {
const res = await adminApi.resetPassword(
currentUser.user_id,
newPassword || undefined
);
Modal.info({
modal.info({
title: '密码重置成功',
content: (
<div>
@@ -194,7 +195,7 @@ export default function UserManagement() {
width: 500,
centered: true,
});
setResetPasswordModalVisible(false);
setNewPassword('');
} catch (error) {
@@ -226,7 +227,7 @@ export default function UserManagement() {
width: 150,
render: (text: string) => (
<Space>
<UserOutlined style={{ color: '#1890ff' }} />
<UserOutlined style={{ color: 'var(--color-primary)' }} />
<Text strong>{text}</Text>
</Space>
),
@@ -292,7 +293,7 @@ export default function UserManagement() {
fixed: 'right' as const,
render: (_: any, record: UserWithStatus) => {
const isActive = record.is_active !== false;
// 移动端:使用下拉菜单
if (isMobile) {
const menuItems = [
@@ -314,7 +315,7 @@ export default function UserManagement() {
icon: isActive ? <StopOutlined /> : <CheckCircleOutlined />,
danger: isActive,
onClick: () => {
Modal.confirm({
modal.confirm({
title: `确定${isActive ? '禁用' : '启用'}该用户吗?`,
onOk: () => handleToggleStatus(record),
okText: '确定',
@@ -328,7 +329,7 @@ export default function UserManagement() {
icon: <DeleteOutlined />,
danger: true,
onClick: () => {
Modal.confirm({
modal.confirm({
title: '确定删除该用户吗?此操作不可恢复!',
onOk: () => handleDelete(record),
okText: '确定',
@@ -345,7 +346,7 @@ export default function UserManagement() {
</Dropdown>
);
}
// 桌面端:保持原有按钮样式
return (
<Space size="small">
@@ -359,7 +360,7 @@ export default function UserManagement() {
</Button>
</Tooltip>
<Tooltip title="重置密码">
<Button
type="link"
@@ -370,7 +371,7 @@ export default function UserManagement() {
</Button>
</Tooltip>
<Popconfirm
title={`确定${isActive ? '禁用' : '启用'}该用户吗?`}
onConfirm={() => handleToggleStatus(record)}
@@ -388,7 +389,7 @@ export default function UserManagement() {
</Button>
</Tooltip>
</Popconfirm>
{!record.is_admin && (
<Popconfirm
title="确定删除该用户吗?此操作不可恢复!"
@@ -418,12 +419,13 @@ export default function UserManagement() {
return (
<div style={{
height: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)',
padding: isMobile ? '20px 16px' : '40px 24px',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}>
{contextHolder}
<div style={{
maxWidth: 1400,
margin: '0 auto',
@@ -437,20 +439,28 @@ export default function UserManagement() {
<Card
variant="borderless"
style={{
background: 'rgba(255, 255, 255, 0.95)',
borderRadius: isMobile ? 12 : 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
background: 'linear-gradient(135deg, var(--color-primary) 0%, #5A9BA5 50%, var(--color-primary-hover) 100%)',
borderRadius: isMobile ? 16 : 24,
boxShadow: '0 12px 40px rgba(77, 128, 136, 0.25), 0 4px 12px rgba(0, 0, 0, 0.06)',
marginBottom: isMobile ? 20 : 24,
border: 'none',
position: 'relative',
overflow: 'hidden'
}}
>
<Row align="middle" justify="space-between" gutter={[16, 16]}>
{/* 装饰性背景元素 */}
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.08)', pointerEvents: 'none' }} />
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.05)', pointerEvents: 'none' }} />
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.06)', pointerEvents: 'none' }} />
<Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
<Col xs={24} sm={12}>
<Space direction="vertical" size={4}>
<Title level={isMobile ? 3 : 2} style={{ margin: 0 }}>
<TeamOutlined style={{ color: '#fa8c16', marginRight: 8 }} />
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: '#fff', textShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
<TeamOutlined style={{ color: 'rgba(255,255,255,0.9)', marginRight: 12 }} />
</Title>
<Text type="secondary" style={{ fontSize: isMobile ? 12 : 14 }}>
<Text style={{ fontSize: isMobile ? 12 : 14, color: 'rgba(255,255,255,0.85)' }}>
</Text>
</Space>
@@ -461,20 +471,21 @@ export default function UserManagement() {
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/')}
style={{
borderRadius: 8,
borderColor: '#d9d9d9',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
borderRadius: 12,
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.3)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
color: '#fff',
backdropFilter: 'blur(10px)',
transition: 'all 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#667eea';
e.currentTarget.style.color = '#667eea';
e.currentTarget.style.boxShadow = '0 2px 12px rgba(102, 126, 234, 0.3)';
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
e.currentTarget.style.transform = 'translateY(-1px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#d9d9d9';
e.currentTarget.style.color = 'rgba(0, 0, 0, 0.88)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.08)';
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
e.currentTarget.style.transform = 'none';
}}
>
@@ -484,10 +495,12 @@ export default function UserManagement() {
icon={<PlusOutlined />}
onClick={() => setModalVisible(true)}
style={{
borderRadius: 8,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.4)'
borderRadius: 12,
background: 'rgba(255, 193, 7, 0.95)',
border: '1px solid rgba(255, 255, 255, 0.3)',
boxShadow: '0 4px 16px rgba(255, 193, 7, 0.4)',
color: '#fff',
fontWeight: 600
}}
>
@@ -502,9 +515,11 @@ export default function UserManagement() {
<Card
variant="borderless"
style={{
background: 'rgba(255, 255, 255, 0.95)',
borderRadius: isMobile ? 12 : 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
background: 'rgba(255, 255, 255, 0.7)',
borderRadius: isMobile ? 16 : 24,
border: '1px solid rgba(255, 255, 255, 0.4)',
backdropFilter: 'blur(20px)',
boxShadow: '0 4px 24px rgba(0, 0, 0, 0.04)',
flex: 1,
display: 'flex',
flexDirection: 'column',
@@ -521,7 +536,7 @@ export default function UserManagement() {
{/* 搜索栏 */}
<div style={{
padding: '16px 24px 0 24px',
borderBottom: '1px solid #f0f0f0',
borderBottom: '1px solid rgba(0, 0, 0, 0.03)',
}}>
<Input
placeholder="搜索用户名、显示名称或用户ID"
@@ -556,12 +571,12 @@ export default function UserManagement() {
pagination={false}
/>
</div>
{/* 固定分页控件 */}
<div style={{
padding: '16px 24px 24px 24px',
borderTop: '1px solid #f0f0f0',
background: 'rgba(255, 255, 255, 0.95)',
borderTop: '1px solid rgba(0, 0, 0, 0.03)',
background: 'transparent',
display: 'flex',
justifyContent: 'center',
}}>
+115 -113
View File
@@ -25,12 +25,13 @@ export default function WorldSetting() {
rules: string;
} | null>(null);
const [isSavingPreview, setIsSavingPreview] = useState(false);
const [modal, contextHolder] = Modal.useModal();
// AI重新生成世界观
const handleRegenerate = async () => {
if (!currentProject) return;
Modal.confirm({
modal.confirm({
title: '确认重新生成',
content: '确定要使用AI重新生成世界观设定吗?这将替换当前的世界观内容。',
centered: true,
@@ -139,14 +140,14 @@ export default function WorldSetting() {
backgroundColor: '#fff',
padding: '16px 0',
marginBottom: 16,
borderBottom: '1px solid #f0f0f0',
borderBottom: '1px solid var(--color-border-secondary)',
display: 'flex',
alignItems: 'center'
}}>
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: '#1890ff' }} />
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: 'var(--color-primary)' }} />
<h2 style={{ margin: 0 }}></h2>
</div>
{/* 可滚动内容区域 */}
<div style={{ flex: 1, overflowY: 'auto' }}>
<Empty
@@ -164,6 +165,7 @@ export default function WorldSetting() {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{contextHolder}
{/* 固定头部 */}
<div style={{
position: 'sticky',
@@ -178,7 +180,7 @@ export default function WorldSetting() {
justifyContent: 'space-between'
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: '#1890ff' }} />
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: 'var(--color-primary)' }} />
<h2 style={{ margin: 0 }}></h2>
</div>
<Space>
@@ -210,116 +212,116 @@ export default function WorldSetting() {
{/* 可滚动内容区域 */}
<div style={{ flex: 1, overflowY: 'auto' }}>
<Card
style={{
...cardStyles.base,
marginBottom: 16
}}
title={
<span style={{ fontSize: 18, fontWeight: 500 }}>
</span>
}
>
<Descriptions bordered column={1} styles={{ label: { width: 120, fontWeight: 500 } }}>
<Descriptions.Item label="小说名称">{currentProject.title}</Descriptions.Item>
{currentProject.description && (
<Descriptions.Item label="小说简介">{currentProject.description}</Descriptions.Item>
)}
<Descriptions.Item label="小说主题">{currentProject.theme || '未设定'}</Descriptions.Item>
<Descriptions.Item label="小说类型">{currentProject.genre || '未设定'}</Descriptions.Item>
<Descriptions.Item label="叙事视角">{currentProject.narrative_perspective || '未设定'}</Descriptions.Item>
<Descriptions.Item label="目标字数">
{currentProject.target_words ? `${currentProject.target_words.toLocaleString()}` : '未设定'}
</Descriptions.Item>
</Descriptions>
</Card>
style={{
...cardStyles.base,
marginBottom: 16
}}
title={
<span style={{ fontSize: 18, fontWeight: 500 }}>
</span>
}
>
<Descriptions bordered column={1} styles={{ label: { width: 120, fontWeight: 500 } }}>
<Descriptions.Item label="小说名称">{currentProject.title}</Descriptions.Item>
{currentProject.description && (
<Descriptions.Item label="小说简介">{currentProject.description}</Descriptions.Item>
)}
<Descriptions.Item label="小说主题">{currentProject.theme || '未设定'}</Descriptions.Item>
<Descriptions.Item label="小说类型">{currentProject.genre || '未设定'}</Descriptions.Item>
<Descriptions.Item label="叙事视角">{currentProject.narrative_perspective || '未设定'}</Descriptions.Item>
<Descriptions.Item label="目标字数">
{currentProject.target_words ? `${currentProject.target_words.toLocaleString()}` : '未设定'}
</Descriptions.Item>
</Descriptions>
</Card>
<Card
style={{
...cardStyles.base,
marginBottom: 16
}}
title={
<span style={{ fontSize: 18, fontWeight: 500 }}>
<GlobalOutlined style={{ marginRight: 8 }} />
</span>
}
>
<div style={{ padding: '16px 0' }}>
{currentProject.world_time_period && (
<div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: '#1890ff', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: '#f5f5f5',
borderRadius: 8,
borderLeft: '4px solid #1890ff'
}}>
{currentProject.world_time_period}
</Paragraph>
</div>
)}
<Card
style={{
...cardStyles.base,
marginBottom: 16
}}
title={
<span style={{ fontSize: 18, fontWeight: 500 }}>
<GlobalOutlined style={{ marginRight: 8 }} />
</span>
}
>
<div style={{ padding: '16px 0' }}>
{currentProject.world_time_period && (
<div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: 'var(--color-primary)', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: 'var(--color-bg-layout)',
borderRadius: 8,
borderLeft: '4px solid var(--color-primary)'
}}>
{currentProject.world_time_period}
</Paragraph>
</div>
)}
{currentProject.world_location && (
<div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: '#52c41a', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: '#f5f5f5',
borderRadius: 8,
borderLeft: '4px solid #52c41a'
}}>
{currentProject.world_location}
</Paragraph>
</div>
)}
{currentProject.world_location && (
<div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: 'var(--color-success)', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: 'var(--color-bg-layout)',
borderRadius: 8,
borderLeft: '4px solid var(--color-success)'
}}>
{currentProject.world_location}
</Paragraph>
</div>
)}
{currentProject.world_atmosphere && (
<div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: '#faad14', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: '#f5f5f5',
borderRadius: 8,
borderLeft: '4px solid #faad14'
}}>
{currentProject.world_atmosphere}
</Paragraph>
</div>
)}
{currentProject.world_atmosphere && (
<div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: 'var(--color-warning)', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: 'var(--color-bg-layout)',
borderRadius: 8,
borderLeft: '4px solid var(--color-warning)'
}}>
{currentProject.world_atmosphere}
</Paragraph>
</div>
)}
{currentProject.world_rules && (
<div style={{ marginBottom: 0 }}>
<Title level={5} style={{ color: '#f5222d', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: '#f5f5f5',
borderRadius: 8,
borderLeft: '4px solid #f5222d'
}}>
{currentProject.world_rules}
</Paragraph>
</div>
)}
</div>
</Card>
{currentProject.world_rules && (
<div style={{ marginBottom: 0 }}>
<Title level={5} style={{ color: 'var(--color-error)', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: 'var(--color-bg-layout)',
borderRadius: 8,
borderLeft: '4px solid var(--color-error)'
}}>
{currentProject.world_rules}
</Paragraph>
</div>
)}
</div>
</Card>
</div>
{/* 编辑世界观模态框 */}
@@ -440,14 +442,14 @@ export default function WorldSetting() {
>
{newWorldData && (
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
<div style={{ marginBottom: 24, padding: 16, background: '#fff7e6', border: '1px solid #ffd591', borderRadius: 8 }}>
<div style={{ marginBottom: 24, padding: 16, background: 'var(--color-warning-bg)', border: '1px solid var(--color-warning-border)', borderRadius: 8 }}>
<Typography.Text type="warning" strong>
"确认替换"
</Typography.Text>
</div>
<div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: '#1890ff', marginBottom: 12 }}>
<Title level={5} style={{ color: 'var(--color-primary)', marginBottom: 12 }}>
</Title>
<Paragraph style={{