refactor: 大量前端页面/组件样式从硬编码颜色迁移到 antd token 主题变量
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo, useEffect, useRef } from 'react';
|
||||
import { theme } from 'antd';
|
||||
|
||||
// 标注数据类型
|
||||
export interface MemoryAnnotation {
|
||||
@@ -35,14 +36,6 @@ interface AnnotatedTextProps {
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
// 类型颜色映射
|
||||
const TYPE_COLORS = {
|
||||
hook: '#ff6b6b',
|
||||
foreshadow: '#6b7bff',
|
||||
plot_point: '#51cf66',
|
||||
character_event: '#ffd93d',
|
||||
};
|
||||
|
||||
// 类型图标映射
|
||||
const TYPE_ICONS = {
|
||||
hook: '🎣',
|
||||
@@ -65,6 +58,14 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
|
||||
}) => {
|
||||
const annotationRefs = useRef<Record<string, HTMLSpanElement | null>>({});
|
||||
|
||||
const { token } = theme.useToken();
|
||||
const typeColors: Record<MemoryAnnotation['type'], string> = {
|
||||
hook: token.colorError,
|
||||
foreshadow: token.colorInfo,
|
||||
plot_point: token.colorSuccess,
|
||||
character_event: token.colorWarning,
|
||||
};
|
||||
|
||||
// 当需要滚动到特定标注时
|
||||
useEffect(() => {
|
||||
if (scrollToAnnotation && annotationRefs.current[scrollToAnnotation]) {
|
||||
@@ -214,7 +215,7 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
|
||||
const { annotation, annotations } = segment;
|
||||
if (!annotation) return null;
|
||||
|
||||
const color = TYPE_COLORS[annotation.type];
|
||||
const color = typeColors[annotation.type];
|
||||
const icon = TYPE_ICONS[annotation.type];
|
||||
const isActive = activeAnnotationId === annotation.id;
|
||||
|
||||
@@ -238,17 +239,17 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
|
||||
position: 'relative',
|
||||
borderBottom: `2px solid ${color}`,
|
||||
cursor: 'pointer',
|
||||
backgroundColor: isActive ? `${color}22` : 'transparent',
|
||||
backgroundColor: isActive ? `color-mix(in srgb, ${color} 13%, transparent)` : 'transparent',
|
||||
transition: 'all 0.2s',
|
||||
padding: '2px 0',
|
||||
}}
|
||||
onClick={() => onAnnotationClick?.(annotation)}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = `${color}33`;
|
||||
e.currentTarget.style.backgroundColor = `color-mix(in srgb, ${color} 20%, transparent)`;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = isActive
|
||||
? `${color}22`
|
||||
? `color-mix(in srgb, ${color} 13%, transparent)`
|
||||
: 'transparent';
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Modal, Button, Space } from 'antd';
|
||||
import { Modal, Button, Space, theme } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface AnnouncementModalProps {
|
||||
@@ -11,6 +11,8 @@ interface AnnouncementModalProps {
|
||||
export default function AnnouncementModal({ visible, onClose, onDoNotShowToday, onNeverShow }: AnnouncementModalProps) {
|
||||
const [qqImageError, setQqImageError] = useState(false);
|
||||
const [wxImageError, setWxImageError] = useState(false);
|
||||
const { token } = theme.useToken();
|
||||
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
@@ -35,7 +37,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
<div style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--color-primary)',
|
||||
color: token.colorPrimary,
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
🎉 欢迎使用 AI小说创作助手
|
||||
@@ -64,9 +66,9 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
borderRadius: '8px',
|
||||
height: '40px',
|
||||
fontSize: '14px',
|
||||
background: 'var(--color-primary)',
|
||||
borderColor: 'var(--color-primary)',
|
||||
boxShadow: 'var(--shadow-primary)',
|
||||
background: token.colorPrimary,
|
||||
borderColor: token.colorPrimary,
|
||||
boxShadow: `0 8px 20px ${alphaColor(token.colorPrimary, 0.32)}`,
|
||||
}}
|
||||
>
|
||||
永不再展示
|
||||
@@ -78,16 +80,16 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
styles={{
|
||||
body: {
|
||||
padding: '20px',
|
||||
background: 'var(--color-bg-container)',
|
||||
background: token.colorBgContainer,
|
||||
},
|
||||
header: {
|
||||
background: 'linear-gradient(135deg, rgba(77, 128, 136, 0.08) 0%, rgba(248, 246, 241, 0.95) 100%)',
|
||||
borderBottom: '1px solid var(--color-border-secondary)',
|
||||
background: `linear-gradient(135deg, ${alphaColor(token.colorPrimary, 0.1)} 0%, ${alphaColor(token.colorBgContainer, 0.98)} 100%)`,
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
padding: '16px 24px',
|
||||
},
|
||||
footer: {
|
||||
background: 'var(--color-bg-container)',
|
||||
borderTop: '1px solid var(--color-border-secondary)',
|
||||
background: token.colorBgContainer,
|
||||
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||||
padding: '16px 24px',
|
||||
},
|
||||
}}
|
||||
@@ -96,7 +98,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
<div style={{
|
||||
marginBottom: '12px',
|
||||
fontSize: '15px',
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
lineHeight: '1.5',
|
||||
}}>
|
||||
<p style={{ marginBottom: '8px' }}>👋 欢迎加入我们的交流群!在这里你可以:</p>
|
||||
@@ -111,7 +113,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
<li>🐛 反馈问题和建议</li>
|
||||
<li>📚 分享创作经验和灵感</li>
|
||||
</ul>
|
||||
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '12px' }}>
|
||||
<p style={{ fontWeight: 600, color: token.colorText, marginBottom: '12px' }}>
|
||||
扫描下方二维码加入交流群:
|
||||
</p>
|
||||
</div>
|
||||
@@ -122,7 +124,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
alignItems: 'flex-start',
|
||||
gap: '24px',
|
||||
padding: '16px',
|
||||
background: 'var(--color-bg-layout)',
|
||||
background: token.colorBgLayout,
|
||||
borderRadius: '8px',
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
@@ -133,7 +135,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
alignItems: 'center',
|
||||
minWidth: '200px',
|
||||
}}>
|
||||
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '8px', fontSize: '14px' }}>
|
||||
<p style={{ fontWeight: 600, color: token.colorText, marginBottom: '8px', fontSize: '14px' }}>
|
||||
QQ交流群
|
||||
</p>
|
||||
{!qqImageError ? (
|
||||
@@ -141,10 +143,10 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'var(--color-bg-container)',
|
||||
background: token.colorBgContainer,
|
||||
borderRadius: '8px',
|
||||
padding: '6px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
boxShadow: `0 2px 8px ${alphaColor(token.colorText, 0.12)}`,
|
||||
}}>
|
||||
<img
|
||||
src="/qq.jpg"
|
||||
@@ -167,9 +169,9 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'var(--color-bg-container)',
|
||||
background: token.colorBgContainer,
|
||||
borderRadius: '8px',
|
||||
color: '#999',
|
||||
color: token.colorTextTertiary,
|
||||
}}>
|
||||
<p>二维码加载失败</p>
|
||||
</div>
|
||||
@@ -183,7 +185,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
alignItems: 'center',
|
||||
minWidth: '200px',
|
||||
}}>
|
||||
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '8px', fontSize: '14px' }}>
|
||||
<p style={{ fontWeight: 600, color: token.colorText, marginBottom: '8px', fontSize: '14px' }}>
|
||||
微信交流群
|
||||
</p>
|
||||
{!wxImageError ? (
|
||||
@@ -191,10 +193,10 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'var(--color-bg-container)',
|
||||
background: token.colorBgContainer,
|
||||
borderRadius: '8px',
|
||||
padding: '6px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
boxShadow: `0 2px 8px ${alphaColor(token.colorText, 0.12)}`,
|
||||
}}>
|
||||
<img
|
||||
src="/WX.png"
|
||||
@@ -217,9 +219,9 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'var(--color-bg-container)',
|
||||
background: token.colorBgContainer,
|
||||
borderRadius: '8px',
|
||||
color: '#999',
|
||||
color: token.colorTextTertiary,
|
||||
}}>
|
||||
<p>二维码加载失败</p>
|
||||
</div>
|
||||
@@ -230,11 +232,11 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
<div style={{
|
||||
marginTop: '16px',
|
||||
padding: '10px',
|
||||
background: 'var(--color-warning-bg)',
|
||||
background: token.colorWarningBg,
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--color-warning-border)',
|
||||
border: `1px solid ${token.colorWarningBorder}`,
|
||||
fontSize: '13px',
|
||||
color: 'var(--color-warning)',
|
||||
color: token.colorWarning,
|
||||
}}>
|
||||
💡 提示:选择"今日内不再展示"当天不再显示,选择"永不再展示"将永久隐藏此公告
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Typography, Space, Divider, Badge, Button, Grid } from 'antd';
|
||||
import { Typography, Space, Divider, Badge, Button, Grid, theme } from 'antd';
|
||||
import { GithubOutlined, CopyrightOutlined, HeartFilled, ClockCircleOutlined, GiftOutlined } from '@ant-design/icons';
|
||||
import { VERSION_INFO, getVersionString } from '../config/version';
|
||||
import { checkLatestVersion } from '../services/versionService';
|
||||
@@ -17,6 +17,8 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
const [hasUpdate, setHasUpdate] = useState(false);
|
||||
const [latestVersion, setLatestVersion] = useState('');
|
||||
const [releaseUrl, setReleaseUrl] = useState('');
|
||||
const { token } = theme.useToken();
|
||||
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||
|
||||
useEffect(() => {
|
||||
// 检查版本更新(每次都重新检查)
|
||||
@@ -55,11 +57,11 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
right: 0,
|
||||
backdropFilter: 'blur(20px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(20px) saturate(180%)',
|
||||
borderTop: '1px solid var(--color-border)',
|
||||
borderTop: `1px solid ${token.colorBorder}`,
|
||||
padding: isMobile ? '8px 12px' : '10px 16px',
|
||||
zIndex: 100,
|
||||
boxShadow: 'var(--shadow-card)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)', // 半透明背景以支持 backdrop-filter
|
||||
boxShadow: `0 -2px 16px ${alphaColor(token.colorText, 0.08)}`,
|
||||
backgroundColor: alphaColor(token.colorBgContainer, 0.82), // 半透明背景以支持 backdrop-filter
|
||||
transition: 'left 0.3s ease', // 平滑过渡
|
||||
}}
|
||||
>
|
||||
@@ -87,23 +89,23 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
color: 'var(--color-primary)',
|
||||
color: token.colorPrimary,
|
||||
cursor: hasUpdate ? 'pointer' : 'default',
|
||||
}}
|
||||
title={hasUpdate ? `发现新版本 v${latestVersion},点击查看` : '当前版本'}
|
||||
>
|
||||
<strong style={{ color: 'var(--color-text-primary)' }}>{VERSION_INFO.projectName}</strong>
|
||||
<strong style={{ color: token.colorText }}>{VERSION_INFO.projectName}</strong>
|
||||
<span>{getVersionString()}</span>
|
||||
</Text>
|
||||
</Badge>
|
||||
<Divider type="vertical" style={{ margin: '0 4px', borderColor: 'var(--color-border)' }} />
|
||||
<Divider type="vertical" style={{ margin: '0 4px', borderColor: token.colorBorder }} />
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<GiftOutlined />}
|
||||
onClick={() => window.open('https://mumuverse.space:1588/', '_blank')}
|
||||
style={{
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
fontSize: 11,
|
||||
height: 24,
|
||||
padding: '0 4px',
|
||||
@@ -114,7 +116,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
>
|
||||
赞助
|
||||
</Button>
|
||||
<Divider type="vertical" style={{ margin: '0 4px', borderColor: 'var(--color-border)' }} />
|
||||
<Divider type="vertical" style={{ margin: '0 4px', borderColor: token.colorBorder }} />
|
||||
<Link
|
||||
href={VERSION_INFO.githubUrl}
|
||||
target="_blank"
|
||||
@@ -124,7 +126,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
}}
|
||||
>
|
||||
<GithubOutlined style={{ fontSize: 12 }} />
|
||||
@@ -132,7 +134,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: 'var(--color-text-tertiary)',
|
||||
color: token.colorTextTertiary,
|
||||
}}
|
||||
>
|
||||
<ClockCircleOutlined style={{ fontSize: 10, marginRight: 4 }} />
|
||||
@@ -144,7 +146,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
<Space
|
||||
direction="horizontal"
|
||||
size={12}
|
||||
split={<Divider type="vertical" style={{ borderColor: 'var(--color-border)' }} />}
|
||||
split={<Divider type="vertical" style={{ borderColor: token.colorBorder }} />}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
@@ -160,7 +162,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
textShadow: 'none',
|
||||
cursor: hasUpdate ? 'pointer' : 'default',
|
||||
transition: 'all 0.3s',
|
||||
@@ -177,7 +179,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
}}
|
||||
title={hasUpdate ? `发现新版本 v${latestVersion},点击查看` : '当前版本'}
|
||||
>
|
||||
<strong style={{ color: 'var(--color-text-primary)' }}>{VERSION_INFO.projectName}</strong>
|
||||
<strong style={{ color: token.colorText }}>{VERSION_INFO.projectName}</strong>
|
||||
<span>{getVersionString()}</span>
|
||||
</Text>
|
||||
</Badge>
|
||||
@@ -192,7 +194,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
}}
|
||||
>
|
||||
<GithubOutlined style={{ fontSize: 13 }} />
|
||||
@@ -206,7 +208,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
}}
|
||||
>
|
||||
LinuxDO 社区
|
||||
@@ -218,9 +220,9 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
icon={<GiftOutlined style={{ fontSize: 14 }} />}
|
||||
onClick={() => window.open('https://mumuverse.space:1588/', '_blank')}
|
||||
style={{
|
||||
background: 'var(--color-primary)',
|
||||
background: token.colorPrimary,
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 12px rgba(77, 128, 136, 0.3)',
|
||||
boxShadow: `0 4px 12px ${alphaColor(token.colorPrimary, 0.35)}`,
|
||||
fontSize: 13,
|
||||
height: 32,
|
||||
padding: '0 20px',
|
||||
@@ -232,11 +234,11 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
e.currentTarget.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.6)';
|
||||
e.currentTarget.style.boxShadow = `0 6px 16px ${alphaColor(token.colorPrimary, 0.5)}`;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.5)';
|
||||
e.currentTarget.style.boxShadow = `0 4px 12px ${alphaColor(token.colorPrimary, 0.35)}`;
|
||||
}}
|
||||
>
|
||||
赞助支持
|
||||
@@ -252,7 +254,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
}}
|
||||
>
|
||||
<CopyrightOutlined style={{ fontSize: 11 }} />
|
||||
@@ -266,7 +268,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
color: 'var(--color-text-tertiary)',
|
||||
color: token.colorTextTertiary,
|
||||
}}
|
||||
>
|
||||
<ClockCircleOutlined style={{ fontSize: 12 }} />
|
||||
@@ -280,12 +282,12 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
color: 'var(--color-text-secondary)',
|
||||
textShadow: '0 1px 3px rgba(0, 0, 0, 0.05)',
|
||||
color: token.colorTextSecondary,
|
||||
textShadow: `0 1px 3px ${alphaColor(token.colorText, 0.08)}`,
|
||||
}}
|
||||
>
|
||||
<span>Made with</span>
|
||||
<HeartFilled style={{ color: 'var(--color-error)', fontSize: 11 }} />
|
||||
<HeartFilled style={{ color: token.colorError, fontSize: 11 }} />
|
||||
<span>by {VERSION_INFO.author}</span>
|
||||
</Text>
|
||||
</Space>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Modal, Spin, Alert, Tabs, Card, Tag, List, Empty, Statistic, Row, Col, Button } from 'antd';
|
||||
import { Modal, Spin, Alert, Tabs, Card, Tag, List, Empty, Statistic, Row, Col, Button, theme } from 'antd';
|
||||
import {
|
||||
ThunderboltOutlined,
|
||||
BulbOutlined,
|
||||
@@ -27,6 +27,7 @@ interface ChapterAnalysisProps {
|
||||
}
|
||||
|
||||
export default function ChapterAnalysis({ chapterId, visible, onClose }: ChapterAnalysisProps) {
|
||||
const { token } = theme.useToken();
|
||||
const [task, setTask] = useState<AnalysisTask | null>(null);
|
||||
const [analysis, setAnalysis] = useState<ChapterAnalysisResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -258,7 +259,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
transition: 'all 0.3s ease',
|
||||
borderRadius: 6,
|
||||
boxShadow: task.progress > 0 && task.status !== 'failed'
|
||||
? '0 0 10px rgba(24, 144, 255, 0.3)'
|
||||
? `0 0 10px color-mix(in srgb, ${token.colorPrimary} 30%, transparent)`
|
||||
: 'none'
|
||||
}} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, Button, Card, Statistic, Row, Col, message } from 'antd';
|
||||
import { Modal, Button, Card, Statistic, Row, Col, message, theme } from 'antd';
|
||||
import { CheckOutlined, CloseOutlined, SwapOutlined } from '@ant-design/icons';
|
||||
import ReactDiffViewer from 'react-diff-viewer-continued';
|
||||
|
||||
@@ -26,6 +26,7 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
|
||||
onApply,
|
||||
onDiscard
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
const [applying, setApplying] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<'split' | 'unified'>('split');
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
@@ -195,7 +196,7 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
|
||||
styles={{
|
||||
variables: {
|
||||
light: {
|
||||
diffViewerBackground: '#fff', // Keep white for diff viewer readability
|
||||
diffViewerBackground: token.colorBgContainer,
|
||||
addedBackground: 'var(--color-success-bg)',
|
||||
addedColor: 'var(--color-text-primary)',
|
||||
removedBackground: 'var(--color-error-bg)',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 提供沉浸式阅读体验,支持主题切换、字体调节、翻页导航等功能
|
||||
*/
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Modal, Button, Slider, Radio, Space, Typography, Spin, message } from 'antd';
|
||||
import { Modal, Button, Slider, Radio, Space, Typography, Spin, message, theme } from 'antd';
|
||||
import {
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
@@ -37,27 +37,12 @@ interface NavigationInfo {
|
||||
current: { id: string; chapter_number: number; title: string };
|
||||
}
|
||||
|
||||
// 主题样式配置
|
||||
const themeStyles = {
|
||||
light: {
|
||||
bg: '#ffffff',
|
||||
text: '#333333',
|
||||
headerBg: '#fafafa',
|
||||
border: '#e8e8e8'
|
||||
},
|
||||
sepia: {
|
||||
bg: '#f5e6c8',
|
||||
text: '#5b4636',
|
||||
headerBg: '#e8d9b8',
|
||||
border: '#d4c5a5'
|
||||
},
|
||||
dark: {
|
||||
bg: '#1a1a1a',
|
||||
text: '#cccccc',
|
||||
headerBg: '#252525',
|
||||
border: '#333333'
|
||||
}
|
||||
};
|
||||
interface ReaderThemeStyle {
|
||||
bg: string;
|
||||
text: string;
|
||||
headerBg: string;
|
||||
border: string;
|
||||
}
|
||||
|
||||
// 本地存储key
|
||||
const SETTINGS_STORAGE_KEY = 'chapter-reader-settings';
|
||||
@@ -94,6 +79,8 @@ export default function ChapterReader({
|
||||
onClose,
|
||||
onChapterChange
|
||||
}: ChapterReaderProps) {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
// 阅读器设置
|
||||
const [settings, setSettings] = useState<ReaderSettings>(loadSettings);
|
||||
|
||||
@@ -200,6 +187,26 @@ export default function ChapterReader({
|
||||
}, [chapter?.id]);
|
||||
|
||||
// 当前主题样式
|
||||
const themeStyles: Record<ReaderSettings['theme'], ReaderThemeStyle> = {
|
||||
light: {
|
||||
bg: token.colorBgContainer,
|
||||
text: token.colorText,
|
||||
headerBg: token.colorBgElevated,
|
||||
border: token.colorBorderSecondary,
|
||||
},
|
||||
sepia: {
|
||||
bg: `color-mix(in srgb, ${token.colorWarningBg} 72%, ${token.colorBgContainer} 28%)`,
|
||||
text: `color-mix(in srgb, ${token.colorText} 85%, ${token.colorTextSecondary} 15%)`,
|
||||
headerBg: `color-mix(in srgb, ${token.colorWarningBg} 58%, ${token.colorBgElevated} 42%)`,
|
||||
border: `color-mix(in srgb, ${token.colorWarningBorder} 65%, ${token.colorBorder} 35%)`,
|
||||
},
|
||||
dark: {
|
||||
bg: `color-mix(in srgb, ${token.colorTextBase} 92%, ${token.colorBgContainer} 8%)`,
|
||||
text: `color-mix(in srgb, ${token.colorTextLightSolid} 82%, ${token.colorTextSecondary} 18%)`,
|
||||
headerBg: `color-mix(in srgb, ${token.colorTextBase} 84%, ${token.colorBgElevated} 16%)`,
|
||||
border: `color-mix(in srgb, ${token.colorTextBase} 60%, ${token.colorBorder} 40%)`,
|
||||
},
|
||||
};
|
||||
const currentTheme = themeStyles[settings.theme];
|
||||
|
||||
// 更新设置的便捷函数
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Card, Space, Tag, Typography, Popconfirm } from 'antd';
|
||||
import { Card, Space, Tag, Typography, Popconfirm, theme } from 'antd';
|
||||
import { EditOutlined, DeleteOutlined, UserOutlined, BankOutlined, ExportOutlined } from '@ant-design/icons';
|
||||
import { cardStyles } from './CardStyles';
|
||||
import { characterCardStyles } from './CardStyles';
|
||||
import type { Character } from '../types';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
@@ -13,6 +13,8 @@ interface CharacterCardProps {
|
||||
}
|
||||
|
||||
export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit, onDelete, onExport }) => {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const getRoleTypeColor = (roleType?: string) => {
|
||||
const roleColors: Record<string, string> = {
|
||||
'protagonist': 'blue',
|
||||
@@ -37,10 +39,10 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
|
||||
|
||||
const getStatusTag = () => {
|
||||
const statusConfig: Record<string, { color: string; label: string }> = {
|
||||
deceased: { color: '#000000', label: '💀 已死亡' },
|
||||
missing: { color: '#faad14', label: '❓ 已失踪' },
|
||||
retired: { color: '#8c8c8c', label: '📤 已退场' },
|
||||
destroyed: { color: '#000000', label: '💀 已覆灭' },
|
||||
deceased: { color: token.colorTextBase, label: '💀 已死亡' },
|
||||
missing: { color: token.colorWarning, label: '❓ 已失踪' },
|
||||
retired: { color: token.colorTextTertiary, label: '📤 已退场' },
|
||||
destroyed: { color: token.colorTextBase, label: '💀 已覆灭' },
|
||||
};
|
||||
const config = statusConfig[charStatus];
|
||||
if (!config) return null;
|
||||
@@ -51,7 +53,7 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
|
||||
<Card
|
||||
hoverable
|
||||
style={{
|
||||
...(isOrganization ? cardStyles.organization : cardStyles.character),
|
||||
...(isOrganization ? characterCardStyles.organizationCard : characterCardStyles.characterCard),
|
||||
...(isInactive ? { opacity: 0.6, filter: 'grayscale(40%)' } : {}),
|
||||
}}
|
||||
styles={{
|
||||
@@ -82,14 +84,14 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
|
||||
<Card.Meta
|
||||
avatar={
|
||||
isOrganization ? (
|
||||
<BankOutlined style={{ fontSize: 32, color: '#52c41a' }} />
|
||||
<BankOutlined style={{ fontSize: 32, color: token.colorSuccess }} />
|
||||
) : (
|
||||
<UserOutlined style={{ fontSize: 32, color: '#1890ff' }} />
|
||||
<UserOutlined style={{ fontSize: 32, color: token.colorPrimary }} />
|
||||
)
|
||||
}
|
||||
title={
|
||||
<Space>
|
||||
<span style={cardStyles.ellipsis}>{character.name}</span>
|
||||
<span style={characterCardStyles.nameEllipsis}>{character.name}</span>
|
||||
{isOrganization ? (
|
||||
<Tag color="green">组织</Tag>
|
||||
) : (
|
||||
@@ -103,7 +105,7 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<div style={cardStyles.description}>
|
||||
<div style={characterCardStyles.descriptionBlock}>
|
||||
{/* 角色特有字段 */}
|
||||
{!isOrganization && (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, Button, Modal, Form, Select, InputNumber, Input, message, Progress, Tag, Space, Divider, Typography } from 'antd';
|
||||
import { Card, Button, Modal, Form, Select, InputNumber, Input, message, Progress, Tag, Space, Divider, Typography, theme } from 'antd';
|
||||
import { EditOutlined, PlusOutlined, DeleteOutlined, TrophyOutlined } from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
|
||||
@@ -44,6 +44,7 @@ export const CharacterCareerCard: React.FC<Props> = ({
|
||||
editable = false,
|
||||
onUpdate
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
const [mainCareer, setMainCareer] = useState<CareerDetail | null>(null);
|
||||
const [subCareers, setSubCareers] = useState<CareerDetail[]>([]);
|
||||
const [allCareers, setAllCareers] = useState<Career[]>([]);
|
||||
@@ -190,7 +191,7 @@ export const CharacterCareerCard: React.FC<Props> = ({
|
||||
<div key={career.id} style={{ marginBottom: 16 }}>
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<Space>
|
||||
<TrophyOutlined style={{ color: isMain ? '#1890ff' : '#8c8c8c' }} />
|
||||
<TrophyOutlined style={{ color: isMain ? token.colorPrimary : token.colorTextTertiary }} />
|
||||
<Text strong={isMain}>{career.career_name}</Text>
|
||||
{isMain && <Tag color="blue">主</Tag>}
|
||||
</Space>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Drawer, Input, List, Typography, Empty, Tag } from 'antd';
|
||||
import { Drawer, Input, List, Typography, Empty, Tag, theme } from 'antd';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import type { Chapter } from '../types';
|
||||
|
||||
@@ -24,6 +24,7 @@ export default function FloatingIndexPanel({
|
||||
groupedChapters,
|
||||
onChapterSelect,
|
||||
}: FloatingIndexPanelProps) {
|
||||
const { token } = theme.useToken();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const filteredGroups = useMemo(() => {
|
||||
@@ -56,7 +57,7 @@ export default function FloatingIndexPanel({
|
||||
body: { padding: 0 },
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '16px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ padding: '16px', borderBottom: `1px solid ${token.colorBorderSecondary}` }}>
|
||||
<Input
|
||||
placeholder="搜索章节标题"
|
||||
prefix={<SearchOutlined />}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo, useEffect, useRef } from 'react';
|
||||
import { Card, Tag, Badge, Empty, Collapse, Divider } from 'antd';
|
||||
import { Card, Tag, Badge, Empty, Collapse, Divider, theme } from 'antd';
|
||||
import {
|
||||
FireOutlined,
|
||||
StarOutlined,
|
||||
@@ -22,22 +22,18 @@ const TYPE_CONFIG = {
|
||||
hook: {
|
||||
label: '钩子',
|
||||
icon: <FireOutlined />,
|
||||
color: '#ff6b6b',
|
||||
},
|
||||
foreshadow: {
|
||||
label: '伏笔',
|
||||
icon: <StarOutlined />,
|
||||
color: '#6b7bff',
|
||||
},
|
||||
plot_point: {
|
||||
label: '情节点',
|
||||
icon: <ThunderboltOutlined />,
|
||||
color: '#51cf66',
|
||||
},
|
||||
character_event: {
|
||||
label: '角色事件',
|
||||
icon: <UserOutlined />,
|
||||
color: '#ffd93d',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -51,7 +47,14 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
||||
onAnnotationClick,
|
||||
scrollToAnnotation,
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
const cardRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
const typeColors: Record<keyof typeof TYPE_CONFIG, string> = {
|
||||
hook: token.colorError,
|
||||
foreshadow: token.colorInfo,
|
||||
plot_point: token.colorSuccess,
|
||||
character_event: token.colorWarning,
|
||||
};
|
||||
|
||||
// 当需要滚动到特定标注卡片时
|
||||
useEffect(() => {
|
||||
@@ -100,6 +103,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
||||
// 渲染单个记忆卡片
|
||||
const renderMemoryCard = (annotation: MemoryAnnotation) => {
|
||||
const config = TYPE_CONFIG[annotation.type];
|
||||
const color = typeColors[annotation.type];
|
||||
const isActive = activeAnnotationId === annotation.id;
|
||||
|
||||
return (
|
||||
@@ -115,8 +119,8 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
||||
onClick={() => onAnnotationClick?.(annotation)}
|
||||
style={{
|
||||
marginBottom: 12,
|
||||
borderLeft: `4px solid ${config.color}`,
|
||||
backgroundColor: isActive ? `${config.color}11` : 'transparent',
|
||||
borderLeft: `4px solid ${color}`,
|
||||
backgroundColor: isActive ? `color-mix(in srgb, ${color} 8%, transparent)` : 'transparent',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
@@ -126,7 +130,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
||||
<Badge
|
||||
count={`${(annotation.importance * 10).toFixed(1)}`}
|
||||
style={{
|
||||
backgroundColor: config.color,
|
||||
backgroundColor: color,
|
||||
float: 'right',
|
||||
}}
|
||||
/>
|
||||
@@ -138,7 +142,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: '#666',
|
||||
color: token.colorTextSecondary,
|
||||
lineHeight: 1.6,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
@@ -160,7 +164,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
||||
|
||||
{/* 特殊元数据 */}
|
||||
{annotation.metadata.strength && (
|
||||
<div style={{ marginTop: 4, fontSize: 11, color: '#999' }}>
|
||||
<div style={{ marginTop: 4, fontSize: 11, color: token.colorTextTertiary }}>
|
||||
强度: {annotation.metadata.strength}/10
|
||||
</div>
|
||||
)}
|
||||
@@ -192,27 +196,27 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
||||
<div style={{ fontWeight: 600, marginBottom: 12 }}>📊 分析概览</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>钩子</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600, color: TYPE_CONFIG.hook.color }}>
|
||||
<div style={{ fontSize: 12, color: token.colorTextTertiary }}>钩子</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600, color: typeColors.hook }}>
|
||||
{stats.hooks}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>伏笔</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600, color: TYPE_CONFIG.foreshadow.color }}>
|
||||
<div style={{ fontSize: 12, color: token.colorTextTertiary }}>伏笔</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600, color: typeColors.foreshadow }}>
|
||||
{stats.foreshadows}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>情节点</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600, color: TYPE_CONFIG.plot_point.color }}>
|
||||
<div style={{ fontSize: 12, color: token.colorTextTertiary }}>情节点</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600, color: typeColors.plot_point }}>
|
||||
{stats.plotPoints}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>角色事件</div>
|
||||
<div style={{ fontSize: 12, color: token.colorTextTertiary }}>角色事件</div>
|
||||
<div
|
||||
style={{ fontSize: 20, fontWeight: 600, color: TYPE_CONFIG.character_event.color }}
|
||||
style={{ fontSize: 20, fontWeight: 600, color: typeColors.character_event }}
|
||||
>
|
||||
{stats.characterEvents}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Modal, Input, Button, Space, Radio, InputNumber, Card, message, Alert, Spin, Typography, Divider } from 'antd';
|
||||
import { Modal, Input, Button, Space, Radio, InputNumber, Card, message, Alert, Spin, Typography, Divider, theme } from 'antd';
|
||||
import { ThunderboltOutlined, CheckOutlined, ReloadOutlined, EditOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import { chapterApi } from '../services/api';
|
||||
|
||||
@@ -33,6 +33,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
onClose,
|
||||
onApply,
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
const [userInstructions, setUserInstructions] = useState('');
|
||||
const [lengthMode, setLengthMode] = useState<LengthMode>('similar');
|
||||
const [customWordCount, setCustomWordCount] = useState<number>(selectedText.length);
|
||||
@@ -178,7 +179,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<EditOutlined style={{ color: 'var(--color-primary)' }} />
|
||||
<EditOutlined style={{ color: token.colorPrimary }} />
|
||||
<span>AI局部重写</span>
|
||||
</Space>
|
||||
}
|
||||
@@ -202,9 +203,9 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
loading={isGenerating}
|
||||
disabled={!userInstructions.trim()}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
||||
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 12px rgba(77, 128, 136, 0.3)',
|
||||
boxShadow: token.boxShadowSecondary,
|
||||
}}
|
||||
>
|
||||
{isGenerating ? '生成中...' : '开始重写'}
|
||||
@@ -221,7 +222,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
type="primary"
|
||||
icon={<CheckOutlined />}
|
||||
onClick={handleAccept}
|
||||
style={{ background: '#52c41a', borderColor: '#52c41a' }}
|
||||
style={{ background: token.colorSuccess, borderColor: token.colorSuccess }}
|
||||
>
|
||||
接受并应用
|
||||
</Button>
|
||||
@@ -250,7 +251,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
body: {
|
||||
maxHeight: 150,
|
||||
overflowY: 'auto',
|
||||
background: '#fafafa',
|
||||
background: token.colorFillAlter,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -258,7 +259,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
style={{
|
||||
margin: 0,
|
||||
whiteSpace: 'pre-wrap',
|
||||
color: '#595959',
|
||||
color: token.colorText,
|
||||
lineHeight: 1.8,
|
||||
}}
|
||||
>
|
||||
@@ -352,7 +353,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
<div
|
||||
style={{
|
||||
height: 4,
|
||||
background: '#f0f0f0',
|
||||
background: token.colorFillTertiary,
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
@@ -360,7 +361,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
||||
background: `linear-gradient(90deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
|
||||
width: `${progress}%`,
|
||||
transition: 'width 0.3s ease',
|
||||
borderRadius: 2,
|
||||
@@ -374,8 +375,8 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
size="small"
|
||||
ref={generatedTextRef}
|
||||
style={{
|
||||
background: generatedText ? '#f6ffed' : '#fafafa',
|
||||
border: generatedText ? '1px solid #b7eb8f' : '1px solid #d9d9d9',
|
||||
background: generatedText ? token.colorSuccessBg : token.colorFillAlter,
|
||||
border: generatedText ? `1px solid ${token.colorSuccessBorder}` : `1px solid ${token.colorBorder}`,
|
||||
}}
|
||||
styles={{
|
||||
body: {
|
||||
@@ -400,7 +401,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
display: 'inline-block',
|
||||
width: 8,
|
||||
height: 16,
|
||||
background: 'var(--color-primary)',
|
||||
background: token.colorPrimary,
|
||||
marginLeft: 2,
|
||||
animation: 'blink 1s infinite',
|
||||
}}
|
||||
@@ -408,7 +409,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
)}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: 20, color: '#8c8c8c' }}>
|
||||
<div style={{ textAlign: 'center', padding: 20, color: token.colorTextTertiary }}>
|
||||
{isGenerating ? '正在生成内容...' : '等待生成...'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { Button, Tooltip, theme } from 'antd';
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
|
||||
interface PartialRegenerateToolbarProps {
|
||||
@@ -19,6 +19,8 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
|
||||
onRegenerate,
|
||||
selectedText
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
if (!visible || !selectedText) return null;
|
||||
|
||||
// 限制显示的选中文本长度
|
||||
@@ -33,15 +35,15 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
zIndex: 10000,
|
||||
background: '#fff',
|
||||
background: token.colorBgElevated,
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.15)',
|
||||
boxShadow: token.boxShadow,
|
||||
padding: '6px 8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
animation: 'fadeIn 0.2s ease-out',
|
||||
border: '1px solid #e8e8e8',
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
@@ -61,7 +63,7 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
|
||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
||||
border: 'none',
|
||||
fontWeight: 500,
|
||||
boxShadow: '0 4px 12px rgba(77, 128, 136, 0.3)',
|
||||
boxShadow: token.boxShadowSecondary,
|
||||
}}
|
||||
>
|
||||
AI重写
|
||||
@@ -69,7 +71,7 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
|
||||
</Tooltip>
|
||||
<span style={{
|
||||
fontSize: 12,
|
||||
color: '#8c8c8c',
|
||||
color: token.colorTextTertiary,
|
||||
maxWidth: 150,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Spin } from 'antd';
|
||||
import { Spin, theme } from 'antd';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
|
||||
interface SSELoadingOverlayProps {
|
||||
@@ -13,6 +13,8 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
progress,
|
||||
message
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
if (!loading) return null;
|
||||
|
||||
return (
|
||||
@@ -22,19 +24,19 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.45)',
|
||||
background: token.colorBgMask,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 9999
|
||||
}}>
|
||||
<div style={{
|
||||
background: '#fff',
|
||||
background: token.colorBgElevated,
|
||||
borderRadius: 12,
|
||||
padding: '40px 60px',
|
||||
minWidth: 400,
|
||||
maxWidth: 600,
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.2)'
|
||||
boxShadow: token.boxShadowSecondary
|
||||
}}>
|
||||
{/* 标题和图标 */}
|
||||
<div style={{
|
||||
@@ -42,13 +44,13 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
marginBottom: 24
|
||||
}}>
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 48, color: 'var(--color-primary)' }} spin />}
|
||||
indicator={<LoadingOutlined style={{ fontSize: 48, color: token.colorPrimary }} spin />}
|
||||
/>
|
||||
<div style={{
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
marginTop: 16,
|
||||
color: 'var(--color-text-primary)'
|
||||
color: token.colorTextHeading
|
||||
}}>
|
||||
AI生成中...
|
||||
</div>
|
||||
@@ -60,7 +62,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
}}>
|
||||
<div style={{
|
||||
height: 12,
|
||||
background: 'var(--color-bg-layout)',
|
||||
background: token.colorFillTertiary,
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
marginBottom: 12
|
||||
@@ -68,12 +70,12 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
<div style={{
|
||||
height: '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%)',
|
||||
? `linear-gradient(90deg, ${token.colorSuccess} 0%, ${token.colorSuccessActive} 100%)`
|
||||
: `linear-gradient(90deg, ${token.colorPrimary} 0%, ${token.colorPrimaryActive} 100%)`,
|
||||
width: `${progress}%`,
|
||||
transition: 'all 0.3s ease',
|
||||
borderRadius: 6,
|
||||
boxShadow: progress > 0 ? 'var(--shadow-card)' : 'none'
|
||||
boxShadow: progress > 0 ? token.boxShadow : 'none'
|
||||
}} />
|
||||
</div>
|
||||
|
||||
@@ -82,7 +84,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
textAlign: 'center',
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
color: progress === 100 ? 'var(--color-success)' : 'var(--color-primary)',
|
||||
color: progress === 100 ? token.colorSuccess : token.colorPrimary,
|
||||
marginBottom: 8
|
||||
}}>
|
||||
{progress}%
|
||||
@@ -93,7 +95,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
color: '#595959',
|
||||
color: token.colorText,
|
||||
minHeight: 24,
|
||||
padding: '0 20px'
|
||||
}}>
|
||||
@@ -104,7 +106,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
fontSize: 13,
|
||||
color: '#8c8c8c',
|
||||
color: token.colorTextTertiary,
|
||||
marginTop: 16
|
||||
}}>
|
||||
请勿关闭页面,生成过程需要一定时间
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { theme } from 'antd';
|
||||
|
||||
interface SSEProgressBarProps {
|
||||
loading: boolean;
|
||||
@@ -11,6 +12,8 @@ export const SSEProgressBar: React.FC<SSEProgressBarProps> = ({
|
||||
progress,
|
||||
message
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
if (!loading) return null;
|
||||
|
||||
return (
|
||||
@@ -18,14 +21,14 @@ export const SSEProgressBar: React.FC<SSEProgressBarProps> = ({
|
||||
{/* 进度条 */}
|
||||
<div style={{
|
||||
height: 8,
|
||||
background: '#f0f0f0',
|
||||
background: token.colorFillTertiary,
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
marginBottom: 8
|
||||
}}>
|
||||
<div style={{
|
||||
height: '100%',
|
||||
background: progress === 100 ? '#52c41a' : '#1890ff',
|
||||
background: progress === 100 ? token.colorSuccess : token.colorPrimary,
|
||||
width: `${progress}%`,
|
||||
transition: 'all 0.3s ease',
|
||||
borderRadius: 4
|
||||
@@ -39,12 +42,12 @@ export const SSEProgressBar: React.FC<SSEProgressBarProps> = ({
|
||||
alignItems: 'center',
|
||||
fontSize: 14
|
||||
}}>
|
||||
<span style={{ color: '#666' }}>
|
||||
<span style={{ color: token.colorTextSecondary }}>
|
||||
{message || '准备生成...'}
|
||||
</span>
|
||||
<span style={{
|
||||
fontWeight: 'bold',
|
||||
color: progress === 100 ? '#52c41a' : '#1890ff'
|
||||
color: progress === 100 ? token.colorSuccess : token.colorPrimary
|
||||
}}>
|
||||
{progress}%
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Modal, Spin, Button } from 'antd';
|
||||
import { Modal, Spin, Button, theme } from 'antd';
|
||||
import { LoadingOutlined, StopOutlined } from '@ant-design/icons';
|
||||
|
||||
interface SSEProgressModalProps {
|
||||
@@ -27,6 +27,8 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
onCancel,
|
||||
cancelButtonText = '取消任务',
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
@@ -53,13 +55,13 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
marginBottom: 24
|
||||
}}>
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 48, color: 'var(--color-primary)' }} spin />}
|
||||
indicator={<LoadingOutlined style={{ fontSize: 48, color: token.colorPrimary }} spin />}
|
||||
/>
|
||||
<div style={{
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
marginTop: 16,
|
||||
color: 'var(--color-text-primary)'
|
||||
color: token.colorText
|
||||
}}>
|
||||
{title}
|
||||
</div>
|
||||
@@ -72,7 +74,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
}}>
|
||||
<div style={{
|
||||
height: 12,
|
||||
background: 'var(--color-bg-layout)',
|
||||
background: token.colorBgLayout,
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
marginBottom: showPercentage ? 12 : 0
|
||||
@@ -80,12 +82,12 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
<div style={{
|
||||
height: '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%)',
|
||||
? `linear-gradient(90deg, ${token.colorSuccess} 0%, ${token.colorSuccess} 100%)`
|
||||
: `linear-gradient(90deg, ${token.colorPrimary} 0%, ${token.colorPrimary} 100%)`,
|
||||
width: `${progress}%`,
|
||||
transition: 'all 0.3s ease',
|
||||
borderRadius: 6,
|
||||
boxShadow: progress > 0 ? 'var(--shadow-card)' : 'none'
|
||||
boxShadow: progress > 0 ? token.boxShadow : 'none'
|
||||
}} />
|
||||
</div>
|
||||
|
||||
@@ -95,7 +97,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
textAlign: 'center',
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
color: progress === 100 ? 'var(--color-success)' : 'var(--color-primary)',
|
||||
color: progress === 100 ? token.colorSuccess : token.colorPrimary,
|
||||
marginBottom: 8
|
||||
}}>
|
||||
{progress}%
|
||||
@@ -107,7 +109,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
minHeight: 24,
|
||||
padding: '0 20px',
|
||||
marginBottom: 16
|
||||
@@ -119,7 +121,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
fontSize: 13,
|
||||
color: 'var(--color-text-tertiary)',
|
||||
color: token.colorTextTertiary,
|
||||
marginBottom: onCancel ? 16 : 0
|
||||
}}>
|
||||
请勿关闭页面,生成过程需要一定时间
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Dropdown, Avatar, Space, Typography, message, Modal, Form, Input, Button } from 'antd';
|
||||
import { Dropdown, Avatar, Space, Typography, message, Modal, Form, Input, Button, theme } from 'antd';
|
||||
import { UserOutlined, LogoutOutlined, TeamOutlined, CrownOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import { authApi } from '../services/api';
|
||||
import type { User } from '../types';
|
||||
@@ -11,14 +11,18 @@ const { Text } = Typography;
|
||||
interface UserMenuProps {
|
||||
/** 是否总是显示完整信息(用于移动端侧边栏) */
|
||||
showFullInfo?: boolean;
|
||||
/** 紧凑模式(用于折叠侧边栏,仅展示头像) */
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export default function UserMenu({ showFullInfo = false }: UserMenuProps) {
|
||||
export default function UserMenu({ showFullInfo = false, compact = false }: UserMenuProps) {
|
||||
const navigate = useNavigate();
|
||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||
const [showChangePassword, setShowChangePassword] = useState(false);
|
||||
const [changePasswordForm] = Form.useForm();
|
||||
const [changingPassword, setChangingPassword] = useState(false);
|
||||
const { token } = theme.useToken();
|
||||
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||
|
||||
useEffect(() => {
|
||||
loadCurrentUser();
|
||||
@@ -126,36 +130,36 @@ export default function UserMenu({ showFullInfo = false }: UserMenuProps) {
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: '8px 16px',
|
||||
background: 'rgba(255, 255, 255, 0.6)', // 保持半透明以配合 Backdrop
|
||||
gap: compact ? 0 : 12,
|
||||
padding: compact ? '4px' : '8px 16px',
|
||||
background: alphaColor(token.colorBgContainer, 0.65), // 保持半透明以配合 Backdrop
|
||||
backdropFilter: 'blur(10px)',
|
||||
WebkitBackdropFilter: 'blur(10px)',
|
||||
borderRadius: 24,
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: compact ? 16 : 24,
|
||||
border: `1px solid ${token.colorBorder}`,
|
||||
transition: 'all 0.3s ease',
|
||||
boxShadow: 'var(--shadow-card)',
|
||||
boxShadow: `0 8px 20px ${alphaColor(token.colorText, 0.08)}`,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'var(--color-bg-container)'; // 悬浮时变实
|
||||
e.currentTarget.style.background = token.colorBgContainer; // 悬浮时变实
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
e.currentTarget.style.boxShadow = 'var(--shadow-elevated)';
|
||||
e.currentTarget.style.boxShadow = `0 12px 28px ${alphaColor(token.colorText, 0.14)}`;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.6)';
|
||||
e.currentTarget.style.background = alphaColor(token.colorBgContainer, 0.65);
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = 'var(--shadow-card)';
|
||||
e.currentTarget.style.boxShadow = `0 8px 20px ${alphaColor(token.colorText, 0.08)}`;
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Avatar
|
||||
src={currentUser.avatar_url}
|
||||
icon={<UserOutlined />}
|
||||
size={40}
|
||||
size={compact ? 32 : 40}
|
||||
style={{
|
||||
backgroundColor: 'var(--color-primary)',
|
||||
border: '3px solid #fff',
|
||||
boxShadow: 'var(--shadow-card)',
|
||||
backgroundColor: token.colorPrimary,
|
||||
border: `3px solid ${token.colorWhite}`,
|
||||
boxShadow: `0 8px 20px ${alphaColor(token.colorText, 0.12)}`,
|
||||
}}
|
||||
/>
|
||||
{currentUser.is_admin && (
|
||||
@@ -165,28 +169,28 @@ export default function UserMenu({ showFullInfo = false }: UserMenuProps) {
|
||||
right: -2,
|
||||
width: 18,
|
||||
height: 18,
|
||||
background: 'linear-gradient(135deg, #ffd700 0%, #ffaa00 100%)',
|
||||
background: `linear-gradient(135deg, ${token.colorWarning} 0%, ${token.colorWarningHover} 100%)`,
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '2px solid white',
|
||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
|
||||
border: `2px solid ${token.colorWhite}`,
|
||||
boxShadow: `0 2px 4px ${alphaColor(token.colorText, 0.2)}`,
|
||||
}}>
|
||||
<CrownOutlined style={{ fontSize: 9, color: '#fff' }} />
|
||||
<CrownOutlined style={{ fontSize: 9, color: token.colorWhite }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Space direction="vertical" size={0} style={{ display: (window.innerWidth <= 768 && !showFullInfo) ? 'none' : 'flex' }}>
|
||||
<Space direction="vertical" size={0} style={{ display: compact ? 'none' : ((window.innerWidth <= 768 && !showFullInfo) ? 'none' : 'flex') }}>
|
||||
<Text strong style={{
|
||||
color: 'var(--color-text-primary)',
|
||||
color: token.colorText,
|
||||
fontSize: 14,
|
||||
lineHeight: '20px',
|
||||
}}>
|
||||
{currentUser.display_name || currentUser.username}
|
||||
</Text>
|
||||
<Text style={{
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
fontSize: 12,
|
||||
lineHeight: '18px',
|
||||
}}>
|
||||
|
||||
+141
-330
@@ -1,70 +1,17 @@
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* --- 中国风配色方案 (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;
|
||||
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%;
|
||||
}
|
||||
@@ -72,43 +19,123 @@
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
/* 禁止移动端双击缩放 */
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
/* 自定义滚动条样式(与主题弱耦合) */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
border-radius: 4px;
|
||||
transition: background 0.3s ease;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
background: rgba(0, 0, 0, 0.34);
|
||||
}
|
||||
|
||||
/* Firefox 滚动条样式 */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #c1c1c1 #f1f1f1;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.24) rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
[data-theme-resolved='dark'] ::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
[data-theme-resolved='dark'] ::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
|
||||
[data-theme-resolved='dark'] ::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
[data-theme-resolved='dark'] * {
|
||||
scrollbar-color: rgba(255, 255, 255, 0.28) rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* 主题切换转场(暗色扩散、亮色收缩) */
|
||||
:root {
|
||||
--theme-transition-duration: 460ms;
|
||||
--theme-transition-easing: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
:root::view-transition-group(root) {
|
||||
animation-duration: var(--theme-transition-duration);
|
||||
}
|
||||
|
||||
:root[data-theme-transition='to-dark']::view-transition-new(root) {
|
||||
z-index: 2;
|
||||
animation: theme-dark-reveal var(--theme-transition-duration) var(--theme-transition-easing) both;
|
||||
}
|
||||
|
||||
:root[data-theme-transition='to-dark']::view-transition-old(root) {
|
||||
z-index: 1;
|
||||
animation: theme-old-fade var(--theme-transition-duration) ease both;
|
||||
}
|
||||
|
||||
:root[data-theme-transition='to-light']::view-transition-old(root) {
|
||||
z-index: 2;
|
||||
animation: theme-light-collapse var(--theme-transition-duration) var(--theme-transition-easing) both;
|
||||
}
|
||||
|
||||
:root[data-theme-transition='to-light']::view-transition-new(root) {
|
||||
z-index: 1;
|
||||
animation: theme-new-fade var(--theme-transition-duration) ease both;
|
||||
}
|
||||
|
||||
@keyframes theme-dark-reveal {
|
||||
from {
|
||||
clip-path: circle(0% at 50% 50%);
|
||||
opacity: 0.78;
|
||||
}
|
||||
to {
|
||||
clip-path: circle(150% at 50% 50%);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes theme-light-collapse {
|
||||
from {
|
||||
clip-path: circle(150% at 50% 50%);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
clip-path: circle(0% at 50% 50%);
|
||||
opacity: 0.82;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes theme-old-fade {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0.9; }
|
||||
}
|
||||
|
||||
@keyframes theme-new-fade {
|
||||
from { opacity: 0.92; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root::view-transition-group(root),
|
||||
:root::view-transition-old(root),
|
||||
:root::view-transition-new(root) {
|
||||
animation-duration: 1ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端响应式样式 */
|
||||
@@ -117,38 +144,17 @@ body {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 移动端隐藏滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
/* 移动端优化触摸区域 */
|
||||
button,
|
||||
a,
|
||||
[role="button"] {
|
||||
[role='button'] {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
:root {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端安全区域适配 (iPhone X+) */
|
||||
@supports (padding: max(0px)) {
|
||||
body {
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端禁止长按选择 */
|
||||
@media (max-width: 768px) {
|
||||
|
||||
img,
|
||||
button {
|
||||
@@ -184,250 +190,55 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
:root {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端安全区域适配 (iPhone X+) */
|
||||
@supports (padding: max(0px)) {
|
||||
body {
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
.ant-tooltip {
|
||||
max-width: min(420px, calc(100vw - 24px));
|
||||
}
|
||||
|
||||
/* 侧边栏菜单整体样式 */
|
||||
.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-layout-sider-collapsed .ant-menu-item .anticon {
|
||||
margin-right: 0 !important;
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
/* 折叠状态下隐藏文字但保持点击区域 */
|
||||
.modern-sider.ant-layout-sider-collapsed .ant-menu-item .ant-menu-title-content {
|
||||
width: 0 !important;
|
||||
overflow: hidden !important;
|
||||
opacity: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 确保折叠状态下的 Link 覆盖整个菜单项区域 */
|
||||
.modern-sider.ant-layout-sider-collapsed .ant-menu-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modern-sider.ant-layout-sider-collapsed .ant-menu-item a {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 选中项左侧指示条 */
|
||||
.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-content,
|
||||
.ant-tooltip .ant-tooltip-inner {
|
||||
background: linear-gradient(135deg,
|
||||
var(--color-primary) 0%,
|
||||
var(--color-primary-hover) 100%);
|
||||
max-width: inherit;
|
||||
}
|
||||
|
||||
.ant-tooltip .ant-tooltip-inner {
|
||||
background: var(--app-tooltip-bg, #884d5c);
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 4px 12px rgba(77, 128, 136, 0.3);
|
||||
}
|
||||
box-shadow: 0 4px 12px var(--app-tooltip-shadow, rgba(136, 77, 92, 0.3));
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ant-tooltip {
|
||||
max-width: calc(100vw - 16px);
|
||||
}
|
||||
|
||||
.ant-tooltip .ant-tooltip-inner {
|
||||
padding: 8px 12px;
|
||||
max-height: 50vh;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Spin, Result, Button, Modal, Input, message } from 'antd';
|
||||
import { Spin, Result, Button, Modal, Input, message, theme } from 'antd';
|
||||
import { authApi } from '../services/api';
|
||||
import AnnouncementModal from '../components/AnnouncementModal';
|
||||
|
||||
@@ -10,6 +10,8 @@ export default function AuthCallback() {
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false);
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||
const { token } = theme.useToken();
|
||||
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||
interface PasswordStatus {
|
||||
has_password: boolean;
|
||||
has_custom_password: boolean;
|
||||
@@ -92,11 +94,11 @@ export default function AuthCallback() {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '100vh',
|
||||
background: 'linear-gradient(135deg, #4D8088 0%, #5F9EA8 100%)',
|
||||
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
|
||||
}}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Spin size="large" />
|
||||
<div style={{ marginTop: 20, color: 'white', fontSize: 16 }}>
|
||||
<div style={{ marginTop: 20, color: token.colorWhite, fontSize: 16 }}>
|
||||
正在处理登录...
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,7 +113,7 @@ export default function AuthCallback() {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '100vh',
|
||||
background: 'linear-gradient(135deg, #4D8088 0%, #5F9EA8 100%)',
|
||||
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
|
||||
}}>
|
||||
<Result
|
||||
status="error"
|
||||
@@ -122,7 +124,7 @@ export default function AuthCallback() {
|
||||
返回登录
|
||||
</Button>
|
||||
}
|
||||
style={{ background: 'white', padding: 40, borderRadius: 8 }}
|
||||
style={{ background: token.colorBgContainer, padding: 40, borderRadius: 8 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -257,17 +259,17 @@ export default function AuthCallback() {
|
||||
<p>系统已为您自动生成默认密码,您可以选择设置自定义密码或继续使用默认密码。</p>
|
||||
{passwordStatus?.default_password && (
|
||||
<div style={{
|
||||
background: '#f0f2f5',
|
||||
background: token.colorFillTertiary,
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
marginTop: 12
|
||||
}}>
|
||||
<strong>账号:</strong>{passwordStatus.username}<br />
|
||||
<strong>默认密码:</strong><code style={{
|
||||
background: '#fff',
|
||||
background: token.colorBgContainer,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 3,
|
||||
color: '#1890ff',
|
||||
color: token.colorPrimary,
|
||||
fontSize: 14
|
||||
}}>{passwordStatus.default_password}</code>
|
||||
</div>
|
||||
@@ -301,13 +303,13 @@ export default function AuthCallback() {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '100vh',
|
||||
background: 'linear-gradient(135deg, #4D8088 0%, #5F9EA8 100%)',
|
||||
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
|
||||
}}>
|
||||
<Result
|
||||
status="success"
|
||||
title="登录成功"
|
||||
subTitle={showPasswordModal ? "请设置账号密码..." : (showAnnouncement ? "欢迎使用..." : "正在跳转...")}
|
||||
style={{ background: 'white', padding: 40, borderRadius: 8 }}
|
||||
style={{ background: alphaColor(token.colorBgContainer, 0.96), padding: 40, borderRadius: 8 }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
InputNumber,
|
||||
List,
|
||||
message,
|
||||
Popconfirm,
|
||||
Progress,
|
||||
Row,
|
||||
Select,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
Tag,
|
||||
Typography,
|
||||
Upload,
|
||||
theme,
|
||||
} from 'antd';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
import { InboxOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined, WarningOutlined, RedoOutlined } from '@ant-design/icons';
|
||||
@@ -31,7 +33,7 @@ import type {
|
||||
BookImportTask,
|
||||
} from '../types';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Text, Title } = Typography;
|
||||
const { Dragger } = Upload;
|
||||
const { TextArea } = Input;
|
||||
|
||||
@@ -106,6 +108,8 @@ function isNotFoundError(error: unknown): boolean {
|
||||
|
||||
export default function BookImport() {
|
||||
const navigate = useNavigate();
|
||||
const { token } = theme.useToken();
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
const [taskId, setTaskId] = useState<string | null>(null);
|
||||
@@ -140,6 +144,40 @@ export default function BookImport() {
|
||||
return 1;
|
||||
}, [taskId, taskStatus, preview, applying, isApplyComplete]);
|
||||
|
||||
const canRestart = useMemo(() => {
|
||||
return Boolean(
|
||||
file ||
|
||||
taskId ||
|
||||
taskStatus ||
|
||||
preview ||
|
||||
applyProgress > 0 ||
|
||||
applyMessage ||
|
||||
applyError ||
|
||||
isApplyComplete ||
|
||||
failedSteps.length > 0 ||
|
||||
retrying
|
||||
);
|
||||
}, [
|
||||
file,
|
||||
taskId,
|
||||
taskStatus,
|
||||
preview,
|
||||
applyProgress,
|
||||
applyMessage,
|
||||
applyError,
|
||||
isApplyComplete,
|
||||
failedSteps,
|
||||
retrying,
|
||||
]);
|
||||
|
||||
const stepItems = [
|
||||
{ title: '上传文件' },
|
||||
{ title: '解析中' },
|
||||
{ title: '预览修改' },
|
||||
{ title: '生成导入' },
|
||||
];
|
||||
const currentStepText = stepItems[currentStep]?.title || '上传文件';
|
||||
|
||||
useEffect(() => {
|
||||
const cache = loadBookImportCache();
|
||||
if (cache) {
|
||||
@@ -487,6 +525,31 @@ export default function BookImport() {
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
const restartImport = useCallback(() => {
|
||||
clearBookImportCache();
|
||||
importedProjectId.current = null;
|
||||
|
||||
setFile(null);
|
||||
setTaskId(null);
|
||||
setTaskStatus(null);
|
||||
setPreview(null);
|
||||
|
||||
setCreatingTask(false);
|
||||
setLoadingPreview(false);
|
||||
setApplying(false);
|
||||
setApplyProgress(0);
|
||||
setApplyMessage('');
|
||||
setApplyError(null);
|
||||
setIsApplyComplete(false);
|
||||
|
||||
setFailedSteps([]);
|
||||
setRetrying(false);
|
||||
setRetryProgress(0);
|
||||
setRetryMessage('');
|
||||
|
||||
message.success('已重新开始,请重新上传 TXT 并解析');
|
||||
}, []);
|
||||
|
||||
const updateChapter = (index: number, patch: Partial<BookImportPreview['chapters'][number]>) => {
|
||||
setPreview(prev => {
|
||||
if (!prev) return prev;
|
||||
@@ -497,18 +560,100 @@ export default function BookImport() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', overflow: 'auto', paddingRight: 8 }}>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Steps
|
||||
current={currentStep}
|
||||
items={[
|
||||
{ title: '上传文件' },
|
||||
{ title: '解析中' },
|
||||
{ title: '预览修改' },
|
||||
{ title: '生成导入' },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<div
|
||||
style={{
|
||||
minHeight: '90vh',
|
||||
overflow: 'auto',
|
||||
background: `linear-gradient(180deg, ${token.colorBgLayout} 0%, ${token.colorFillSecondary} 100%)`,
|
||||
padding: isMobile ? '20px 16px 70px' : '24px 24px 70px',
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: 1400, margin: '0 auto', width: '100%' }}>
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
|
||||
borderRadius: isMobile ? 16 : 20,
|
||||
boxShadow: token.boxShadowSecondary,
|
||||
marginBottom: isMobile ? 14 : 16,
|
||||
border: 'none',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'absolute', top: -48, right: -48, width: 160, height: 160, borderRadius: '50%', background: token.colorWhite, opacity: 0.08, pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', bottom: -40, left: '26%', width: 110, height: 110, borderRadius: '50%', background: token.colorWhite, opacity: 0.05, 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, color: token.colorWhite, textShadow: `0 2px 4px ${token.colorBgMask}` }}>
|
||||
<InboxOutlined style={{ color: token.colorWhite, opacity: 0.9, marginRight: 8 }} />
|
||||
拆书导入
|
||||
</Title>
|
||||
<Text style={{ fontSize: isMobile ? 12 : 14, color: token.colorTextLightSolid, opacity: 0.85, marginLeft: isMobile ? 40 : 48 }}>
|
||||
上传TXT并自动解析为章节、预览并导入项目
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Space
|
||||
size={12}
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: isMobile ? 'flex-start' : 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Tag
|
||||
style={{
|
||||
marginInlineEnd: 0,
|
||||
background: token.colorWhite,
|
||||
border: `1px solid ${token.colorWhite}`,
|
||||
color: token.colorPrimary,
|
||||
fontWeight: 600,
|
||||
borderRadius: 8,
|
||||
paddingInline: 10,
|
||||
}}
|
||||
>
|
||||
当前进度:{currentStepText}
|
||||
</Tag>
|
||||
<Popconfirm
|
||||
title="确认重新开始?"
|
||||
description="将清空当前拆书任务与缓存,并回到上传文件步骤。"
|
||||
onConfirm={restartImport}
|
||||
okText="重新开始"
|
||||
cancelText="取消"
|
||||
disabled={!canRestart}
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
type="primary"
|
||||
icon={<ReloadOutlined />}
|
||||
disabled={!canRestart}
|
||||
style={{ boxShadow: '0 6px 16px rgba(0, 0, 0, 0.2)', borderRadius: 10 }}
|
||||
>
|
||||
重新开始
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
marginTop: isMobile ? 14 : 18,
|
||||
borderRadius: 12,
|
||||
background: token.colorBgContainer,
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
boxShadow: token.boxShadow,
|
||||
}}
|
||||
styles={{ body: { padding: isMobile ? '10px 12px' : '12px 16px' } }}
|
||||
>
|
||||
<Steps current={currentStep} size={isMobile ? 'small' : 'default'} items={stepItems} />
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
{currentStep === 0 && (
|
||||
<Card title="上传 TXT 并开始解析" style={{ marginBottom: 16 }}>
|
||||
@@ -913,6 +1058,8 @@ export default function BookImport() {
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, List, Button, Space, Empty, Tag, Spin, Alert, Switch, Drawer, message } from 'antd';
|
||||
import { Card, List, Button, Space, Empty, Tag, Spin, Alert, Switch, Drawer, message, theme } from 'antd';
|
||||
import {
|
||||
EyeOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
@@ -77,6 +77,7 @@ const ChapterAnalysis: React.FC = () => {
|
||||
const [scrollToContentAnnotation, setScrollToContentAnnotation] = useState<string | undefined>();
|
||||
const [scrollToSidebarAnnotation, setScrollToSidebarAnnotation] = useState<string | undefined>();
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
||||
const { token } = theme.useToken();
|
||||
|
||||
// 监听窗口大小变化
|
||||
useEffect(() => {
|
||||
@@ -196,7 +197,7 @@ const ChapterAnalysis: React.FC = () => {
|
||||
<div style={{
|
||||
padding: '16px 0',
|
||||
marginBottom: 16,
|
||||
borderBottom: '1px solid #f0f0f0'
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`
|
||||
}}>
|
||||
<h2 style={{ margin: 0, fontSize: 24 }}>
|
||||
<FundOutlined style={{ marginRight: 8 }} />
|
||||
@@ -231,8 +232,8 @@ const ChapterAnalysis: React.FC = () => {
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '12px 16px',
|
||||
background: selectedChapter?.id === chapter.id ? '#e6f7ff' : 'transparent',
|
||||
borderLeft: selectedChapter?.id === chapter.id ? '3px solid #1890ff' : '3px solid transparent',
|
||||
background: selectedChapter?.id === chapter.id ? token.colorPrimaryBg : 'transparent',
|
||||
borderLeft: selectedChapter?.id === chapter.id ? `3px solid ${token.colorPrimary}` : '3px solid transparent',
|
||||
}}
|
||||
>
|
||||
<List.Item.Meta
|
||||
@@ -278,8 +279,8 @@ const ChapterAnalysis: React.FC = () => {
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '12px 16px',
|
||||
background: selectedChapter?.id === chapter.id ? '#e6f7ff' : 'transparent',
|
||||
borderLeft: selectedChapter?.id === chapter.id ? '3px solid #1890ff' : '3px solid transparent',
|
||||
background: selectedChapter?.id === chapter.id ? token.colorPrimaryBg : 'transparent',
|
||||
borderLeft: selectedChapter?.id === chapter.id ? `3px solid ${token.colorPrimary}` : '3px solid transparent',
|
||||
}}
|
||||
>
|
||||
<List.Item.Meta
|
||||
@@ -430,7 +431,7 @@ const ChapterAnalysis: React.FC = () => {
|
||||
checkedChildren={<EyeOutlined />}
|
||||
unCheckedChildren={<EyeInvisibleOutlined />}
|
||||
/>
|
||||
<span style={{ fontSize: 13, color: '#666' }}>显示标注</span>
|
||||
<span style={{ fontSize: 13, color: token.colorTextSecondary }}>显示标注</span>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
@@ -441,7 +442,7 @@ const ChapterAnalysis: React.FC = () => {
|
||||
<div style={{
|
||||
marginTop: 12,
|
||||
fontSize: isMobile ? 11 : 12,
|
||||
color: '#999',
|
||||
color: token.colorTextTertiary,
|
||||
lineHeight: 1.5
|
||||
}}>
|
||||
共有 {annotationsData.summary.total_annotations} 个标注:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Card, Spin, Alert, Button, Space, Switch, Drawer, message, Progress } from 'antd';
|
||||
import { Card, Spin, Alert, Button, Space, Switch, Drawer, message, Progress, theme } from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
EyeOutlined,
|
||||
@@ -64,6 +64,8 @@ const ChapterReader: React.FC = () => {
|
||||
const { chapterId } = useParams<{ chapterId: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [chapter, setChapter] = useState<ChapterData | null>(null);
|
||||
@@ -303,7 +305,7 @@ const ChapterReader: React.FC = () => {
|
||||
checkedChildren={<EyeOutlined />}
|
||||
unCheckedChildren={<EyeInvisibleOutlined />}
|
||||
/>
|
||||
<span style={{ fontSize: 13, color: '#666' }}>显示标注</span>
|
||||
<span style={{ fontSize: 13, color: token.colorTextSecondary }}>显示标注</span>
|
||||
<Button
|
||||
icon={<MenuOutlined />}
|
||||
onClick={() => setSidebarVisible(true)}
|
||||
@@ -319,14 +321,14 @@ const ChapterReader: React.FC = () => {
|
||||
{analyzing && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Progress percent={analysisProgress} size="small" status="active" />
|
||||
<span style={{ fontSize: 12, color: '#666', marginLeft: 8 }}>
|
||||
<span style={{ fontSize: 12, color: token.colorTextSecondary, marginLeft: 8 }}>
|
||||
正在分析章节...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!analyzing && hasAnnotations && annotationsData && (
|
||||
<div style={{ marginTop: 12, fontSize: 12, color: '#999' }}>
|
||||
<div style={{ marginTop: 12, fontSize: 12, color: token.colorTextTertiary }}>
|
||||
共有 {annotationsData.summary.total_annotations} 个标注:
|
||||
{annotationsData.summary.hooks > 0 && ` 🎣${annotationsData.summary.hooks}个钩子`}
|
||||
{annotationsData.summary.foreshadows > 0 &&
|
||||
@@ -383,7 +385,7 @@ const ChapterReader: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* 底部翻页按钮 */}
|
||||
<div style={{ marginTop: 48, paddingTop: 24, borderTop: '1px solid #f0f0f0' }}>
|
||||
<div style={{ marginTop: 48, paddingTop: 24, borderTop: `1px solid ${token.colorBorderSecondary}` }}>
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<Button
|
||||
size="large"
|
||||
@@ -418,9 +420,9 @@ const ChapterReader: React.FC = () => {
|
||||
<div
|
||||
style={{
|
||||
width: 400,
|
||||
borderLeft: '1px solid #f0f0f0',
|
||||
borderLeft: `1px solid ${token.colorBorderSecondary}`,
|
||||
overflowY: 'auto',
|
||||
background: '#fafafa',
|
||||
background: token.colorBgLayout,
|
||||
}}
|
||||
>
|
||||
<MemorySidebar
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, InputNumber, Alert, Radio, Descriptions, Collapse, Popconfirm, Pagination } from 'antd';
|
||||
import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, InputNumber, Alert, Radio, Descriptions, Collapse, Popconfirm, Pagination, theme } from 'antd';
|
||||
import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, RocketOutlined, StopOutlined, InfoCircleOutlined, CaretRightOutlined, DeleteOutlined, BookOutlined, FormOutlined, PlusOutlined, ReadOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { useChapterSync } from '../store/hooks';
|
||||
@@ -48,6 +48,7 @@ const setCachedWordCount = (value: number): void => {
|
||||
export default function Chapters() {
|
||||
const { currentProject, chapters, outlines, setCurrentChapter, setCurrentProject } = useStore();
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const { token } = theme.useToken();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
||||
const [isContinuing, setIsContinuing] = useState(false);
|
||||
@@ -900,11 +901,11 @@ export default function Chapters() {
|
||||
<div style={{
|
||||
marginTop: 16,
|
||||
padding: 12,
|
||||
background: 'var(--color-info-bg)',
|
||||
borderRadius: 4,
|
||||
border: '1px solid var(--color-info-border)'
|
||||
background: token.colorInfoBg,
|
||||
borderRadius: token.borderRadius,
|
||||
border: `1px solid ${token.colorInfoBorder}`
|
||||
}}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500, color: 'var(--color-primary)' }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500, color: token.colorPrimary }}>
|
||||
📚 将引用的前置章节(共{previousChapters.length}章):
|
||||
</div>
|
||||
<div style={{ maxHeight: 150, overflowY: 'auto' }}>
|
||||
@@ -914,13 +915,13 @@ export default function Chapters() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: '#666' }}>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: token.colorTextSecondary }}>
|
||||
💡 AI会参考这些章节内容,确保情节连贯、角色状态一致
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p style={{ color: '#ff4d4f', marginTop: 16, marginBottom: 0 }}>
|
||||
<p style={{ color: token.colorError, marginTop: 16, marginBottom: 0 }}>
|
||||
⚠️ 注意:此操作将覆盖当前章节内容
|
||||
</p>
|
||||
</div>
|
||||
@@ -1406,7 +1407,7 @@ export default function Chapters() {
|
||||
// 显示冲突提示Modal
|
||||
modal.confirm({
|
||||
title: '章节序号冲突',
|
||||
icon: <InfoCircleOutlined style={{ color: '#ff4d4f' }} />,
|
||||
icon: <InfoCircleOutlined style={{ color: token.colorError }} />,
|
||||
width: 500,
|
||||
centered: true,
|
||||
content: (
|
||||
@@ -1416,9 +1417,9 @@ export default function Chapters() {
|
||||
</p>
|
||||
<div style={{
|
||||
padding: 12,
|
||||
background: '#fff7e6',
|
||||
borderRadius: 4,
|
||||
border: '1px solid #ffd591',
|
||||
background: token.colorWarningBg,
|
||||
borderRadius: token.borderRadius,
|
||||
border: `1px solid ${token.colorWarningBorder}`,
|
||||
marginBottom: 12
|
||||
}}>
|
||||
<div><strong>标题:</strong>{conflictChapter.title}</div>
|
||||
@@ -1428,10 +1429,10 @@ export default function Chapters() {
|
||||
<div><strong>所属大纲:</strong>{conflictChapter.outline_title}</div>
|
||||
)}
|
||||
</div>
|
||||
<p style={{ color: '#ff4d4f', marginBottom: 8 }}>
|
||||
<p style={{ color: token.colorError, marginBottom: 8 }}>
|
||||
⚠️ 是否删除旧章节并创建新章节?
|
||||
</p>
|
||||
<p style={{ fontSize: 12, color: '#666', marginBottom: 0 }}>
|
||||
<p style={{ fontSize: 12, color: token.colorTextSecondary, marginBottom: 0 }}>
|
||||
删除后将无法恢复,章节内容和分析结果都将被删除。
|
||||
</p>
|
||||
</div>
|
||||
@@ -1551,7 +1552,7 @@ export default function Chapters() {
|
||||
modal.info({
|
||||
title: (
|
||||
<Space style={{ flexWrap: 'wrap' }}>
|
||||
<InfoCircleOutlined style={{ color: 'var(--color-primary)' }} />
|
||||
<InfoCircleOutlined style={{ color: token.colorPrimary }} />
|
||||
<span style={{ wordBreak: 'break-word' }}>第{chapter.chapter_number}章展开规划</span>
|
||||
</Space>
|
||||
),
|
||||
@@ -1684,7 +1685,7 @@ export default function Chapters() {
|
||||
key={idx}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: '#fafafa',
|
||||
backgroundColor: token.colorFillQuaternary,
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
@@ -1875,10 +1876,10 @@ export default function Chapters() {
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: 'var(--color-bg-container)',
|
||||
backgroundColor: token.colorBgContainer,
|
||||
padding: isMobile ? '12px 0' : '16px 0',
|
||||
marginBottom: isMobile ? 12 : 16,
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
display: 'flex',
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
gap: isMobile ? 12 : 0,
|
||||
@@ -1925,7 +1926,7 @@ export default function Chapters() {
|
||||
disabled={chapters.length === 0 || batchAnalyzableChapterCount === 0}
|
||||
block={isMobile}
|
||||
size={isMobile ? 'middle' : 'middle'}
|
||||
style={{ background: '#fa8c16', borderColor: '#fa8c16' }}
|
||||
style={{ background: token.colorWarning, borderColor: token.colorWarning }}
|
||||
title={batchAnalyzableChapterCount === 0 ? '暂无可一键分析章节' : `可一键分析 ${batchAnalyzableChapterCount} 章`}
|
||||
>
|
||||
一键分析{batchAnalyzableChapterCount > 0 ? ` (${batchAnalyzableChapterCount})` : ''}
|
||||
@@ -1937,7 +1938,7 @@ export default function Chapters() {
|
||||
disabled={chapters.length === 0}
|
||||
block={isMobile}
|
||||
size={isMobile ? 'middle' : 'middle'}
|
||||
style={{ background: '#722ed1', borderColor: '#722ed1' }}
|
||||
style={{ background: token.colorInfo, borderColor: token.colorInfo }}
|
||||
>
|
||||
批量生成
|
||||
</Button>
|
||||
@@ -1969,9 +1970,9 @@ export default function Chapters() {
|
||||
style={{
|
||||
padding: '16px',
|
||||
marginBottom: 16,
|
||||
background: '#fff',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #f0f0f0',
|
||||
background: token.colorBgContainer,
|
||||
borderRadius: token.borderRadius,
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
alignItems: isMobile ? 'flex-start' : 'center',
|
||||
}}
|
||||
@@ -2025,7 +2026,7 @@ export default function Chapters() {
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
<List.Item.Meta
|
||||
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: 'var(--color-primary)' }} />}
|
||||
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: token.colorPrimary }} />}
|
||||
title={
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
@@ -2039,7 +2040,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: 'var(--color-success)' }} />
|
||||
<Badge count={`${item.word_count || 0}字`} style={{ backgroundColor: token.colorSuccess }} />
|
||||
{renderAnalysisStatus(item.id)}
|
||||
{!canGenerateChapter(item) && (
|
||||
<Tag icon={<LockOutlined />} color="warning" title={getGenerateDisabledReason(item)}>
|
||||
@@ -2051,12 +2052,12 @@ export default function Chapters() {
|
||||
}
|
||||
description={
|
||||
item.content ? (
|
||||
<div style={{ marginTop: 8, color: 'rgba(0,0,0,0.65)', lineHeight: 1.6, fontSize: isMobile ? 12 : 14 }}>
|
||||
<div style={{ marginTop: 8, color: token.colorTextSecondary, lineHeight: 1.6, fontSize: isMobile ? 12 : 14 }}>
|
||||
{item.content.substring(0, isMobile ? 80 : 150)}
|
||||
{item.content.length > (isMobile ? 80 : 150) && '...'}
|
||||
</div>
|
||||
) : (
|
||||
<span style={{ color: 'rgba(0,0,0,0.45)', fontSize: isMobile ? 12 : 14 }}>暂无内容</span>
|
||||
<span style={{ color: token.colorTextTertiary, fontSize: isMobile ? 12 : 14 }}>暂无内容</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -2134,19 +2135,19 @@ export default function Chapters() {
|
||||
</span>
|
||||
<Badge
|
||||
count={`${group.chapters.length} 章`}
|
||||
style={{ backgroundColor: 'var(--color-success)' }}
|
||||
style={{ backgroundColor: token.colorSuccess }}
|
||||
/>
|
||||
<Badge
|
||||
count={`${group.chapters.reduce((sum, ch) => sum + (ch.word_count || 0), 0)} 字`}
|
||||
style={{ backgroundColor: 'var(--color-primary)' }}
|
||||
style={{ backgroundColor: token.colorPrimary }}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
background: '#fff',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #f0f0f0',
|
||||
background: token.colorBgContainer,
|
||||
borderRadius: token.borderRadius,
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
}}
|
||||
>
|
||||
<List
|
||||
@@ -2230,7 +2231,7 @@ export default function Chapters() {
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
<List.Item.Meta
|
||||
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: 'var(--color-primary)' }} />}
|
||||
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: token.colorPrimary }} />}
|
||||
title={
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
@@ -2244,7 +2245,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: 'var(--color-success)' }} />
|
||||
<Badge count={`${item.word_count || 0}字`} style={{ backgroundColor: token.colorSuccess }} />
|
||||
{renderAnalysisStatus(item.id)}
|
||||
{!canGenerateChapter(item) && (
|
||||
<Tag icon={<LockOutlined />} color="warning" title={getGenerateDisabledReason(item)}>
|
||||
@@ -2255,7 +2256,7 @@ export default function Chapters() {
|
||||
{item.expansion_plan && (
|
||||
<InfoCircleOutlined
|
||||
title="查看展开详情"
|
||||
style={{ color: 'var(--color-primary)', cursor: 'pointer', fontSize: 16 }}
|
||||
style={{ color: token.colorPrimary, cursor: 'pointer', fontSize: 16 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
showExpansionPlanModal(item);
|
||||
@@ -2264,7 +2265,7 @@ export default function Chapters() {
|
||||
)}
|
||||
<FormOutlined
|
||||
title={item.expansion_plan ? "编辑规划信息" : "创建规划信息"}
|
||||
style={{ color: 'var(--color-success)', cursor: 'pointer', fontSize: 16 }}
|
||||
style={{ color: token.colorSuccess, cursor: 'pointer', fontSize: 16 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOpenPlanEditor(item);
|
||||
@@ -2276,12 +2277,12 @@ export default function Chapters() {
|
||||
}
|
||||
description={
|
||||
item.content ? (
|
||||
<div style={{ marginTop: 8, color: 'rgba(0,0,0,0.65)', lineHeight: 1.6, fontSize: isMobile ? 12 : 14 }}>
|
||||
<div style={{ marginTop: 8, color: token.colorTextSecondary, lineHeight: 1.6, fontSize: isMobile ? 12 : 14 }}>
|
||||
{item.content.substring(0, isMobile ? 80 : 150)}
|
||||
{item.content.length > (isMobile ? 80 : 150) && '...'}
|
||||
</div>
|
||||
) : (
|
||||
<span style={{ color: 'rgba(0,0,0,0.45)', fontSize: isMobile ? 12 : 14 }}>暂无内容</span>
|
||||
<span style={{ color: token.colorTextTertiary, fontSize: isMobile ? 12 : 14 }}>暂无内容</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -2539,7 +2540,7 @@ export default function Chapters() {
|
||||
))}
|
||||
</Select>
|
||||
{!selectedStyleId && (
|
||||
<div style={{ color: '#ff4d4f', fontSize: 12, marginTop: 4 }}>请选择写作风格</div>
|
||||
<div style={{ color: token.colorError, fontSize: 12, marginTop: 4 }}>请选择写作风格</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
@@ -2560,7 +2561,7 @@ export default function Chapters() {
|
||||
<Select.Option value="全知视角">全知视角</Select.Option>
|
||||
</Select>
|
||||
{temporaryNarrativePerspective && (
|
||||
<div style={{ color: 'var(--color-success)', fontSize: 12, marginTop: 4 }}>
|
||||
<div style={{ color: token.colorSuccess, fontSize: 12, marginTop: 4 }}>
|
||||
✓ {getNarrativePerspectiveText(temporaryNarrativePerspective)}
|
||||
</div>
|
||||
)}
|
||||
@@ -2703,7 +2704,7 @@ export default function Chapters() {
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<RocketOutlined style={{ color: '#722ed1' }} />
|
||||
<RocketOutlined style={{ color: token.colorInfo }} />
|
||||
<span>批量生成章节内容</span>
|
||||
</Space>
|
||||
}
|
||||
@@ -2879,7 +2880,7 @@ export default function Chapters() {
|
||||
>
|
||||
<Radio.Group disabled>
|
||||
<Radio value={true}>
|
||||
<span style={{ fontSize: 12, color: '#52c41a' }}>✓ 自动更新角色状态</span>
|
||||
<span style={{ fontSize: 12, color: token.colorSuccess }}>✓ 自动更新角色状态</span>
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Button, Modal, Form, Input, Select, message, Row, Col, Empty, Tabs, Divider, Typography, Space, InputNumber, Checkbox } from 'antd';
|
||||
import { Button, Modal, Form, Input, Select, message, Row, Col, Empty, Tabs, Divider, Typography, Space, InputNumber, Checkbox, theme } from 'antd';
|
||||
import { ThunderboltOutlined, UserOutlined, TeamOutlined, PlusOutlined, ExportOutlined, ImportOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { useCharacterSync } from '../store/hooks';
|
||||
import { characterGridConfig } from '../components/CardStyles';
|
||||
import { charactersPageGridConfig } from '../components/CardStyles';
|
||||
import { CharacterCard } from '../components/CharacterCard';
|
||||
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
|
||||
import type { Character, ApiError } from '../types';
|
||||
@@ -94,6 +94,7 @@ interface CharacterUpdateData {
|
||||
}
|
||||
|
||||
export default function Characters() {
|
||||
const { token } = theme.useToken();
|
||||
const { currentProject, characters } = useStore();
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
@@ -390,7 +391,7 @@ export default function Characters() {
|
||||
content: (
|
||||
<div>
|
||||
{validation.errors.map((error, index) => (
|
||||
<div key={index} style={{ color: 'red' }}>• {error}</div>
|
||||
<div key={index} style={{ color: token.colorError }}>• {error}</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
@@ -415,10 +416,10 @@ export default function Characters() {
|
||||
{validation.warnings.length > 0 && (
|
||||
<>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<p style={{ color: '#faad14' }}><strong>⚠️ 警告:</strong></p>
|
||||
<p style={{ color: token.colorWarning }}><strong>⚠️ 警告:</strong></p>
|
||||
<ul style={{ marginLeft: 20 }}>
|
||||
{validation.warnings.map((warning, index) => (
|
||||
<li key={index} style={{ color: '#faad14' }}>{warning}</li>
|
||||
<li key={index} style={{ color: token.colorWarning }}>{warning}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
@@ -463,10 +464,10 @@ export default function Characters() {
|
||||
{result.statistics.skipped > 0 && (
|
||||
<>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<p style={{ color: '#faad14' }}>⚠️ 跳过: {result.statistics.skipped} 个</p>
|
||||
<p style={{ color: token.colorWarning }}>⚠️ 跳过: {result.statistics.skipped} 个</p>
|
||||
<ul style={{ marginLeft: 20 }}>
|
||||
{result.details.skipped.map((name, index) => (
|
||||
<li key={index} style={{ color: '#faad14' }}>{name}</li>
|
||||
<li key={index} style={{ color: token.colorWarning }}>{name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
@@ -474,10 +475,10 @@ export default function Characters() {
|
||||
{result.warnings.length > 0 && (
|
||||
<>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<p style={{ color: '#faad14' }}>⚠️ 警告:</p>
|
||||
<p style={{ color: token.colorWarning }}>⚠️ 警告:</p>
|
||||
<ul style={{ marginLeft: 20 }}>
|
||||
{result.warnings.map((warning, index) => (
|
||||
<li key={index} style={{ color: '#faad14' }}>{warning}</li>
|
||||
<li key={index} style={{ color: token.colorWarning }}>{warning}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
@@ -485,10 +486,10 @@ export default function Characters() {
|
||||
{result.details.errors.length > 0 && (
|
||||
<>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<p style={{ color: 'red' }}>❌ 失败: {result.statistics.errors} 个</p>
|
||||
<p style={{ color: token.colorError }}>❌ 失败: {result.statistics.errors} 个</p>
|
||||
<ul style={{ marginLeft: 20 }}>
|
||||
{result.details.errors.map((error, index) => (
|
||||
<li key={index} style={{ color: 'red' }}>{error}</li>
|
||||
<li key={index} style={{ color: token.colorError }}>{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
@@ -777,7 +778,7 @@ export default function Characters() {
|
||||
<Empty description="还没有角色或组织,开始创建吧!" />
|
||||
) : (
|
||||
<>
|
||||
<Row gutter={isMobile ? [8, 8] : characterGridConfig.gutter}>
|
||||
<Row gutter={isMobile ? [8, 8] : charactersPageGridConfig.gutter}>
|
||||
{activeTab === 'all' && (
|
||||
<>
|
||||
{characterList.length > 0 && (
|
||||
@@ -793,10 +794,10 @@ export default function Characters() {
|
||||
{characterList.map((character) => (
|
||||
<Col
|
||||
xs={24}
|
||||
sm={characterGridConfig.sm}
|
||||
md={characterGridConfig.md}
|
||||
lg={characterGridConfig.lg}
|
||||
xl={characterGridConfig.xl}
|
||||
sm={charactersPageGridConfig.sm}
|
||||
md={charactersPageGridConfig.md}
|
||||
lg={charactersPageGridConfig.lg}
|
||||
xl={charactersPageGridConfig.xl}
|
||||
key={character.id}
|
||||
style={{ padding: isMobile ? '4px' : '8px' }}
|
||||
>
|
||||
@@ -831,10 +832,10 @@ export default function Characters() {
|
||||
{organizationList.map((org) => (
|
||||
<Col
|
||||
xs={24}
|
||||
sm={characterGridConfig.sm}
|
||||
md={characterGridConfig.md}
|
||||
lg={characterGridConfig.lg}
|
||||
xl={characterGridConfig.xl}
|
||||
sm={charactersPageGridConfig.sm}
|
||||
md={charactersPageGridConfig.md}
|
||||
lg={charactersPageGridConfig.lg}
|
||||
xl={charactersPageGridConfig.xl}
|
||||
key={org.id}
|
||||
style={{ padding: isMobile ? '4px' : '8px' }}
|
||||
>
|
||||
@@ -861,10 +862,10 @@ export default function Characters() {
|
||||
{activeTab === 'character' && characterList.map((character) => (
|
||||
<Col
|
||||
xs={24}
|
||||
sm={characterGridConfig.sm}
|
||||
md={characterGridConfig.md}
|
||||
lg={characterGridConfig.lg}
|
||||
xl={characterGridConfig.xl}
|
||||
sm={charactersPageGridConfig.sm}
|
||||
md={charactersPageGridConfig.md}
|
||||
lg={charactersPageGridConfig.lg}
|
||||
xl={charactersPageGridConfig.xl}
|
||||
key={character.id}
|
||||
style={{ padding: isMobile ? '4px' : '8px' }}
|
||||
>
|
||||
@@ -887,10 +888,10 @@ export default function Characters() {
|
||||
{activeTab === 'organization' && organizationList.map((org) => (
|
||||
<Col
|
||||
xs={24}
|
||||
sm={characterGridConfig.sm}
|
||||
md={characterGridConfig.md}
|
||||
lg={characterGridConfig.lg}
|
||||
xl={characterGridConfig.xl}
|
||||
sm={charactersPageGridConfig.sm}
|
||||
md={charactersPageGridConfig.md}
|
||||
lg={charactersPageGridConfig.lg}
|
||||
xl={charactersPageGridConfig.xl}
|
||||
key={org.id}
|
||||
style={{ padding: isMobile ? '4px' : '8px' }}
|
||||
>
|
||||
@@ -1020,7 +1021,7 @@ export default function Characters() {
|
||||
value={editingCharacter.relationships}
|
||||
readOnly
|
||||
autoSize={{ minRows: 1, maxRows: 3 }}
|
||||
style={{ backgroundColor: '#f5f5f5', cursor: 'default' }}
|
||||
style={{ backgroundColor: token.colorFillTertiary, cursor: 'default' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
@@ -1195,10 +1196,10 @@ export default function Characters() {
|
||||
disabled
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
placeholder="暂无成员,请在组织管理中添加"
|
||||
style={{ color: '#333', backgroundColor: '#fafafa' }}
|
||||
style={{ color: token.colorText, backgroundColor: token.colorFillAlter }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div style={{ marginBottom: 12, fontSize: 12, color: '#8c8c8c' }}>
|
||||
<div style={{ marginBottom: 12, fontSize: 12, color: token.colorTextTertiary }}>
|
||||
💡 请前往「组织管理」页面添加或管理组织成员
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
Card, Table, Button, Tag, Space, Modal, Form, Input, Select,
|
||||
InputNumber, Switch, message, Tooltip, Popconfirm, Statistic,
|
||||
Row, Col, Empty, Divider, Badge, Alert, Pagination, Dropdown
|
||||
Row, Col, Empty, Divider, Badge, Alert, Pagination, Dropdown, theme
|
||||
} from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import {
|
||||
@@ -73,6 +73,7 @@ export default function Foreshadows() {
|
||||
// 表格容器引用,用于计算滚动高度
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [tableScrollY, setTableScrollY] = useState<number>(400);
|
||||
const { token } = theme.useToken();
|
||||
|
||||
// 加载伏笔列表
|
||||
const loadForeshadows = useCallback(async () => {
|
||||
@@ -508,22 +509,22 @@ export default function Foreshadows() {
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Card size="small">
|
||||
<Statistic title="待埋入" value={stats.pending} valueStyle={{ color: '#8c8c8c' }} />
|
||||
<Statistic title="待埋入" value={stats.pending} valueStyle={{ color: token.colorTextSecondary }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Card size="small">
|
||||
<Statistic title="已埋入" value={stats.planted} valueStyle={{ color: '#52c41a' }} />
|
||||
<Statistic title="已埋入" value={stats.planted} valueStyle={{ color: token.colorSuccess }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Card size="small">
|
||||
<Statistic title="已回收" value={stats.resolved} valueStyle={{ color: '#1890ff' }} />
|
||||
<Statistic title="已回收" value={stats.resolved} valueStyle={{ color: token.colorPrimary }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Card size="small">
|
||||
<Statistic title="长线伏笔" value={stats.long_term_count} valueStyle={{ color: '#722ed1' }} />
|
||||
<Statistic title="长线伏笔" value={stats.long_term_count} valueStyle={{ color: token.colorInfo }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
@@ -531,7 +532,7 @@ export default function Foreshadows() {
|
||||
<Statistic
|
||||
title="超期未回收"
|
||||
value={stats.overdue_count}
|
||||
valueStyle={{ color: stats.overdue_count > 0 ? '#ff4d4f' : '#8c8c8c' }}
|
||||
valueStyle={{ color: stats.overdue_count > 0 ? token.colorError : token.colorTextSecondary }}
|
||||
prefix={stats.overdue_count > 0 ? <WarningOutlined /> : null}
|
||||
/>
|
||||
</Card>
|
||||
@@ -659,12 +660,12 @@ export default function Foreshadows() {
|
||||
{/* 分页器 - 固定在底部居中 */}
|
||||
<div style={{
|
||||
padding: '12px 0',
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
background: '#fff',
|
||||
background: token.colorBgContainer,
|
||||
}}>
|
||||
<Pagination
|
||||
current={currentPage}
|
||||
@@ -868,7 +869,7 @@ export default function Foreshadows() {
|
||||
{currentForeshadow.hint_text && (
|
||||
<Col span={24}>
|
||||
<strong>暗示文本:</strong>
|
||||
<p style={{ marginTop: 8, whiteSpace: 'pre-wrap', color: '#666' }}>
|
||||
<p style={{ marginTop: 8, whiteSpace: 'pre-wrap', color: token.colorTextSecondary }}>
|
||||
{currentForeshadow.hint_text}
|
||||
</p>
|
||||
</Col>
|
||||
@@ -877,7 +878,7 @@ export default function Foreshadows() {
|
||||
{currentForeshadow.resolution_text && (
|
||||
<Col span={24}>
|
||||
<strong>揭示文本:</strong>
|
||||
<p style={{ marginTop: 8, whiteSpace: 'pre-wrap', color: '#666' }}>
|
||||
<p style={{ marginTop: 8, whiteSpace: 'pre-wrap', color: token.colorTextSecondary }}>
|
||||
{currentForeshadow.resolution_text}
|
||||
</p>
|
||||
</Col>
|
||||
@@ -920,7 +921,7 @@ export default function Foreshadows() {
|
||||
{currentForeshadow.notes && (
|
||||
<Col span={24}>
|
||||
<strong>备注:</strong>
|
||||
<p style={{ marginTop: 8, color: '#666' }}>{currentForeshadow.notes}</p>
|
||||
<p style={{ marginTop: 8, color: token.colorTextSecondary }}>{currentForeshadow.notes}</p>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Card, Input, Button, Space, Typography, message, Spin, Modal } from 'antd';
|
||||
import { Card, Input, Button, Space, Typography, message, Spin, Modal, theme } from 'antd';
|
||||
import { SendOutlined, ArrowLeftOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { inspirationApi } from '../services/api';
|
||||
import { AIProjectGenerator, type GenerationConfig } from '../components/AIProjectGenerator';
|
||||
@@ -52,6 +52,7 @@ const Inspiration: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [currentStep, setCurrentStep] = useState<Step>('idea');
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||
const { token } = theme.useToken();
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
@@ -852,7 +853,7 @@ const Inspiration: React.FC = () => {
|
||||
height: isMobile ? 'calc(100vh - 280px)' : 600,
|
||||
overflowY: 'auto',
|
||||
marginBottom: 16,
|
||||
boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
|
||||
boxShadow: `0 8px 24px color-mix(in srgb, ${token.colorTextBase} 20%, transparent)`,
|
||||
scrollBehavior: 'smooth'
|
||||
}}
|
||||
>
|
||||
@@ -873,16 +874,16 @@ const Inspiration: React.FC = () => {
|
||||
maxWidth: '80%',
|
||||
padding: '12px 16px',
|
||||
borderRadius: 12,
|
||||
background: msg.type === 'ai' ? 'var(--color-bg-container)' : 'var(--color-primary)',
|
||||
color: msg.type === 'ai' ? 'var(--color-text-primary)' : '#fff',
|
||||
background: msg.type === 'ai' ? token.colorBgContainer : token.colorPrimary,
|
||||
color: msg.type === 'ai' ? token.colorText : token.colorWhite,
|
||||
boxShadow: msg.type === 'ai'
|
||||
? 'var(--shadow-card)'
|
||||
: 'var(--shadow-primary)',
|
||||
? `0 2px 10px color-mix(in srgb, ${token.colorTextBase} 12%, transparent)`
|
||||
: `0 4px 14px color-mix(in srgb, ${token.colorPrimary} 30%, transparent)`,
|
||||
}}>
|
||||
<Paragraph
|
||||
style={{
|
||||
margin: 0,
|
||||
color: msg.type === 'ai' ? 'var(--color-text-primary)' : '#fff',
|
||||
color: msg.type === 'ai' ? token.colorText : token.colorWhite,
|
||||
whiteSpace: 'pre-wrap'
|
||||
}}
|
||||
>
|
||||
@@ -904,13 +905,13 @@ const Inspiration: React.FC = () => {
|
||||
style={{
|
||||
cursor: msg.optionsDisabled ? 'not-allowed' : 'pointer',
|
||||
border: msg.isMultiSelect && selectedOptions.includes(option)
|
||||
? '2px solid var(--color-primary)'
|
||||
: '1px solid var(--color-border)',
|
||||
? `2px solid ${token.colorPrimary}`
|
||||
: `1px solid ${token.colorBorder}`,
|
||||
background: msg.optionsDisabled
|
||||
? 'var(--color-bg-layout)'
|
||||
? token.colorBgLayout
|
||||
: msg.isMultiSelect && selectedOptions.includes(option)
|
||||
? 'var(--color-bg-spotlight)'
|
||||
: 'var(--color-bg-container)',
|
||||
? token.colorPrimaryBg
|
||||
: token.colorBgContainer,
|
||||
opacity: msg.optionsDisabled ? 0.6 : 1,
|
||||
animation: 'floatIn 0.6s ease-out',
|
||||
animationDelay: `${optIndex * 0.1}s`,
|
||||
@@ -920,7 +921,7 @@ const Inspiration: React.FC = () => {
|
||||
onMouseEnter={(e) => {
|
||||
if (!msg.optionsDisabled) {
|
||||
e.currentTarget.style.transform = 'translateY(-2px) scale(1.02)';
|
||||
e.currentTarget.style.boxShadow = 'var(--shadow-elevated)';
|
||||
e.currentTarget.style.boxShadow = `0 8px 22px color-mix(in srgb, ${token.colorTextBase} 14%, transparent)`;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
@@ -947,7 +948,7 @@ const Inspiration: React.FC = () => {
|
||||
|
||||
{/* 反馈优化区域 - 新增 */}
|
||||
{msg.canRefine && !msg.optionsDisabled && !msg.isMultiSelect && (
|
||||
<div style={{ marginTop: 8, paddingTop: 8, borderTop: '1px dashed var(--color-border)' }}>
|
||||
<div style={{ marginTop: 8, paddingTop: 8, borderTop: `1px dashed ${token.colorBorder}` }}>
|
||||
{showFeedbackInput === index ? (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||
<TextArea
|
||||
@@ -1018,7 +1019,7 @@ const Inspiration: React.FC = () => {
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
style={{ boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}
|
||||
style={{ boxShadow: `0 4px 12px color-mix(in srgb, ${token.colorTextBase} 14%, transparent)` }}
|
||||
styles={{ body: { padding: 12 } }}
|
||||
>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
@@ -1059,7 +1060,7 @@ const Inspiration: React.FC = () => {
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100dvh',
|
||||
background: 'var(--color-bg-base)',
|
||||
background: token.colorBgBase,
|
||||
}}>
|
||||
{contextHolder}
|
||||
<style>
|
||||
@@ -1105,8 +1106,8 @@ const Inspiration: React.FC = () => {
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
background: 'var(--color-primary)',
|
||||
boxShadow: 'var(--shadow-header)',
|
||||
background: token.colorPrimary,
|
||||
boxShadow: `0 6px 20px color-mix(in srgb, ${token.colorPrimary} 30%, transparent)`,
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: 1200,
|
||||
@@ -1121,9 +1122,9 @@ const Inspiration: React.FC = () => {
|
||||
onClick={handleBack}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
borderColor: 'rgba(255,255,255,0.3)',
|
||||
color: '#fff',
|
||||
background: `color-mix(in srgb, ${token.colorWhite} 20%, transparent)`,
|
||||
borderColor: `color-mix(in srgb, ${token.colorWhite} 30%, transparent)`,
|
||||
color: token.colorWhite,
|
||||
}}
|
||||
>
|
||||
{isMobile ? '返回' : '返回首页'}
|
||||
@@ -1134,8 +1135,8 @@ const Inspiration: React.FC = () => {
|
||||
level={isMobile ? 4 : 2}
|
||||
style={{
|
||||
margin: 0,
|
||||
color: '#fff',
|
||||
textShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
color: token.colorWhite,
|
||||
textShadow: '0 2px 4px color-mix(in srgb, var(--ant-color-black) 18%, transparent)',
|
||||
lineHeight: 1.2
|
||||
}}
|
||||
>
|
||||
@@ -1162,9 +1163,9 @@ const Inspiration: React.FC = () => {
|
||||
}}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
borderColor: 'rgba(255,255,255,0.3)',
|
||||
color: '#fff',
|
||||
background: `color-mix(in srgb, ${token.colorWhite} 20%, transparent)`,
|
||||
borderColor: `color-mix(in srgb, ${token.colorWhite} 30%, transparent)`,
|
||||
color: token.colorWhite,
|
||||
}}
|
||||
>
|
||||
{isMobile ? '重新' : '重新开始'}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Alert,
|
||||
Row,
|
||||
Col,
|
||||
theme,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
@@ -39,7 +40,32 @@ const { TextArea } = Input;
|
||||
export default function MCPPluginsPage() {
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const { token } = theme.useToken();
|
||||
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||
|
||||
const statusStyles = {
|
||||
success: {
|
||||
bg: token.colorSuccessBg,
|
||||
border: token.colorSuccessBorder,
|
||||
text: token.colorSuccessText,
|
||||
},
|
||||
info: {
|
||||
bg: token.colorInfoBg,
|
||||
border: token.colorInfoBorder,
|
||||
text: token.colorInfoText,
|
||||
},
|
||||
warning: {
|
||||
bg: token.colorWarningBg,
|
||||
border: token.colorWarningBorder,
|
||||
text: token.colorWarningText,
|
||||
},
|
||||
error: {
|
||||
bg: token.colorErrorBg,
|
||||
border: token.colorErrorBorder,
|
||||
text: token.colorErrorText,
|
||||
},
|
||||
};
|
||||
|
||||
// 响应式监听窗口大小变化
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
@@ -261,25 +287,25 @@ export default function MCPPluginsPage() {
|
||||
width: isMobile ? '95%' : 700,
|
||||
content: (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
|
||||
<Typography.Text strong style={{ color: 'var(--color-success)', fontSize: 14 }}>
|
||||
<div style={{ marginBottom: 16, padding: 12, background: statusStyles.success.bg, border: `1px solid ${statusStyles.success.border}`, borderRadius: 8 }}>
|
||||
<Typography.Text strong style={{ color: statusStyles.success.text, fontSize: 14 }}>
|
||||
✓ {result.message}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 16 }}>
|
||||
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
|
||||
<div style={{ padding: 12, background: token.colorBgLayout, borderRadius: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>可用工具数</Text>
|
||||
<div><Text strong style={{ fontSize: 20 }}>{result.tools_count || 0}</Text></div>
|
||||
</div>
|
||||
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
|
||||
<div style={{ padding: 12, background: token.colorBgLayout, borderRadius: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>总响应时间</Text>
|
||||
<div><Text strong style={{ fontSize: 20 }}>{result.response_time_ms?.toFixed(0) || 0}ms</Text></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{aiChoice && (
|
||||
<div style={{ marginBottom: 12, padding: 12, background: 'var(--color-info-bg)', borderRadius: 8, border: '1px solid var(--color-info-border)' }}>
|
||||
<div style={{ marginBottom: 12, padding: 12, background: statusStyles.info.bg, borderRadius: 8, border: `1px solid ${statusStyles.info.border}` }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🤖 AI选择的工具</Text>
|
||||
<Text code strong>{aiChoice}</Text>
|
||||
{callTime && <Tag color="blue" style={{ marginLeft: 8 }}>{callTime}</Tag>}
|
||||
@@ -289,7 +315,7 @@ export default function MCPPluginsPage() {
|
||||
{paramsStr && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📝 调用参数</Text>
|
||||
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 12, overflow: 'auto', maxHeight: 100 }}>
|
||||
<pre style={{ margin: 0, padding: 8, background: token.colorBgLayout, borderRadius: 4, fontSize: 12, overflow: 'auto', maxHeight: 100 }}>
|
||||
{(() => { try { return JSON.stringify(JSON.parse(paramsStr), null, 2); } catch { return paramsStr; } })()}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -298,7 +324,7 @@ export default function MCPPluginsPage() {
|
||||
{resultStr && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📊 返回结果预览</Text>
|
||||
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 150, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
<pre style={{ margin: 0, padding: 8, background: token.colorBgLayout, borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 150, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
{resultStr}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -326,13 +352,13 @@ export default function MCPPluginsPage() {
|
||||
{result.error && (
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: 'var(--color-error-bg)',
|
||||
border: '1px solid var(--color-error-border)',
|
||||
background: statusStyles.error.bg,
|
||||
border: `1px solid ${statusStyles.error.border}`,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>错误信息:</Text>
|
||||
<Text style={{ fontSize: 13, color: 'var(--color-error)', fontFamily: 'monospace', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
<Text style={{ fontSize: 13, color: statusStyles.error.text, fontFamily: 'monospace', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
{result.error}
|
||||
</Text>
|
||||
</div>
|
||||
@@ -341,8 +367,8 @@ export default function MCPPluginsPage() {
|
||||
{result.suggestions && result.suggestions.length > 0 && (
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: 'var(--color-warning-bg)',
|
||||
border: '1px solid var(--color-warning-border)',
|
||||
background: statusStyles.warning.bg,
|
||||
border: `1px solid ${statusStyles.warning.border}`,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
@@ -418,24 +444,24 @@ export default function MCPPluginsPage() {
|
||||
width: isMobile ? '95%' : 700,
|
||||
content: (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
|
||||
<Typography.Text strong style={{ color: 'var(--color-success)', fontSize: 14 }}>
|
||||
<div style={{ marginBottom: 16, padding: 12, background: statusStyles.success.bg, border: `1px solid ${statusStyles.success.border}`, borderRadius: 8 }}>
|
||||
<Typography.Text strong style={{ color: statusStyles.success.text, fontSize: 14 }}>
|
||||
✓ {result.message}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 16 }}>
|
||||
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
|
||||
<div style={{ padding: 12, background: token.colorBgLayout, borderRadius: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>API 提供商</Text>
|
||||
<div><Text strong style={{ fontSize: 16 }}>{result.provider}</Text></div>
|
||||
</div>
|
||||
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
|
||||
<div style={{ padding: 12, background: token.colorBgLayout, borderRadius: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>响应时间</Text>
|
||||
<div><Text strong style={{ fontSize: 16 }}>{result.response_time_ms?.toFixed(0) || 0}ms</Text></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12, padding: 12, background: 'var(--color-info-bg)', borderRadius: 8, border: '1px solid var(--color-info-border)' }}>
|
||||
<div style={{ marginBottom: 12, padding: 12, background: statusStyles.info.bg, borderRadius: 8, border: `1px solid ${statusStyles.info.border}` }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🔧 模型信息</Text>
|
||||
<Text code strong>{result.model}</Text>
|
||||
{result.details?.finish_reason && (
|
||||
@@ -446,7 +472,7 @@ export default function MCPPluginsPage() {
|
||||
{result.details && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📊 检测详情</Text>
|
||||
<div style={{ padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 12 }}>
|
||||
<div style={{ padding: 8, background: token.colorBgLayout, borderRadius: 4, fontSize: 12 }}>
|
||||
<div>✓ 工具调用数量: {result.details.tool_call_count || 0}</div>
|
||||
<div>✓ 测试工具: {result.details.test_tool || 'N/A'}</div>
|
||||
<div>✓ 响应类型: {result.details.response_type || 'N/A'}</div>
|
||||
@@ -457,14 +483,14 @@ export default function MCPPluginsPage() {
|
||||
{result.tool_calls && result.tool_calls.length > 0 && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🔨 工具调用示例</Text>
|
||||
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 150 }}>
|
||||
<pre style={{ margin: 0, padding: 8, background: token.colorBgLayout, borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 150 }}>
|
||||
{JSON.stringify(result.tool_calls[0], null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.suggestions && result.suggestions.length > 0 && (
|
||||
<div style={{ padding: 12, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
|
||||
<div style={{ padding: 12, background: statusStyles.success.bg, border: `1px solid ${statusStyles.success.border}`, borderRadius: 8 }}>
|
||||
<Text strong style={{ fontSize: 13, display: 'block', marginBottom: 8 }}>💡 建议</Text>
|
||||
<ul style={{ margin: 0, paddingLeft: 20, fontSize: 12 }}>
|
||||
{result.suggestions.map((s: string, i: number) => (
|
||||
@@ -495,8 +521,8 @@ export default function MCPPluginsPage() {
|
||||
{result.error && (
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: 'var(--color-warning-bg)',
|
||||
border: '1px solid var(--color-warning-border)',
|
||||
background: statusStyles.warning.bg,
|
||||
border: `1px solid ${statusStyles.warning.border}`,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
@@ -510,7 +536,7 @@ export default function MCPPluginsPage() {
|
||||
{result.response_preview && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📝 模型返回内容(前200字符)</Text>
|
||||
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 100, whiteSpace: 'pre-wrap' }}>
|
||||
<pre style={{ margin: 0, padding: 8, background: token.colorBgLayout, borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 100, whiteSpace: 'pre-wrap' }}>
|
||||
{result.response_preview}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -519,8 +545,8 @@ export default function MCPPluginsPage() {
|
||||
{result.suggestions && result.suggestions.length > 0 && (
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: 'var(--color-info-bg)',
|
||||
border: '1px solid var(--color-info-border)',
|
||||
background: statusStyles.info.bg,
|
||||
border: `1px solid ${statusStyles.info.border}`,
|
||||
borderRadius: 8
|
||||
}}>
|
||||
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>💡 建议:</Text>
|
||||
@@ -599,7 +625,7 @@ export default function MCPPluginsPage() {
|
||||
{contextHolder}
|
||||
<div style={{
|
||||
minHeight: '90vh',
|
||||
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)',
|
||||
background: `linear-gradient(180deg, ${token.colorBgLayout} 0%, ${alphaColor(token.colorPrimary, 0.08)} 100%)`,
|
||||
padding: isMobile ? '20px 16px 70px' : '24px 24px 70px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -616,9 +642,9 @@ export default function MCPPluginsPage() {
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, #5A9BA5 50%, var(--color-primary-hover) 100%)',
|
||||
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${alphaColor(token.colorPrimary, 0.8)} 50%, ${token.colorPrimaryHover} 100%)`,
|
||||
borderRadius: isMobile ? 16 : 24,
|
||||
boxShadow: '0 12px 40px rgba(77, 128, 136, 0.25), 0 4px 12px rgba(0, 0, 0, 0.06)',
|
||||
boxShadow: `0 12px 40px ${alphaColor(token.colorPrimary, 0.25)}, 0 4px 12px ${alphaColor(token.colorText, 0.08)}`,
|
||||
marginBottom: isMobile ? 20 : 24,
|
||||
border: 'none',
|
||||
position: 'relative',
|
||||
@@ -626,20 +652,20 @@ export default function MCPPluginsPage() {
|
||||
}}
|
||||
>
|
||||
{/* 装饰性背景元素 */}
|
||||
<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' }} />
|
||||
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: alphaColor(token.colorWhite, 0.08), pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: alphaColor(token.colorWhite, 0.05), pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: alphaColor(token.colorWhite, 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 }} />
|
||||
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: token.colorWhite, textShadow: `0 2px 4px ${alphaColor(token.colorText, 0.2)}` }}>
|
||||
<ToolOutlined style={{ color: alphaColor(token.colorWhite, 0.9), marginRight: 8 }} />
|
||||
MCP插件管理
|
||||
</Title>
|
||||
</Space>
|
||||
<Text style={{ fontSize: isMobile ? 12 : 14, color: 'rgba(255,255,255,0.85)', marginLeft: isMobile ? 40 : 48 }}>
|
||||
<Text style={{ fontSize: isMobile ? 12 : 14, color: alphaColor(token.colorWhite, 0.85), marginLeft: isMobile ? 40 : 48 }}>
|
||||
扩展AI能力,连接外部工具与服务
|
||||
</Text>
|
||||
</Space>
|
||||
@@ -652,10 +678,10 @@ export default function MCPPluginsPage() {
|
||||
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',
|
||||
background: alphaColor(token.colorWarning, 0.95),
|
||||
border: `1px solid ${alphaColor(token.colorWhite, 0.3)}`,
|
||||
boxShadow: `0 4px 16px ${alphaColor(token.colorWarning, 0.4)}`,
|
||||
color: token.colorWhite,
|
||||
fontWeight: 600
|
||||
}}
|
||||
>
|
||||
@@ -671,10 +697,10 @@ export default function MCPPluginsPage() {
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 12,
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.6)',
|
||||
background: alphaColor(token.colorBgContainer, 0.9),
|
||||
border: `1px solid ${alphaColor(token.colorBorder, 0.6)}`,
|
||||
backdropFilter: 'blur(10px)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.03)'
|
||||
boxShadow: `0 4px 12px ${alphaColor(token.colorText, 0.06)}`
|
||||
}}
|
||||
styles={{ body: { padding: isMobile ? 14 : 20 } }}
|
||||
>
|
||||
@@ -690,21 +716,21 @@ export default function MCPPluginsPage() {
|
||||
width: isMobile ? 36 : 40,
|
||||
height: isMobile ? 36 : 40,
|
||||
borderRadius: '50%',
|
||||
background: modelSupportStatus === 'supported' ? 'var(--color-success-bg)' : modelSupportStatus === 'unsupported' ? 'var(--color-error-bg)' : 'var(--color-info-bg)',
|
||||
background: modelSupportStatus === 'supported' ? statusStyles.success.bg : modelSupportStatus === 'unsupported' ? statusStyles.error.bg : statusStyles.info.bg,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
border: `1px solid ${modelSupportStatus === 'supported' ? 'var(--color-success-border)' : modelSupportStatus === 'unsupported' ? 'var(--color-error-border)' : 'var(--color-info-border)'}`,
|
||||
border: `1px solid ${modelSupportStatus === 'supported' ? statusStyles.success.border : modelSupportStatus === 'unsupported' ? statusStyles.error.border : statusStyles.info.border}`,
|
||||
flexShrink: 0
|
||||
}}>
|
||||
{modelSupportStatus === 'supported' ? (
|
||||
<CheckCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: 'var(--color-success)' }} />
|
||||
<CheckCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: statusStyles.success.text }} />
|
||||
) : modelSupportStatus === 'unsupported' ? (
|
||||
<CloseCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: 'var(--color-error)' }} />
|
||||
<CloseCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: statusStyles.error.text }} />
|
||||
) : (
|
||||
<QuestionCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: 'var(--color-info)' }} />
|
||||
<QuestionCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: statusStyles.info.text }} />
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text strong style={{ fontSize: isMobile ? 14 : 16, display: 'block', color: 'var(--color-text-primary)' }}>模型能力检查</Text>
|
||||
<Text strong style={{ fontSize: isMobile ? 14 : 16, display: 'block', color: token.colorText }}>模型能力检查</Text>
|
||||
<Text type="secondary" style={{ fontSize: isMobile ? 12 : 13, display: 'block', lineHeight: 1.5 }}>
|
||||
{modelSupportStatus === 'supported'
|
||||
? '当前模型支持 Function Calling,可正常使用 MCP 插件'
|
||||
@@ -732,18 +758,18 @@ export default function MCPPluginsPage() {
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 12,
|
||||
background: 'rgba(230, 247, 255, 0.6)',
|
||||
border: '1px solid rgba(145, 213, 255, 0.6)',
|
||||
background: alphaColor(token.colorInfoBg, 0.7),
|
||||
border: `1px solid ${alphaColor(token.colorInfoBorder, 0.8)}`,
|
||||
backdropFilter: 'blur(10px)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.03)'
|
||||
boxShadow: `0 4px 12px ${alphaColor(token.colorText, 0.06)}`
|
||||
}}
|
||||
styles={{ body: { padding: isMobile ? 14 : 20 } }}
|
||||
>
|
||||
<Space align="start">
|
||||
<InfoCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: 'var(--color-primary)', marginTop: 2, flexShrink: 0 }} />
|
||||
<InfoCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: token.colorPrimary, marginTop: 2, flexShrink: 0 }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text strong style={{ fontSize: isMobile ? 14 : 16, display: 'block', color: 'var(--color-text-primary)', marginBottom: 4 }}>什么是 MCP 插件?</Text>
|
||||
<Text style={{ fontSize: isMobile ? 12 : 13, display: 'block', color: 'var(--color-text-secondary)', lineHeight: 1.6 }}>
|
||||
<Text strong style={{ fontSize: isMobile ? 14 : 16, display: 'block', color: token.colorText, marginBottom: 4 }}>什么是 MCP 插件?</Text>
|
||||
<Text style={{ fontSize: isMobile ? 12 : 13, display: 'block', color: token.colorTextSecondary, lineHeight: 1.6 }}>
|
||||
MCP (Model Context Protocol) 协议允许 AI 调用外部工具获取数据。通过添加插件,AI 可以访问搜索引擎、数据库、API 等服务,大幅增强创作能力。
|
||||
</Text>
|
||||
</div>
|
||||
@@ -794,7 +820,7 @@ export default function MCPPluginsPage() {
|
||||
size="small"
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
border: '1px solid #f0f0f0',
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
}}
|
||||
styles={{ body: { padding: isMobile ? 12 : 16 } }}
|
||||
>
|
||||
@@ -932,7 +958,7 @@ export default function MCPPluginsPage() {
|
||||
alignItems: 'center',
|
||||
gap: isMobile ? 8 : 8,
|
||||
flexWrap: 'wrap',
|
||||
borderTop: isMobile ? '1px solid #f0f0f0' : 'none',
|
||||
borderTop: isMobile ? `1px solid ${token.colorBorderSecondary}` : 'none',
|
||||
paddingTop: isMobile ? 12 : 0
|
||||
}}>
|
||||
{/* 桌面端显示开关 */}
|
||||
@@ -1055,7 +1081,7 @@ export default function MCPPluginsPage() {
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<ToolOutlined style={{ color: 'var(--color-primary)' }} />
|
||||
<ToolOutlined style={{ color: token.colorPrimary }} />
|
||||
<span>可用工具列表</span>
|
||||
{viewingTools && viewingTools.tools.length > 0 && (
|
||||
<Tag color="blue">{viewingTools.tools.length} 个工具</Tag>
|
||||
@@ -1094,12 +1120,12 @@ export default function MCPPluginsPage() {
|
||||
size="small"
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
border: '1px solid var(--color-border-secondary)',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.05)'
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
boxShadow: `0 2px 4px ${alphaColor(token.colorText, 0.08)}`
|
||||
}}
|
||||
title={
|
||||
<Space>
|
||||
<Text code strong style={{ fontSize: isMobile ? '13px' : '14px', color: 'var(--color-primary)' }}>
|
||||
<Text code strong style={{ fontSize: isMobile ? '13px' : '14px', color: token.colorPrimary }}>
|
||||
{tool.name}
|
||||
</Text>
|
||||
<Tag color="processing" style={{ fontSize: '11px' }}>
|
||||
@@ -1119,9 +1145,9 @@ export default function MCPPluginsPage() {
|
||||
margin: 0,
|
||||
fontSize: isMobile ? '12px' : '13px',
|
||||
padding: '8px 12px',
|
||||
background: 'var(--color-bg-layout)',
|
||||
background: token.colorBgLayout,
|
||||
borderRadius: 4,
|
||||
borderLeft: '3px solid var(--color-info)'
|
||||
borderLeft: `3px solid ${token.colorInfo}`
|
||||
}}
|
||||
>
|
||||
{tool.description}
|
||||
@@ -1137,12 +1163,12 @@ export default function MCPPluginsPage() {
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: isMobile ? '8px' : '12px',
|
||||
background: 'var(--color-bg-layout)',
|
||||
background: token.colorBgLayout,
|
||||
borderRadius: 4,
|
||||
fontSize: isMobile ? '11px' : '12px',
|
||||
overflow: 'auto',
|
||||
maxHeight: '200px',
|
||||
border: '1px solid var(--color-border-secondary)',
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
lineHeight: 1.6
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, InputNumber, Input, Descriptions, Drawer } from 'antd';
|
||||
import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, InputNumber, Input, Descriptions, Drawer, theme } from 'antd';
|
||||
import { PlusOutlined, UserOutlined, EditOutlined, DeleteOutlined, UnorderedListOutlined, BankOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { useCharacterSync } from '../store/hooks';
|
||||
@@ -58,6 +58,7 @@ export default function Organizations() {
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [orgListVisible, setOrgListVisible] = useState(false);
|
||||
const { token } = theme.useToken();
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
@@ -310,7 +311,7 @@ export default function Organizations() {
|
||||
<div style={{
|
||||
padding: '16px 0',
|
||||
marginBottom: 16,
|
||||
borderBottom: '1px solid #f0f0f0'
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`
|
||||
}}>
|
||||
<h2 style={{ margin: 0, fontSize: 24 }}>
|
||||
<BankOutlined style={{ marginRight: 8 }} />
|
||||
@@ -335,7 +336,7 @@ export default function Organizations() {
|
||||
loading={loading}
|
||||
>
|
||||
{organizations.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#999' }}>
|
||||
<div style={{ textAlign: 'center', padding: '40px 20px', color: token.colorTextTertiary }}>
|
||||
暂无组织
|
||||
</div>
|
||||
) : (
|
||||
@@ -347,15 +348,15 @@ export default function Organizations() {
|
||||
hoverable
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
border: selectedOrg?.id === org.id ? '2px solid #1890ff' : '1px solid #d9d9d9',
|
||||
background: selectedOrg?.id === org.id ? '#e6f7ff' : 'transparent'
|
||||
border: selectedOrg?.id === org.id ? `2px solid ${token.colorPrimary}` : `1px solid ${token.colorBorder}`,
|
||||
background: selectedOrg?.id === org.id ? token.colorPrimaryBg : 'transparent'
|
||||
}}
|
||||
onClick={() => handleSelectOrganization(org)}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<strong style={{ fontSize: 14 }}>{org.name}</strong>
|
||||
<Tag color="blue">{org.type}</Tag>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
<div style={{ fontSize: '12px', color: token.colorTextSecondary }}>
|
||||
成员: {org.member_count} | 势力: {org.power_level}
|
||||
</div>
|
||||
</Space>
|
||||
@@ -377,7 +378,7 @@ export default function Organizations() {
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
{organizations.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#999' }}>
|
||||
<div style={{ textAlign: 'center', padding: '40px 20px', color: token.colorTextTertiary }}>
|
||||
暂无组织
|
||||
</div>
|
||||
) : (
|
||||
@@ -389,8 +390,8 @@ export default function Organizations() {
|
||||
hoverable
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
border: selectedOrg?.id === org.id ? '2px solid #1890ff' : '1px solid #d9d9d9',
|
||||
background: selectedOrg?.id === org.id ? '#e6f7ff' : 'transparent'
|
||||
border: selectedOrg?.id === org.id ? `2px solid ${token.colorPrimary}` : `1px solid ${token.colorBorder}`,
|
||||
background: selectedOrg?.id === org.id ? token.colorPrimaryBg : 'transparent'
|
||||
}}
|
||||
onClick={() => {
|
||||
handleSelectOrganization(org);
|
||||
@@ -400,7 +401,7 @@ export default function Organizations() {
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<strong style={{ fontSize: 14 }}>{org.name}</strong>
|
||||
<Tag color="blue">{org.type}</Tag>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
<div style={{ fontSize: '12px', color: token.colorTextSecondary }}>
|
||||
成员: {org.member_count} | 势力: {org.power_level}
|
||||
</div>
|
||||
</Space>
|
||||
@@ -415,7 +416,7 @@ export default function Organizations() {
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
{!selectedOrg ? (
|
||||
<Card style={{ height: '100%' }}>
|
||||
<div style={{ textAlign: 'center', padding: '100px 20px', color: '#999' }}>
|
||||
<div style={{ textAlign: 'center', padding: '100px 20px', color: token.colorTextTertiary }}>
|
||||
{isMobile && organizations.length > 0 && (
|
||||
<Button
|
||||
type="primary"
|
||||
|
||||
+142
-131
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Button, List, Modal, Form, Input, message, Empty, Space, Popconfirm, Card, Select, Radio, Tag, InputNumber, Tabs, Pagination } from 'antd';
|
||||
import { Button, List, Modal, Form, Input, message, Empty, Space, Popconfirm, Card, Select, Radio, Tag, InputNumber, Tabs, Pagination, theme } from 'antd';
|
||||
import { EditOutlined, DeleteOutlined, ThunderboltOutlined, BranchesOutlined, AppstoreAddOutlined, CheckCircleOutlined, ExclamationCircleOutlined, PlusOutlined, FileTextOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { useOutlineSync } from '../store/hooks';
|
||||
@@ -119,6 +119,9 @@ export default function Outline() {
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||
const [isExpanding, setIsExpanding] = useState(false);
|
||||
const [projectCharacters, setProjectCharacters] = useState<Array<{ label: string; value: string }>>([]);
|
||||
const { token } = theme.useToken();
|
||||
const alphaColor = (color: string, alpha: number) =>
|
||||
`color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||
|
||||
// ✅ 新增:记录场景区域的展开/折叠状态
|
||||
const [scenesExpandStatus, setScenesExpandStatus] = useState<Record<string, boolean>>({});
|
||||
@@ -776,7 +779,7 @@ export default function Outline() {
|
||||
console.log('已同步到Form,当前Form值:', generateForm.getFieldsValue());
|
||||
}}
|
||||
/>
|
||||
<div style={{ color: 'var(--color-text-tertiary)', fontSize: 12, marginTop: 4 }}>
|
||||
<div style={{ color: token.colorTextTertiary, fontSize: 12, marginTop: 4 }}>
|
||||
{defaultModel ? `当前默认模型: ${loadedModels.find(m => m.value === defaultModel)?.label || defaultModel}` : '未配置默认模型'}
|
||||
</div>
|
||||
</Form.Item>
|
||||
@@ -853,19 +856,19 @@ export default function Outline() {
|
||||
<p>序号 <strong>{values.order_index}</strong> 已被使用:</p>
|
||||
<div style={{
|
||||
padding: 12,
|
||||
background: 'var(--color-warning-bg)',
|
||||
borderRadius: 4,
|
||||
border: '1px solid var(--color-warning-border)',
|
||||
background: token.colorWarningBg,
|
||||
borderRadius: token.borderRadius,
|
||||
border: `1px solid ${token.colorWarningBorder}`,
|
||||
marginTop: 8
|
||||
}}>
|
||||
<div style={{ fontWeight: 500, color: 'var(--color-warning)' }}>
|
||||
<div style={{ fontWeight: 500, color: token.colorWarning }}>
|
||||
{currentProject?.outline_mode === 'one-to-one'
|
||||
? `第${existingOutline.order_index}章`
|
||||
: `第${existingOutline.order_index}卷`
|
||||
}:{existingOutline.title}
|
||||
</div>
|
||||
</div>
|
||||
<p style={{ marginTop: 12, color: 'var(--color-text-secondary)' }}>
|
||||
<p style={{ marginTop: 12, color: token.colorTextSecondary }}>
|
||||
💡 建议使用序号 <strong>{nextOrderIndex}</strong>,或选择其他未使用的序号
|
||||
</p>
|
||||
</div>
|
||||
@@ -928,18 +931,18 @@ export default function Outline() {
|
||||
</p>
|
||||
<div style={{
|
||||
padding: 12,
|
||||
background: 'var(--color-warning-bg)',
|
||||
borderRadius: 4,
|
||||
border: '1px solid var(--color-warning-border)'
|
||||
background: token.colorWarningBg,
|
||||
borderRadius: token.borderRadius,
|
||||
border: `1px solid ${token.colorWarningBorder}`
|
||||
}}>
|
||||
<div style={{ fontWeight: 500, marginBottom: 8, color: 'var(--color-warning)' }}>
|
||||
<div style={{ fontWeight: 500, marginBottom: 8, color: token.colorWarning }}>
|
||||
⚠️ 需要先展开:
|
||||
</div>
|
||||
<div style={{ color: 'var(--color-text-secondary)' }}>
|
||||
<div style={{ color: token.colorTextSecondary }}>
|
||||
第{prevOutline.order_index}卷:《{prevOutline.title}》
|
||||
</div>
|
||||
</div>
|
||||
<p style={{ marginTop: 12, color: 'var(--color-text-secondary)', fontSize: 13 }}>
|
||||
<p style={{ marginTop: 12, color: token.colorTextSecondary, fontSize: 13 }}>
|
||||
💡 提示:您也可以使用「批量展开」功能,系统会自动按顺序处理所有大纲。
|
||||
</p>
|
||||
</div>
|
||||
@@ -978,9 +981,9 @@ export default function Outline() {
|
||||
centered: true,
|
||||
content: (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-bg-layout)', borderRadius: 4 }}>
|
||||
<div style={{ marginBottom: 16, padding: 12, background: token.colorBgLayout, borderRadius: token.borderRadius }}>
|
||||
<div style={{ fontWeight: 500, marginBottom: 4 }}>大纲标题</div>
|
||||
<div style={{ color: 'var(--color-text-secondary)' }}>{outlineTitle}</div>
|
||||
<div style={{ color: token.colorTextSecondary }}>{outlineTitle}</div>
|
||||
</div>
|
||||
<Form
|
||||
form={expansionForm}
|
||||
@@ -1132,7 +1135,7 @@ export default function Outline() {
|
||||
modalApi.info({
|
||||
title: (
|
||||
<Space style={{ flexWrap: 'wrap' }}>
|
||||
<CheckCircleOutlined style={{ color: 'var(--color-success)' }} />
|
||||
<CheckCircleOutlined style={{ color: token.colorSuccess }} />
|
||||
<span>《{outlineTitle}》展开信息</span>
|
||||
</Space>
|
||||
),
|
||||
@@ -1164,10 +1167,10 @@ export default function Outline() {
|
||||
content: (
|
||||
<div>
|
||||
<p>此操作将删除大纲《{outlineTitle}》展开的所有 <strong>{data.chapter_count}</strong> 个章节。</p>
|
||||
<p style={{ color: 'var(--color-primary)', marginTop: 8 }}>
|
||||
<p style={{ color: token.colorPrimary, marginTop: 8 }}>
|
||||
📝 注意:大纲本身会保留,您可以重新展开
|
||||
</p>
|
||||
<p style={{ color: '#ff4d4f', marginTop: 8 }}>
|
||||
<p style={{ color: token.colorError, marginTop: 8 }}>
|
||||
⚠️ 警告:章节内容将永久删除且无法恢复!
|
||||
</p>
|
||||
</div>
|
||||
@@ -1324,7 +1327,7 @@ export default function Outline() {
|
||||
key={sceneIdx}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: '#fafafa',
|
||||
backgroundColor: token.colorFillQuaternary,
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
@@ -1374,7 +1377,7 @@ export default function Outline() {
|
||||
modalApi.confirm({
|
||||
title: (
|
||||
<Space>
|
||||
<CheckCircleOutlined style={{ color: 'var(--color-success)' }} />
|
||||
<CheckCircleOutlined style={{ color: token.colorSuccess }} />
|
||||
<span>展开规划预览</span>
|
||||
</Space>
|
||||
),
|
||||
@@ -1438,7 +1441,7 @@ export default function Outline() {
|
||||
<Card size="small" title="场景">
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
{plan.scenes.map((scene, sceneIdx) => (
|
||||
<Card key={sceneIdx} size="small" style={{ backgroundColor: '#fafafa' }}>
|
||||
<Card key={sceneIdx} size="small" style={{ backgroundColor: token.colorFillQuaternary }}>
|
||||
<div><strong>地点:</strong>{scene.location}</div>
|
||||
<div><strong>角色:</strong>{scene.characters.join('、')}</div>
|
||||
<div><strong>目的:</strong>{scene.purpose}</div>
|
||||
@@ -1511,8 +1514,16 @@ export default function Outline() {
|
||||
centered: true,
|
||||
content: (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-warning-bg)', borderRadius: 4 }}>
|
||||
<div style={{ color: '#856404' }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
padding: 12,
|
||||
background: token.colorWarningBg,
|
||||
borderRadius: token.borderRadius,
|
||||
border: `1px solid ${token.colorWarningBorder}`,
|
||||
}}
|
||||
>
|
||||
<div style={{ color: token.colorWarningText }}>
|
||||
⚠️ 将对当前项目的所有 {outlines.length} 个大纲进行展开
|
||||
</div>
|
||||
</div>
|
||||
@@ -1639,16 +1650,16 @@ export default function Outline() {
|
||||
<div style={{
|
||||
marginBottom: 16,
|
||||
padding: 12,
|
||||
background: 'var(--color-warning-bg)',
|
||||
borderRadius: 4,
|
||||
border: '1px solid #ffe58f'
|
||||
background: token.colorWarningBg,
|
||||
borderRadius: token.borderRadius,
|
||||
border: `1px solid ${token.colorWarningBorder}`
|
||||
}}>
|
||||
<div style={{ fontWeight: 500, marginBottom: 8, color: 'var(--color-warning)' }}>
|
||||
<div style={{ fontWeight: 500, marginBottom: 8, color: token.colorWarning }}>
|
||||
⚠️ 以下大纲已展开过,已自动跳过:
|
||||
</div>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
{batchPreviewData.skipped_outlines.map((skipped: SkippedOutlineInfo, idx: number) => (
|
||||
<div key={idx} style={{ fontSize: 13, color: '#666' }}>
|
||||
<div key={idx} style={{ fontSize: 13, color: token.colorTextSecondary }}>
|
||||
• {skipped.outline_title} <Tag color="default" style={{ fontSize: 11 }}>{skipped.reason}</Tag>
|
||||
</div>
|
||||
))}
|
||||
@@ -1661,11 +1672,11 @@ export default function Outline() {
|
||||
{/* 左栏:大纲列表 */}
|
||||
<div style={{
|
||||
width: 280,
|
||||
borderRight: '1px solid #f0f0f0',
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||
paddingRight: 12,
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
<div style={{ fontWeight: 500, marginBottom: 8, color: '#666' }}>大纲列表</div>
|
||||
<div style={{ fontWeight: 500, marginBottom: 8, color: token.colorTextSecondary }}>大纲列表</div>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={batchPreviewData.expansion_results}
|
||||
@@ -1679,10 +1690,10 @@ export default function Outline() {
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '8px 12px',
|
||||
background: selectedOutlineIdx === idx ? '#e6f7ff' : 'transparent',
|
||||
borderRadius: 4,
|
||||
background: selectedOutlineIdx === idx ? token.colorPrimaryBg : 'transparent',
|
||||
borderRadius: token.borderRadius,
|
||||
marginBottom: 4,
|
||||
border: selectedOutlineIdx === idx ? '1px solid var(--color-primary)' : '1px solid transparent'
|
||||
border: selectedOutlineIdx === idx ? `1px solid ${token.colorPrimary}` : '1px solid transparent'
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
@@ -1702,11 +1713,11 @@ export default function Outline() {
|
||||
{/* 中栏:章节列表 */}
|
||||
<div style={{
|
||||
width: 320,
|
||||
borderRight: '1px solid #f0f0f0',
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||
paddingRight: 12,
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
<div style={{ fontWeight: 500, marginBottom: 8, color: '#666' }}>
|
||||
<div style={{ fontWeight: 500, marginBottom: 8, color: token.colorTextSecondary }}>
|
||||
章节列表 ({batchPreviewData.expansion_results[selectedOutlineIdx]?.actual_chapter_count || 0} 章)
|
||||
</div>
|
||||
{batchPreviewData.expansion_results[selectedOutlineIdx] && (
|
||||
@@ -1720,10 +1731,10 @@ export default function Outline() {
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '8px 12px',
|
||||
background: selectedChapterIdx === idx ? '#e6f7ff' : 'transparent',
|
||||
borderRadius: 4,
|
||||
background: selectedChapterIdx === idx ? token.colorPrimaryBg : 'transparent',
|
||||
borderRadius: token.borderRadius,
|
||||
marginBottom: 4,
|
||||
border: selectedChapterIdx === idx ? '1px solid var(--color-primary)' : '1px solid transparent'
|
||||
border: selectedChapterIdx === idx ? `1px solid ${token.colorPrimary}` : '1px solid transparent'
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
@@ -1744,7 +1755,7 @@ export default function Outline() {
|
||||
|
||||
{/* 右栏:章节详情 */}
|
||||
<div style={{ flex: 1, overflowY: 'auto', paddingLeft: 12 }}>
|
||||
<div style={{ fontWeight: 500, marginBottom: 12, color: '#666' }}>章节详情</div>
|
||||
<div style={{ fontWeight: 500, marginBottom: 12, color: token.colorTextSecondary }}>章节详情</div>
|
||||
{batchPreviewData.expansion_results[selectedOutlineIdx]?.chapter_plans[selectedChapterIdx] ? (
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Card size="small" title="情节概要" bordered={false}>
|
||||
@@ -1775,7 +1786,7 @@ export default function Outline() {
|
||||
<Card size="small" title="场景" bordered={false}>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
{batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].scenes!.map((scene: SceneInfo, sceneIdx: number) => (
|
||||
<Card key={sceneIdx} size="small" style={{ backgroundColor: '#fafafa' }}>
|
||||
<Card key={sceneIdx} size="small" style={{ backgroundColor: token.colorFillQuaternary }}>
|
||||
<div><strong>地点:</strong>{scene.location}</div>
|
||||
<div><strong>角色:</strong>{scene.characters.join('、')}</div>
|
||||
<div><strong>目的:</strong>{scene.purpose}</div>
|
||||
@@ -1876,7 +1887,7 @@ export default function Outline() {
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<CheckCircleOutlined style={{ color: 'var(--color-success)' }} />
|
||||
<CheckCircleOutlined style={{ color: token.colorSuccess }} />
|
||||
<span>批量展开规划预览</span>
|
||||
</Space>
|
||||
}
|
||||
@@ -1907,10 +1918,10 @@ export default function Outline() {
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: 'var(--color-bg-container)',
|
||||
backgroundColor: token.colorBgContainer,
|
||||
padding: isMobile ? '12px 0' : '16px 0',
|
||||
marginBottom: isMobile ? 12 : 16,
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
display: 'flex',
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
gap: isMobile ? 12 : 0,
|
||||
@@ -1995,8 +2006,8 @@ export default function Outline() {
|
||||
style={{
|
||||
width: '100%',
|
||||
borderRadius: isMobile ? 6 : 8,
|
||||
border: '1px solid #f0f0f0',
|
||||
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.03)',
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
boxShadow: `0 1px 2px ${alphaColor(token.colorTextBase, 0.08)}`,
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
bodyStyle={{
|
||||
@@ -2004,14 +2015,14 @@ export default function Outline() {
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isMobile) {
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.08)';
|
||||
e.currentTarget.style.borderColor = 'var(--color-primary)';
|
||||
e.currentTarget.style.boxShadow = `0 4px 12px ${alphaColor(token.colorTextBase, 0.16)}`;
|
||||
e.currentTarget.style.borderColor = token.colorPrimary;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isMobile) {
|
||||
e.currentTarget.style.boxShadow = '0 1px 2px rgba(0, 0, 0, 0.03)';
|
||||
e.currentTarget.style.borderColor = '#f0f0f0';
|
||||
e.currentTarget.style.boxShadow = `0 1px 2px ${alphaColor(token.colorTextBase, 0.08)}`;
|
||||
e.currentTarget.style.borderColor = token.colorBorderSecondary;
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -2019,7 +2030,7 @@ export default function Outline() {
|
||||
style={{ width: '100%' }}
|
||||
title={
|
||||
<Space size="small" style={{ fontSize: isMobile ? 13 : 16, flexWrap: 'wrap', lineHeight: isMobile ? '1.4' : '1.5' }}>
|
||||
<span style={{ color: 'var(--color-primary)', fontWeight: 'bold', fontSize: isMobile ? 13 : 16 }}>
|
||||
<span style={{ color: token.colorPrimary, fontWeight: 'bold', fontSize: isMobile ? 13 : 16 }}>
|
||||
{currentProject?.outline_mode === 'one-to-one'
|
||||
? `第${item.order_index || '?'}章`
|
||||
: `第${item.order_index || '?'}卷`
|
||||
@@ -2042,16 +2053,16 @@ export default function Outline() {
|
||||
<div style={{
|
||||
marginBottom: isMobile ? 10 : 12,
|
||||
padding: isMobile ? '8px 10px' : '10px 12px',
|
||||
background: 'linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%)',
|
||||
borderLeft: '3px solid #8c8c8c',
|
||||
borderRadius: isMobile ? 4 : 6,
|
||||
background: token.colorFillQuaternary,
|
||||
borderLeft: `3px solid ${token.colorBorderSecondary}`,
|
||||
borderRadius: token.borderRadius,
|
||||
fontSize: isMobile ? 12 : 13,
|
||||
color: '#262626',
|
||||
color: token.colorText,
|
||||
lineHeight: '1.6'
|
||||
}}>
|
||||
<div style={{
|
||||
fontWeight: 600,
|
||||
color: '#595959',
|
||||
color: token.colorTextSecondary,
|
||||
marginBottom: isMobile ? 4 : 6,
|
||||
fontSize: isMobile ? 12 : 13
|
||||
}}>
|
||||
@@ -2059,11 +2070,11 @@ export default function Outline() {
|
||||
</div>
|
||||
<div style={{
|
||||
padding: isMobile ? '6px 8px' : '6px 10px',
|
||||
background: '#ffffff',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 4,
|
||||
background: token.colorBgContainer,
|
||||
border: `1px solid ${token.colorBorder}`,
|
||||
borderRadius: token.borderRadiusSM,
|
||||
fontSize: isMobile ? 12 : 13,
|
||||
color: '#262626',
|
||||
color: token.colorText,
|
||||
lineHeight: '1.6'
|
||||
}}>
|
||||
{item.content}
|
||||
@@ -2075,9 +2086,9 @@ export default function Outline() {
|
||||
<div style={{
|
||||
marginTop: isMobile ? 10 : 12,
|
||||
padding: isMobile ? '8px 10px' : '10px 12px',
|
||||
background: 'linear-gradient(135deg, #f5f3ff 0%, #faf5ff 100%)',
|
||||
borderLeft: '3px solid #9333ea',
|
||||
borderRadius: isMobile ? 4 : 6
|
||||
background: token.colorPrimaryBg,
|
||||
borderLeft: `3px solid ${token.colorPrimary}`,
|
||||
borderRadius: token.borderRadius
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
@@ -2088,7 +2099,7 @@ export default function Outline() {
|
||||
<span style={{
|
||||
fontSize: isMobile ? 12 : 13,
|
||||
fontWeight: 600,
|
||||
color: '#7c3aed',
|
||||
color: token.colorPrimary,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4
|
||||
@@ -2118,9 +2129,9 @@ export default function Outline() {
|
||||
padding: isMobile ? '2px 8px' : '3px 10px',
|
||||
fontSize: isMobile ? 11 : 12,
|
||||
fontWeight: 500,
|
||||
border: '1px solid #e9d5ff',
|
||||
background: '#ffffff',
|
||||
color: '#7c3aed',
|
||||
border: `1px solid ${token.colorPrimaryBorder}`,
|
||||
background: token.colorBgContainer,
|
||||
color: token.colorPrimary,
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word',
|
||||
height: 'auto',
|
||||
@@ -2139,9 +2150,9 @@ export default function Outline() {
|
||||
<div style={{
|
||||
marginTop: isMobile ? 10 : 12,
|
||||
padding: isMobile ? '8px 10px' : '10px 12px',
|
||||
background: 'linear-gradient(135deg, #fff7ed 0%, #fffbeb 100%)',
|
||||
borderLeft: '3px solid #ea580c',
|
||||
borderRadius: isMobile ? 4 : 6
|
||||
background: token.colorWarningBg,
|
||||
borderLeft: `3px solid ${token.colorWarning}`,
|
||||
borderRadius: token.borderRadius
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
@@ -2152,7 +2163,7 @@ export default function Outline() {
|
||||
<span style={{
|
||||
fontSize: isMobile ? 12 : 13,
|
||||
fontWeight: 600,
|
||||
color: '#ea580c',
|
||||
color: token.colorWarning,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4
|
||||
@@ -2182,9 +2193,9 @@ export default function Outline() {
|
||||
padding: isMobile ? '2px 8px' : '3px 10px',
|
||||
fontSize: isMobile ? 11 : 12,
|
||||
fontWeight: 500,
|
||||
border: '1px solid #fed7aa',
|
||||
background: '#ffffff',
|
||||
color: '#ea580c',
|
||||
border: `1px solid ${token.colorWarningBorder}`,
|
||||
background: token.colorBgContainer,
|
||||
color: token.colorWarning,
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word',
|
||||
height: 'auto',
|
||||
@@ -2209,9 +2220,9 @@ export default function Outline() {
|
||||
<div style={{
|
||||
marginTop: isMobile ? 10 : 12,
|
||||
padding: isMobile ? '8px 10px' : '10px 12px',
|
||||
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
|
||||
borderLeft: '3px solid #0ea5e9',
|
||||
borderRadius: isMobile ? 4 : 6
|
||||
background: token.colorInfoBg,
|
||||
borderLeft: `3px solid ${token.colorInfo}`,
|
||||
borderRadius: token.borderRadius
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
@@ -2224,7 +2235,7 @@ export default function Outline() {
|
||||
<span style={{
|
||||
fontSize: isMobile ? 12 : 13,
|
||||
fontWeight: 600,
|
||||
color: '#0284c7',
|
||||
color: token.colorInfo,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4
|
||||
@@ -2254,7 +2265,7 @@ export default function Outline() {
|
||||
fontSize: isMobile ? 10 : 11,
|
||||
height: isMobile ? 20 : 22,
|
||||
padding: isMobile ? '0 6px' : '0 8px',
|
||||
color: '#0284c7'
|
||||
color: token.colorInfo
|
||||
}}
|
||||
>
|
||||
{isExpanded ? '收起 ▲' : `展开 (${structureData.scenes!.length - maxVisibleScenes}+) ▼`}
|
||||
@@ -2278,11 +2289,11 @@ export default function Outline() {
|
||||
key={idx}
|
||||
style={{
|
||||
padding: isMobile ? '6px 8px' : '8px 10px',
|
||||
background: '#ffffff',
|
||||
border: '1px solid #bae6fd',
|
||||
borderRadius: isMobile ? 4 : 6,
|
||||
background: token.colorBgContainer,
|
||||
border: `1px solid ${token.colorInfoBorder}`,
|
||||
borderRadius: token.borderRadius,
|
||||
fontSize: isMobile ? 11 : 12,
|
||||
color: '#0c4a6e',
|
||||
color: token.colorText,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: isMobile ? 6 : 8,
|
||||
@@ -2294,13 +2305,13 @@ export default function Outline() {
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isMobile) {
|
||||
e.currentTarget.style.borderColor = '#0ea5e9';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.15)';
|
||||
e.currentTarget.style.borderColor = token.colorInfo;
|
||||
e.currentTarget.style.boxShadow = `0 2px 8px ${alphaColor(token.colorInfo, 0.25)}`;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isMobile) {
|
||||
e.currentTarget.style.borderColor = '#bae6fd';
|
||||
e.currentTarget.style.borderColor = token.colorInfoBorder;
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}
|
||||
}}
|
||||
@@ -2332,9 +2343,9 @@ export default function Outline() {
|
||||
key={idx}
|
||||
style={{
|
||||
padding: isMobile ? '8px 10px' : '10px 12px',
|
||||
background: '#ffffff',
|
||||
border: '1px solid #bae6fd',
|
||||
borderRadius: isMobile ? 4 : 6,
|
||||
background: token.colorBgContainer,
|
||||
border: `1px solid ${token.colorInfoBorder}`,
|
||||
borderRadius: token.borderRadius,
|
||||
fontSize: isMobile ? 11 : 12,
|
||||
transition: 'all 0.2s ease',
|
||||
cursor: 'default',
|
||||
@@ -2344,13 +2355,13 @@ export default function Outline() {
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isMobile) {
|
||||
e.currentTarget.style.borderColor = '#0ea5e9';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.15)';
|
||||
e.currentTarget.style.borderColor = token.colorInfo;
|
||||
e.currentTarget.style.boxShadow = `0 2px 8px ${alphaColor(token.colorInfo, 0.25)}`;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isMobile) {
|
||||
e.currentTarget.style.borderColor = '#bae6fd';
|
||||
e.currentTarget.style.borderColor = token.colorInfoBorder;
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}
|
||||
}}
|
||||
@@ -2374,7 +2385,7 @@ export default function Outline() {
|
||||
</Tag>
|
||||
<span style={{
|
||||
fontWeight: 600,
|
||||
color: '#0c4a6e',
|
||||
color: token.colorText,
|
||||
fontSize: isMobile ? 12 : 13,
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
@@ -2387,7 +2398,7 @@ export default function Outline() {
|
||||
{scene.characters && scene.characters.length > 0 && (
|
||||
<div style={{
|
||||
fontSize: isMobile ? 10 : 11,
|
||||
color: '#64748b',
|
||||
color: token.colorTextSecondary,
|
||||
marginBottom: 4,
|
||||
paddingLeft: isMobile ? 2 : 4,
|
||||
overflow: 'hidden',
|
||||
@@ -2401,7 +2412,7 @@ export default function Outline() {
|
||||
{scene.purpose && (
|
||||
<div style={{
|
||||
fontSize: isMobile ? 10 : 11,
|
||||
color: '#64748b',
|
||||
color: token.colorTextSecondary,
|
||||
paddingLeft: isMobile ? 2 : 4,
|
||||
lineHeight: '1.5',
|
||||
overflow: 'hidden',
|
||||
@@ -2426,9 +2437,9 @@ export default function Outline() {
|
||||
<div style={{
|
||||
marginTop: 12,
|
||||
padding: '10px 12px',
|
||||
background: 'linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%)',
|
||||
borderLeft: '3px solid #f97316',
|
||||
borderRadius: 6
|
||||
background: token.colorWarningBg,
|
||||
borderLeft: `3px solid ${token.colorWarning}`,
|
||||
borderRadius: token.borderRadius
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
@@ -2439,7 +2450,7 @@ export default function Outline() {
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#ea580c',
|
||||
color: token.colorWarning,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4
|
||||
@@ -2464,11 +2475,11 @@ export default function Outline() {
|
||||
key={idx}
|
||||
style={{
|
||||
padding: '6px 10px',
|
||||
background: '#ffffff',
|
||||
border: '1px solid #fed7aa',
|
||||
borderRadius: 4,
|
||||
background: token.colorBgContainer,
|
||||
border: `1px solid ${token.colorWarningBorder}`,
|
||||
borderRadius: token.borderRadiusSM,
|
||||
fontSize: 12,
|
||||
color: '#9a3412',
|
||||
color: token.colorWarningText,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: 8
|
||||
@@ -2503,9 +2514,9 @@ export default function Outline() {
|
||||
<div style={{
|
||||
marginTop: 12,
|
||||
padding: '10px 12px',
|
||||
background: 'linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%)',
|
||||
borderLeft: '3px solid #22c55e',
|
||||
borderRadius: 6
|
||||
background: token.colorSuccessBg,
|
||||
borderLeft: `3px solid ${token.colorSuccess}`,
|
||||
borderRadius: token.borderRadius
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
@@ -2516,7 +2527,7 @@ export default function Outline() {
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#15803d',
|
||||
color: token.colorSuccess,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4
|
||||
@@ -2548,11 +2559,11 @@ export default function Outline() {
|
||||
key={idx}
|
||||
style={{
|
||||
padding: isMobile ? '6px 8px' : '8px 10px',
|
||||
background: '#ffffff',
|
||||
border: '1px solid #bbf7d0',
|
||||
borderRadius: isMobile ? 4 : 6,
|
||||
background: token.colorBgContainer,
|
||||
border: `1px solid ${token.colorSuccessBorder}`,
|
||||
borderRadius: token.borderRadius,
|
||||
fontSize: isMobile ? 11 : 12,
|
||||
color: '#166534',
|
||||
color: token.colorText,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: isMobile ? 6 : 8,
|
||||
@@ -2564,13 +2575,13 @@ export default function Outline() {
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isMobile) {
|
||||
e.currentTarget.style.borderColor = '#22c55e';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(34, 197, 94, 0.15)';
|
||||
e.currentTarget.style.borderColor = token.colorSuccess;
|
||||
e.currentTarget.style.boxShadow = `0 2px 8px ${alphaColor(token.colorSuccess, 0.25)}`;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isMobile) {
|
||||
e.currentTarget.style.borderColor = '#bbf7d0';
|
||||
e.currentTarget.style.borderColor = token.colorSuccessBorder;
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}
|
||||
}}
|
||||
@@ -2604,9 +2615,9 @@ export default function Outline() {
|
||||
<div style={{
|
||||
marginTop: 12,
|
||||
padding: '10px 12px',
|
||||
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
|
||||
borderLeft: '3px solid #f59e0b',
|
||||
borderRadius: 6,
|
||||
background: token.colorWarningBg,
|
||||
borderLeft: `3px solid ${token.colorWarning}`,
|
||||
borderRadius: token.borderRadius,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8
|
||||
@@ -2614,7 +2625,7 @@ export default function Outline() {
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#b45309'
|
||||
color: token.colorWarning
|
||||
}}>
|
||||
💫 情感基调:
|
||||
</span>
|
||||
@@ -2625,9 +2636,9 @@ export default function Outline() {
|
||||
fontSize: 12,
|
||||
padding: '2px 12px',
|
||||
borderRadius: 12,
|
||||
background: '#ffffff',
|
||||
border: '1px solid #fbbf24',
|
||||
color: '#b45309'
|
||||
background: token.colorBgContainer,
|
||||
border: `1px solid ${token.colorWarningBorder}`,
|
||||
color: token.colorWarningText
|
||||
}}
|
||||
>
|
||||
{structureData.emotion}
|
||||
@@ -2640,26 +2651,26 @@ export default function Outline() {
|
||||
<div style={{
|
||||
marginTop: 12,
|
||||
padding: '10px 12px',
|
||||
background: 'linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)',
|
||||
borderLeft: '3px solid #3b82f6',
|
||||
borderRadius: 6
|
||||
background: token.colorInfoBg,
|
||||
borderLeft: `3px solid ${token.colorInfo}`,
|
||||
borderRadius: token.borderRadius
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#1e40af',
|
||||
color: token.colorInfo,
|
||||
marginBottom: 6
|
||||
}}>
|
||||
🎯 叙事目标
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 12,
|
||||
color: '#1e3a8a',
|
||||
color: token.colorText,
|
||||
lineHeight: '1.6',
|
||||
padding: '6px 10px',
|
||||
background: '#ffffff',
|
||||
border: '1px solid #93c5fd',
|
||||
borderRadius: 4,
|
||||
background: token.colorBgContainer,
|
||||
border: `1px solid ${token.colorInfoBorder}`,
|
||||
borderRadius: token.borderRadiusSM,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
@@ -2676,7 +2687,7 @@ export default function Outline() {
|
||||
<div style={{
|
||||
marginTop: 16,
|
||||
paddingTop: 12,
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 8
|
||||
@@ -2729,8 +2740,8 @@ export default function Outline() {
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: 'var(--color-bg-container)',
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
backgroundColor: token.colorBgContainer,
|
||||
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||||
padding: isMobile ? '8px 0' : '10px 0',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end'
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Card, Input, Button, message, Space } from 'antd';
|
||||
import { ThunderboltOutlined } from '@ant-design/icons';
|
||||
import { polishApi } from '../services/api';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
export default function Polish() {
|
||||
const [originalText, setOriginalText] = useState('');
|
||||
const [polishedText, setPolishedText] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handlePolish = async () => {
|
||||
if (!originalText.trim()) {
|
||||
message.warning('请输入要去味的文本');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await polishApi.polishText({ text: originalText });
|
||||
setPolishedText(result.polished_text);
|
||||
message.success('AI去味完成');
|
||||
} catch {
|
||||
message.error('AI去味失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(polishedText);
|
||||
message.success('已复制到剪贴板');
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ marginBottom: 16 }}>AI去味工具</h2>
|
||||
<p style={{ color: 'rgba(0,0,0,0.45)', marginBottom: 24 }}>
|
||||
将AI生成的文本变得更自然、更像人类作家的手笔
|
||||
</p>
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||
<Card title="原始文本" extra={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={handlePolish}
|
||||
loading={loading}
|
||||
>
|
||||
开始去味
|
||||
</Button>
|
||||
}>
|
||||
<TextArea
|
||||
rows={10}
|
||||
placeholder="粘贴或输入需要去味的文本..."
|
||||
value={originalText}
|
||||
onChange={(e) => setOriginalText(e.target.value)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{polishedText && (
|
||||
<Card title="去味后文本" extra={
|
||||
<Button onClick={handleCopy}>复制文本</Button>
|
||||
}>
|
||||
<TextArea
|
||||
rows={10}
|
||||
value={polishedText}
|
||||
readOnly
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useParams, useNavigate, Outlet, Link, useLocation } from 'react-router-dom';
|
||||
import { Layout, Menu, Spin, Button, Drawer } from 'antd';
|
||||
import { Layout, Menu, Spin, Button, Drawer, theme } from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
FileTextOutlined,
|
||||
@@ -18,10 +18,14 @@ import {
|
||||
TrophyOutlined,
|
||||
BulbOutlined,
|
||||
CloudOutlined,
|
||||
MoonOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { useCharacterSync, useOutlineSync, useChapterSync } from '../store/hooks';
|
||||
import { projectApi } from '../services/api';
|
||||
import ThemeSwitch from '../components/ThemeSwitch';
|
||||
import { useThemeMode } from '../theme/useThemeMode';
|
||||
import { getStoredSidebarCollapsed, setStoredSidebarCollapsed } from '../utils/sidebarState';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
|
||||
@@ -32,9 +36,17 @@ export default function ProjectDetail() {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [collapsed, setCollapsed] = useState<boolean>(() => getStoredSidebarCollapsed());
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
const [mobile, setMobile] = useState(isMobile());
|
||||
const { token } = theme.useToken();
|
||||
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||
const { mode, resolvedMode, setMode } = useThemeMode();
|
||||
const cycleThemeMode = () => {
|
||||
const nextMode = mode === 'light' ? 'dark' : mode === 'dark' ? 'system' : 'light';
|
||||
setMode(nextMode);
|
||||
};
|
||||
const collapsedThemeIcon = mode === 'light' ? <BulbOutlined /> : mode === 'dark' ? <MoonOutlined /> : <CloudOutlined />;
|
||||
|
||||
// 监听窗口大小变化
|
||||
useEffect(() => {
|
||||
@@ -47,6 +59,10 @@ export default function ProjectDetail() {
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setStoredSidebarCollapsed(collapsed);
|
||||
}, [collapsed]);
|
||||
const {
|
||||
currentProject,
|
||||
setCurrentProject,
|
||||
@@ -97,6 +113,81 @@ export default function ProjectDetail() {
|
||||
// Hook 内部已经更新了 store,不需要再次刷新
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: 'sponsor',
|
||||
icon: <HeartOutlined />,
|
||||
label: <Link to={`/project/${projectId}/sponsor`}>赞助支持</Link>,
|
||||
},
|
||||
{
|
||||
type: 'group' as const,
|
||||
label: '创作管理',
|
||||
children: [
|
||||
{
|
||||
key: 'world-setting',
|
||||
icon: <GlobalOutlined />,
|
||||
label: <Link to={`/project/${projectId}/world-setting`}>世界设定</Link>,
|
||||
},
|
||||
{
|
||||
key: 'characters',
|
||||
icon: <TeamOutlined />,
|
||||
label: <Link to={`/project/${projectId}/characters`}>角色管理</Link>,
|
||||
},
|
||||
{
|
||||
key: 'organizations',
|
||||
icon: <BankOutlined />,
|
||||
label: <Link to={`/project/${projectId}/organizations`}>组织管理</Link>,
|
||||
},
|
||||
{
|
||||
key: 'careers',
|
||||
icon: <TrophyOutlined />,
|
||||
label: <Link to={`/project/${projectId}/careers`}>职业管理</Link>,
|
||||
},
|
||||
{
|
||||
key: 'relationships',
|
||||
icon: <ApartmentOutlined />,
|
||||
label: <Link to={`/project/${projectId}/relationships`}>关系管理</Link>,
|
||||
},
|
||||
{
|
||||
key: 'outline',
|
||||
icon: <FileTextOutlined />,
|
||||
label: <Link to={`/project/${projectId}/outline`}>大纲管理</Link>,
|
||||
},
|
||||
{
|
||||
key: 'chapters',
|
||||
icon: <BookOutlined />,
|
||||
label: <Link to={`/project/${projectId}/chapters`}>章节管理</Link>,
|
||||
},
|
||||
{
|
||||
key: 'chapter-analysis',
|
||||
icon: <FundOutlined />,
|
||||
label: <Link to={`/project/${projectId}/chapter-analysis`}>剧情分析</Link>,
|
||||
},
|
||||
{
|
||||
key: 'foreshadows',
|
||||
icon: <BulbOutlined />,
|
||||
label: <Link to={`/project/${projectId}/foreshadows`}>伏笔管理</Link>,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group' as const,
|
||||
label: '创作工具',
|
||||
children: [
|
||||
{
|
||||
key: 'writing-styles',
|
||||
icon: <EditOutlined />,
|
||||
label: <Link to={`/project/${projectId}/writing-styles`}>写作风格</Link>,
|
||||
},
|
||||
{
|
||||
key: 'prompt-workshop',
|
||||
icon: <CloudOutlined />,
|
||||
label: <Link to={`/project/${projectId}/prompt-workshop`}>提示词工坊</Link>,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const menuItemsCollapsed = [
|
||||
{
|
||||
key: 'sponsor',
|
||||
icon: <HeartOutlined />,
|
||||
@@ -157,11 +248,6 @@ export default function ProjectDetail() {
|
||||
icon: <CloudOutlined />,
|
||||
label: <Link to={`/project/${projectId}/prompt-workshop`}>提示词工坊</Link>,
|
||||
},
|
||||
// {
|
||||
// key: 'polish',
|
||||
// icon: <ToolOutlined />,
|
||||
// label: <Link to={`/project/${projectId}/polish`}>AI去味</Link>,
|
||||
// },
|
||||
];
|
||||
|
||||
// 根据当前路径动态确定选中的菜单项
|
||||
@@ -204,9 +290,9 @@ export default function ProjectDetail() {
|
||||
selectedKeys={[selectedKey]}
|
||||
style={{
|
||||
borderRight: 0,
|
||||
paddingTop: '16px'
|
||||
paddingTop: '12px'
|
||||
}}
|
||||
items={menuItems}
|
||||
items={collapsed ? menuItemsCollapsed : menuItems}
|
||||
onClick={() => mobile && setDrawerVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
@@ -215,54 +301,43 @@ export default function ProjectDetail() {
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh', height: '100vh', overflow: 'hidden' }}>
|
||||
<Header style={{
|
||||
background: 'var(--color-primary)',
|
||||
background: token.colorPrimary,
|
||||
padding: mobile ? '0 12px' : '0 24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
left: mobile ? 0 : (collapsed ? 60 : 220),
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
boxShadow: 'var(--shadow-header)',
|
||||
height: mobile ? 56 : 70
|
||||
boxShadow: `0 2px 10px ${alphaColor(token.colorText, 0.16)}`,
|
||||
height: mobile ? 56 : 70,
|
||||
transition: 'left 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', zIndex: 1 }}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={mobile ? <MenuUnfoldOutlined /> : (collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />)}
|
||||
onClick={() => mobile ? setDrawerVisible(true) : setCollapsed(!collapsed)}
|
||||
style={{
|
||||
fontSize: mobile ? '18px' : '20px',
|
||||
color: '#fff',
|
||||
width: mobile ? '36px' : '40px',
|
||||
height: mobile ? '36px' : '40px'
|
||||
}}
|
||||
/>
|
||||
{!mobile && (
|
||||
{mobile && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/')}
|
||||
icon={<MenuUnfoldOutlined />}
|
||||
onClick={() => setDrawerVisible(true)}
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
color: '#fff',
|
||||
height: '40px',
|
||||
padding: '0 16px'
|
||||
fontSize: '18px',
|
||||
color: token.colorWhite,
|
||||
width: '36px',
|
||||
height: '36px'
|
||||
}}
|
||||
>
|
||||
返回主页
|
||||
</Button>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h2 style={{
|
||||
margin: 0,
|
||||
color: '#fff',
|
||||
color: token.colorWhite,
|
||||
fontSize: mobile ? '16px' : '24px',
|
||||
fontWeight: 600,
|
||||
textShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
textShadow: `0 2px 4px ${alphaColor(token.colorText, 0.2)}`,
|
||||
position: mobile ? 'static' : 'absolute',
|
||||
left: mobile ? 'auto' : '50%',
|
||||
transform: mobile ? 'none' : 'translateX(-50%)',
|
||||
@@ -284,7 +359,7 @@ export default function ProjectDetail() {
|
||||
onClick={() => navigate('/')}
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
color: '#fff',
|
||||
color: token.colorWhite,
|
||||
height: '36px',
|
||||
padding: '0 8px',
|
||||
zIndex: 1
|
||||
@@ -315,23 +390,23 @@ export default function ProjectDetail() {
|
||||
minWidth: '56px',
|
||||
height: '56px',
|
||||
padding: '0 12px',
|
||||
boxShadow: 'inset 0 0 15px rgba(255, 255, 255, 0.15), 0 4px 10px rgba(0, 0, 0, 0.1)',
|
||||
boxShadow: `inset 0 0 15px ${alphaColor(token.colorWhite, 0.15)}, 0 4px 10px ${alphaColor(token.colorText, 0.1)}`,
|
||||
cursor: 'default',
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-3px) scale(1.02)';
|
||||
e.currentTarget.style.boxShadow = 'inset 0 0 20px rgba(255, 255, 255, 0.25), 0 8px 16px rgba(0, 0, 0, 0.15)';
|
||||
e.currentTarget.style.border = '1px solid rgba(255, 255, 255, 0.1)';
|
||||
e.currentTarget.style.boxShadow = `inset 0 0 20px ${alphaColor(token.colorWhite, 0.25)}, 0 8px 16px ${alphaColor(token.colorText, 0.15)}`;
|
||||
e.currentTarget.style.border = `1px solid ${alphaColor(token.colorWhite, 0.1)}`;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0) scale(1)';
|
||||
e.currentTarget.style.boxShadow = 'inset 0 0 15px rgba(255, 255, 255, 0.15), 0 4px 10px rgba(0, 0, 0, 0.1)';
|
||||
e.currentTarget.style.boxShadow = `inset 0 0 15px ${alphaColor(token.colorWhite, 0.15)}, 0 4px 10px ${alphaColor(token.colorText, 0.1)}`;
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
fontSize: '11px',
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
color: alphaColor(token.colorWhite, 0.9),
|
||||
marginBottom: '2px',
|
||||
lineHeight: 1
|
||||
}}>
|
||||
@@ -340,7 +415,7 @@ export default function ProjectDetail() {
|
||||
<span style={{
|
||||
fontSize: '15px',
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
color: token.colorWhite,
|
||||
lineHeight: 1,
|
||||
fontFamily: 'Monaco, monospace'
|
||||
}}>
|
||||
@@ -357,7 +432,24 @@ export default function ProjectDetail() {
|
||||
<Layout style={{ marginTop: mobile ? 56 : 70 }}>
|
||||
{mobile ? (
|
||||
<Drawer
|
||||
title="导航菜单"
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
background: token.colorPrimary,
|
||||
borderRadius: 8,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: token.colorWhite,
|
||||
fontSize: 16,
|
||||
}}>
|
||||
<BookOutlined />
|
||||
</div>
|
||||
<span style={{ fontWeight: 600, fontSize: 16 }}>MuMuAINovel</span>
|
||||
</div>
|
||||
}
|
||||
placement="left"
|
||||
onClose={() => setDrawerVisible(false)}
|
||||
open={drawerVisible}
|
||||
@@ -365,6 +457,13 @@ export default function ProjectDetail() {
|
||||
styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column' } }}
|
||||
>
|
||||
{renderMenu()}
|
||||
<div style={{ padding: 16, borderTop: `1px solid ${token.colorBorderSecondary}` }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 12, color: token.colorTextTertiary, marginBottom: 8 }}>
|
||||
<span>主题模式</span>
|
||||
<span>{resolvedMode === 'dark' ? '深色' : '浅色'}</span>
|
||||
</div>
|
||||
<ThemeSwitch block />
|
||||
</div>
|
||||
</Drawer>
|
||||
) : (
|
||||
<Sider
|
||||
@@ -374,15 +473,18 @@ export default function ProjectDetail() {
|
||||
trigger={null}
|
||||
width={220}
|
||||
collapsedWidth={60}
|
||||
className="modern-sider"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
top: 70,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
overflow: 'hidden',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
height: 'calc(100vh - 70px)'
|
||||
height: '100vh',
|
||||
background: token.colorBgContainer,
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||
boxShadow: `4px 0 16px ${alphaColor(token.colorText, 0.06)}`,
|
||||
zIndex: 1000
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
@@ -390,18 +492,148 @@ export default function ProjectDetail() {
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}>
|
||||
<div style={{
|
||||
height: 70,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: collapsed ? 0 : '0 12px',
|
||||
background: token.colorPrimary,
|
||||
flexShrink: 0,
|
||||
justifyContent: collapsed ? 'center' : 'space-between',
|
||||
gap: 8
|
||||
}}>
|
||||
{collapsed ? (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<MenuUnfoldOutlined />}
|
||||
onClick={() => setCollapsed(false)}
|
||||
style={{
|
||||
color: token.colorWhite,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: 0,
|
||||
borderRadius: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0, overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
background: alphaColor(token.colorWhite, 0.2),
|
||||
borderRadius: 8,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: token.colorWhite,
|
||||
fontSize: 16,
|
||||
backdropFilter: 'blur(4px)'
|
||||
}}>
|
||||
<BookOutlined />
|
||||
</div>
|
||||
<span style={{
|
||||
color: token.colorWhite,
|
||||
fontWeight: 600,
|
||||
fontSize: 15,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}>
|
||||
MuMuAINovel
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<MenuFoldOutlined />}
|
||||
onClick={() => setCollapsed(true)}
|
||||
style={{
|
||||
color: token.colorWhite,
|
||||
width: 32,
|
||||
height: 32,
|
||||
padding: 0,
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{renderMenu()}
|
||||
<div style={{
|
||||
padding: collapsed ? '12px 8px' : '12px',
|
||||
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||||
flexShrink: 0
|
||||
}}>
|
||||
{collapsed ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsedThemeIcon}
|
||||
onClick={cycleThemeMode}
|
||||
title={`主题模式:${mode === 'light' ? '浅色' : mode === 'dark' ? '深色' : '跟随系统'}(点击切换)`}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
background: alphaColor(token.colorBgContainer, 0.65),
|
||||
border: `1px solid ${token.colorBorder}`,
|
||||
color: token.colorText,
|
||||
padding: 0,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/')}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
background: alphaColor(token.colorBgContainer, 0.65),
|
||||
border: `1px solid ${token.colorBorder}`,
|
||||
color: token.colorText,
|
||||
padding: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 12, color: token.colorTextTertiary }}>
|
||||
<span>主题模式</span>
|
||||
<span>{resolvedMode === 'dark' ? '深色' : '浅色'}</span>
|
||||
</div>
|
||||
<ThemeSwitch block />
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/')}
|
||||
block
|
||||
style={{
|
||||
color: token.colorText,
|
||||
height: 40,
|
||||
justifyContent: 'flex-start',
|
||||
padding: '0 12px'
|
||||
}}
|
||||
>
|
||||
返回主页
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Sider>
|
||||
)}
|
||||
|
||||
<Layout style={{
|
||||
marginLeft: mobile ? 0 : (collapsed ? 60 : 220),
|
||||
transition: 'all 0.2s'
|
||||
transition: 'margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
}}>
|
||||
<Content
|
||||
style={{
|
||||
background: 'var(--color-bg-base)',
|
||||
background: token.colorBgLayout,
|
||||
padding: mobile ? 12 : 24,
|
||||
height: mobile ? 'calc(100vh - 56px)' : 'calc(100vh - 70px)',
|
||||
overflow: 'hidden',
|
||||
@@ -410,10 +642,10 @@ export default function ProjectDetail() {
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
background: 'var(--color-bg-container)',
|
||||
background: token.colorBgContainer,
|
||||
padding: mobile ? 12 : 24,
|
||||
borderRadius: mobile ? '8px' : '12px',
|
||||
boxShadow: 'var(--shadow-card)',
|
||||
boxShadow: `0 8px 24px ${alphaColor(token.colorText, 0.08)}`,
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Form, Input, InputNumber, Select, Button, Card,
|
||||
Row, Col, Typography, Space, message, Radio
|
||||
Row, Col, Typography, Space, message, Radio, theme
|
||||
} from 'antd';
|
||||
import {
|
||||
RocketOutlined, ArrowLeftOutlined, CheckCircleOutlined
|
||||
@@ -18,6 +18,7 @@ export default function ProjectWizardNew() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [form] = Form.useForm();
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||
const { token } = theme.useToken();
|
||||
|
||||
// 状态管理
|
||||
const [currentStep, setCurrentStep] = useState<'form' | 'generating'>('form');
|
||||
@@ -196,7 +197,7 @@ export default function ProjectWizardNew() {
|
||||
<Card
|
||||
hoverable
|
||||
style={{
|
||||
borderColor: form.getFieldValue('outline_mode') === 'one-to-one' ? 'var(--color-primary)' : 'var(--color-border)',
|
||||
// borderColor: form.getFieldValue('outline_mode') === 'one-to-one' ? token.colorPrimary : token.colorBorder,
|
||||
borderWidth: 2,
|
||||
height: '100%',
|
||||
}}
|
||||
@@ -205,13 +206,13 @@ 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: 'var(--color-success)' }} />
|
||||
<CheckCircleOutlined style={{ marginRight: 8, color: token.colorSuccess }} />
|
||||
传统模式 (1→1)
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#666' }}>
|
||||
<div style={{ fontSize: 12, color: token.colorTextSecondary }}>
|
||||
一个大纲对应一个章节,简单直接
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#999' }}>
|
||||
<div style={{ fontSize: 11, color: token.colorTextTertiary }}>
|
||||
💡 适合:简单剧情、快速创作、短篇小说
|
||||
</div>
|
||||
</Space>
|
||||
@@ -223,7 +224,7 @@ export default function ProjectWizardNew() {
|
||||
<Card
|
||||
hoverable
|
||||
style={{
|
||||
borderColor: form.getFieldValue('outline_mode') === 'one-to-many' ? 'var(--color-primary)' : 'var(--color-border)',
|
||||
// borderColor: form.getFieldValue('outline_mode') === 'one-to-many' ? token.colorPrimary : token.colorBorder,
|
||||
borderWidth: 2,
|
||||
height: '100%',
|
||||
}}
|
||||
@@ -232,13 +233,13 @@ 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: 'var(--color-success)' }} />
|
||||
<CheckCircleOutlined style={{ marginRight: 8, color: token.colorSuccess }} />
|
||||
细化模式 (1→N) 推荐
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#666' }}>
|
||||
<div style={{ fontSize: 12, color: token.colorTextSecondary }}>
|
||||
一个大纲可展开为多个章节,灵活控制
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#999' }}>
|
||||
<div style={{ fontSize: 11, color: token.colorTextTertiary }}>
|
||||
💡 适合:复杂剧情、长篇创作、需要细化控制
|
||||
</div>
|
||||
</Space>
|
||||
@@ -322,15 +323,15 @@ export default function ProjectWizardNew() {
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100dvh',
|
||||
background: 'var(--color-bg-base)',
|
||||
background: token.colorBgBase,
|
||||
}}>
|
||||
{/* 顶部标题栏 - 固定不滚动 */}
|
||||
<div style={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
background: 'var(--color-primary)',
|
||||
boxShadow: 'var(--shadow-header)',
|
||||
background: token.colorPrimary,
|
||||
boxShadow: `0 6px 20px color-mix(in srgb, ${token.colorPrimary} 30%, transparent)`,
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: 1200,
|
||||
@@ -346,9 +347,9 @@ export default function ProjectWizardNew() {
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
disabled={currentStep === 'generating'}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
borderColor: 'rgba(255,255,255,0.3)',
|
||||
color: '#fff',
|
||||
background: `color-mix(in srgb, ${token.colorWhite} 20%, transparent)`,
|
||||
borderColor: `color-mix(in srgb, ${token.colorWhite} 30%, transparent)`,
|
||||
color: token.colorWhite,
|
||||
}}
|
||||
>
|
||||
{isMobile ? '返回' : '返回首页'}
|
||||
@@ -356,8 +357,8 @@ export default function ProjectWizardNew() {
|
||||
|
||||
<Title level={isMobile ? 4 : 2} style={{
|
||||
margin: 0,
|
||||
color: '#fff',
|
||||
textShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
color: token.colorWhite,
|
||||
textShadow: '0 2px 4px color-mix(in srgb, var(--ant-color-black) 18%, transparent)',
|
||||
}}>
|
||||
<RocketOutlined style={{ marginRight: 8 }} />
|
||||
项目创建向导
|
||||
|
||||
@@ -15,7 +15,8 @@ import {
|
||||
Alert,
|
||||
Upload,
|
||||
Spin,
|
||||
Empty
|
||||
Empty,
|
||||
theme
|
||||
} from 'antd';
|
||||
import {
|
||||
EditOutlined,
|
||||
@@ -27,7 +28,7 @@ import {
|
||||
InfoCircleOutlined
|
||||
} from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import { cardStyles, cardHoverHandlers, gridConfig } from '../components/CardStyles';
|
||||
import { promptTemplateCardStyles, promptTemplateCardHoverHandlers, promptTemplateGridConfig } from '../components/CardStyles';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
@@ -54,6 +55,7 @@ interface CategoryGroup {
|
||||
}
|
||||
|
||||
export default function PromptTemplates() {
|
||||
const { token } = theme.useToken();
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [categories, setCategories] = useState<CategoryGroup[]>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('0');
|
||||
@@ -246,13 +248,15 @@ export default function PromptTemplates() {
|
||||
};
|
||||
|
||||
const currentTemplates = getCurrentTemplates();
|
||||
const pageBackground = `linear-gradient(180deg, ${token.colorBgLayout} 0%, ${token.colorFillSecondary} 100%)`;
|
||||
const headerBackground = `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`;
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<div style={{
|
||||
minHeight: '90vh',
|
||||
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)',
|
||||
background: pageBackground,
|
||||
padding: isMobile ? '20px 16px 70px' : '24px 24px 70px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -269,9 +273,9 @@ export default function PromptTemplates() {
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, #5A9BA5 50%, var(--color-primary-hover) 100%)',
|
||||
background: headerBackground,
|
||||
borderRadius: isMobile ? 16 : 24,
|
||||
boxShadow: '0 12px 40px rgba(77, 128, 136, 0.25), 0 4px 12px rgba(0, 0, 0, 0.06)',
|
||||
boxShadow: token.boxShadowSecondary,
|
||||
marginBottom: isMobile ? 20 : 24,
|
||||
border: 'none',
|
||||
position: 'relative',
|
||||
@@ -279,18 +283,18 @@ export default function PromptTemplates() {
|
||||
}}
|
||||
>
|
||||
{/* 装饰性背景元素 */}
|
||||
<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' }} />
|
||||
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: token.colorWhite, opacity: 0.08, pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: token.colorWhite, opacity: 0.05, pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: token.colorWhite, opacity: 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}>
|
||||
<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 level={isMobile ? 3 : 2} style={{ margin: 0, color: token.colorWhite, textShadow: `0 2px 4px ${token.colorBgMask}` }}>
|
||||
<FileSearchOutlined style={{ color: token.colorWhite, opacity: 0.9, marginRight: 8 }} />
|
||||
提示词模板管理
|
||||
</Title>
|
||||
<Text style={{ fontSize: isMobile ? 12 : 14, color: 'rgba(255,255,255,0.85)', marginLeft: isMobile ? 40 : 48 }}>
|
||||
<Text style={{ fontSize: isMobile ? 12 : 14, color: token.colorTextLightSolid, opacity: 0.85, marginLeft: isMobile ? 40 : 48 }}>
|
||||
自定义AI生成提示词,打造个性化创作体验
|
||||
</Text>
|
||||
</Space>
|
||||
@@ -303,10 +307,11 @@ export default function PromptTemplates() {
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
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',
|
||||
background: token.colorWhite,
|
||||
border: `1px solid ${token.colorWhite}`,
|
||||
boxShadow: token.boxShadow,
|
||||
color: token.colorPrimary,
|
||||
fontWeight: 600,
|
||||
backdropFilter: 'blur(10px)',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
@@ -323,10 +328,11 @@ export default function PromptTemplates() {
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
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',
|
||||
background: token.colorWhite,
|
||||
border: `1px solid ${token.colorWhite}`,
|
||||
boxShadow: token.boxShadow,
|
||||
color: token.colorPrimary,
|
||||
fontWeight: 600,
|
||||
backdropFilter: 'blur(10px)',
|
||||
}}
|
||||
>
|
||||
@@ -341,7 +347,7 @@ export default function PromptTemplates() {
|
||||
<Alert
|
||||
message={
|
||||
<Space align="center">
|
||||
<InfoCircleOutlined style={{ fontSize: 16, color: 'var(--color-primary)' }} />
|
||||
<InfoCircleOutlined style={{ fontSize: 16, color: token.colorPrimary }} />
|
||||
<Text strong style={{ fontSize: isMobile ? 13 : 14 }}>使用说明</Text>
|
||||
</Space>
|
||||
}
|
||||
@@ -360,8 +366,8 @@ export default function PromptTemplates() {
|
||||
style={{
|
||||
marginTop: isMobile ? 16 : 24,
|
||||
borderRadius: 12,
|
||||
background: 'var(--color-info-bg)',
|
||||
border: '1px solid var(--color-info-border)'
|
||||
background: token.colorInfoBg,
|
||||
border: `1px solid ${token.colorInfoBorder}`
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
@@ -374,9 +380,9 @@ export default function PromptTemplates() {
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
background: token.colorBgContainer,
|
||||
borderRadius: isMobile ? 12 : 16,
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
||||
boxShadow: token.boxShadowSecondary,
|
||||
marginBottom: isMobile ? 16 : 24
|
||||
}}
|
||||
styles={{ body: { padding: isMobile ? '12px' : '16px' } }}
|
||||
@@ -400,9 +406,9 @@ export default function PromptTemplates() {
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
background: token.colorBgContainer,
|
||||
borderRadius: isMobile ? 12 : 16,
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
||||
boxShadow: token.boxShadowSecondary,
|
||||
}}
|
||||
>
|
||||
<Empty
|
||||
@@ -413,25 +419,25 @@ export default function PromptTemplates() {
|
||||
) : (
|
||||
<Row gutter={[16, 16]}>
|
||||
{currentTemplates.map(template => (
|
||||
<Col {...gridConfig} key={template.id}>
|
||||
<Col {...promptTemplateGridConfig} key={template.id}>
|
||||
<Card
|
||||
hoverable
|
||||
variant="borderless"
|
||||
style={cardStyles.project}
|
||||
style={promptTemplateCardStyles.templateCard}
|
||||
styles={{ body: { padding: 0, overflow: 'hidden' } }}
|
||||
{...cardHoverHandlers}
|
||||
{...promptTemplateCardHoverHandlers}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div style={{
|
||||
background: template.is_system_default
|
||||
? 'var(--color-bg-layout)'
|
||||
: 'var(--color-primary)',
|
||||
? token.colorFillTertiary
|
||||
: token.colorPrimary,
|
||||
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>
|
||||
<Title level={isMobile ? 5 : 4} style={{ margin: 0, color: template.is_system_default ? token.colorText : token.colorWhite, flex: 1 }} ellipsis>
|
||||
{template.template_name}
|
||||
</Title>
|
||||
{!template.is_system_default && (
|
||||
@@ -444,10 +450,10 @@ export default function PromptTemplates() {
|
||||
)}
|
||||
</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' }}>
|
||||
<Tag color={template.is_system_default ? 'default' : 'rgba(255,255,255,0.3)'} style={{ color: template.is_system_default ? token.colorTextSecondary : token.colorWhite, 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' }}>
|
||||
<Tag color={template.is_system_default ? 'default' : 'rgba(255,255,255,0.3)'} style={{ color: template.is_system_default ? token.colorTextSecondary : token.colorWhite, border: 'none' }}>
|
||||
{template.is_system_default ? '系统默认' : '已自定义'}
|
||||
</Tag>
|
||||
</Space>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
Pagination,
|
||||
Alert,
|
||||
Statistic,
|
||||
theme,
|
||||
} from 'antd';
|
||||
import {
|
||||
SearchOutlined,
|
||||
@@ -120,6 +121,7 @@ export default function PromptWorkshop() {
|
||||
const [activeTab, setActiveTab] = useState<string>('browse');
|
||||
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
const { token } = theme.useToken();
|
||||
|
||||
// 判断是否为服务端管理员
|
||||
const isServerAdmin = serviceStatus?.mode === 'server' && currentUser?.is_admin;
|
||||
@@ -431,7 +433,7 @@ export default function PromptWorkshop() {
|
||||
borderRadius: 12,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
border: '1px solid #f0f0f0',
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
}}
|
||||
bodyStyle={{
|
||||
padding: 16,
|
||||
@@ -446,7 +448,7 @@ export default function PromptWorkshop() {
|
||||
<Tooltip title={item.is_liked ? '取消点赞' : '点赞'} key="like">
|
||||
<span onClick={() => handleLike(item)}>
|
||||
{item.is_liked ? (
|
||||
<HeartFilled style={{ color: '#ff4d4f' }} />
|
||||
<HeartFilled style={{ color: token.colorError }} />
|
||||
) : (
|
||||
<HeartOutlined />
|
||||
)}
|
||||
@@ -489,7 +491,7 @@ export default function PromptWorkshop() {
|
||||
style={{
|
||||
fontSize: 12,
|
||||
marginBottom: 0,
|
||||
backgroundColor: '#fafafa',
|
||||
backgroundColor: token.colorFillQuaternary,
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
flex: 1,
|
||||
@@ -512,7 +514,7 @@ export default function PromptWorkshop() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 8, color: '#999', fontSize: 12 }}>
|
||||
<div style={{ marginTop: 8, color: token.colorTextTertiary, fontSize: 12 }}>
|
||||
<Space>
|
||||
<span><UserOutlined /> {item.author_name || '匿名'}</span>
|
||||
</Space>
|
||||
@@ -569,7 +571,7 @@ export default function PromptWorkshop() {
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
style={{ borderRadius: 12, height: '100%', border: '1px solid #f0f0f0' }}
|
||||
style={{ borderRadius: 12, height: '100%', border: `1px solid ${token.colorBorderSecondary}` }}
|
||||
bodyStyle={{ padding: 16 }}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
@@ -599,7 +601,7 @@ export default function PromptWorkshop() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div style={{ fontSize: 12, color: '#999' }}>
|
||||
<div style={{ fontSize: 12, color: token.colorTextTertiary }}>
|
||||
提交时间: {sub.created_at ? new Date(sub.created_at).toLocaleDateString() : '-'}
|
||||
</div>
|
||||
|
||||
@@ -802,7 +804,7 @@ export default function PromptWorkshop() {
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card size="small">
|
||||
<Statistic title="待审核" value={adminStats.total_pending} valueStyle={{ color: '#faad14' }} />
|
||||
<Statistic title="待审核" value={adminStats.total_pending} valueStyle={{ color: token.colorWarning }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
@@ -853,13 +855,13 @@ export default function PromptWorkshop() {
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
style={{ borderRadius: 12, border: '1px solid #f0f0f0' }}
|
||||
style={{ borderRadius: 12, border: `1px solid ${token.colorBorderSecondary}` }}
|
||||
bodyStyle={{ padding: 16 }}
|
||||
actions={[
|
||||
<Button
|
||||
key="approve"
|
||||
type="link"
|
||||
style={{ color: '#52c41a' }}
|
||||
style={{ color: token.colorSuccess }}
|
||||
onClick={() => {
|
||||
setReviewingSubmission(sub);
|
||||
reviewForm.setFieldsValue({
|
||||
@@ -887,7 +889,7 @@ export default function PromptWorkshop() {
|
||||
{sub.prompt_content}
|
||||
</Paragraph>
|
||||
|
||||
<div style={{ fontSize: 11, color: '#999' }}>
|
||||
<div style={{ fontSize: 11, color: token.colorTextTertiary }}>
|
||||
<div>提交者: {sub.submitter_name || '未知'}</div>
|
||||
<div>来源: {sub.source_instance}</div>
|
||||
<div>时间: {sub.created_at ? new Date(sub.created_at).toLocaleDateString() : '-'}</div>
|
||||
@@ -928,7 +930,7 @@ export default function PromptWorkshop() {
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
style={{ borderRadius: 12, border: '1px solid #f0f0f0' }}
|
||||
style={{ borderRadius: 12, border: `1px solid ${token.colorBorderSecondary}` }}
|
||||
bodyStyle={{ padding: 16 }}
|
||||
actions={[
|
||||
<Tooltip title="编辑" key="edit">
|
||||
@@ -965,7 +967,7 @@ export default function PromptWorkshop() {
|
||||
{item.prompt_content}
|
||||
</Paragraph>
|
||||
|
||||
<div style={{ fontSize: 11, color: '#999' }}>
|
||||
<div style={{ fontSize: 11, color: token.colorTextTertiary }}>
|
||||
<Space>
|
||||
<span><HeartOutlined /> {item.like_count || 0}</span>
|
||||
<span><DownloadOutlined /> {item.download_count || 0}</span>
|
||||
@@ -992,7 +994,7 @@ export default function PromptWorkshop() {
|
||||
alignItems: 'center',
|
||||
padding: isMobile ? '12px 0' : '16px 0',
|
||||
marginBottom: isMobile ? 12 : 16,
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
flexWrap: 'wrap',
|
||||
gap: 12,
|
||||
}}>
|
||||
@@ -1185,7 +1187,7 @@ export default function PromptWorkshop() {
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
backgroundColor: token.colorFillSecondary,
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
@@ -1236,7 +1238,7 @@ export default function PromptWorkshop() {
|
||||
{reviewingSubmission && (
|
||||
<div>
|
||||
<div style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
backgroundColor: token.colorFillSecondary,
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useMemo, type CSSProperties } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Card, Tag, Button, Space, message, Typography } from 'antd';
|
||||
import { Card, Tag, Button, Space, message, Typography, theme } from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
ApartmentOutlined,
|
||||
@@ -100,16 +100,18 @@ interface CharacterListResponse {
|
||||
const GROUP_MAIN_CAREER_NODE_ID = '__career_group_main__';
|
||||
const GROUP_SUB_CAREER_NODE_ID = '__career_group_sub__';
|
||||
|
||||
const EDGE_CATEGORY_META: Record<string, { label: string; color: string; order: number }> = {
|
||||
organization: { label: '组织成员', color: '#722ed1', order: 1 },
|
||||
career_main: { label: '主职业关联', color: '#faad14', order: 2 },
|
||||
career_sub: { label: '副职业关联', color: '#13c2c2', order: 3 },
|
||||
career_group: { label: '职业分类', color: '#8c8c8c', order: 4 },
|
||||
family: { label: '亲属关系', color: '#f39c12', order: 5 },
|
||||
hostile: { label: '敌对关系', color: '#e74c3c', order: 6 },
|
||||
professional: { label: '职业关系', color: '#3498db', order: 7 },
|
||||
social: { label: '社交关系', color: '#27ae60', order: 8 },
|
||||
default: { label: '其他关系', color: '#95a5a6', order: 99 },
|
||||
type EdgeColorPreset = 'primary' | 'warning' | 'info' | 'textTertiary' | 'error' | 'success';
|
||||
|
||||
const EDGE_CATEGORY_META: Record<string, { label: string; colorPreset: EdgeColorPreset; order: number }> = {
|
||||
organization: { label: '组织成员', colorPreset: 'primary', order: 1 },
|
||||
career_main: { label: '主职业关联', colorPreset: 'warning', order: 2 },
|
||||
career_sub: { label: '副职业关联', colorPreset: 'info', order: 3 },
|
||||
career_group: { label: '职业分类', colorPreset: 'textTertiary', order: 4 },
|
||||
family: { label: '亲属关系', colorPreset: 'warning', order: 5 },
|
||||
hostile: { label: '敌对关系', colorPreset: 'error', order: 6 },
|
||||
professional: { label: '职业关系', colorPreset: 'info', order: 7 },
|
||||
social: { label: '社交关系', colorPreset: 'success', order: 8 },
|
||||
default: { label: '其他关系', colorPreset: 'textTertiary', order: 99 },
|
||||
};
|
||||
|
||||
const getEdgeCategory = (edge: Edge) =>
|
||||
@@ -118,13 +120,36 @@ const getEdgeCategory = (edge: Edge) =>
|
||||
const getEdgeCategoryMeta = (category: string) =>
|
||||
EDGE_CATEGORY_META[category] || {
|
||||
label: `${category}关系`,
|
||||
color: '#95a5a6',
|
||||
colorPreset: 'textTertiary' as const,
|
||||
order: 999,
|
||||
};
|
||||
|
||||
const resolveEdgePresetColor = (
|
||||
colorPreset: EdgeColorPreset,
|
||||
token: {
|
||||
colorPrimary: string;
|
||||
colorWarning: string;
|
||||
colorInfo: string;
|
||||
colorTextTertiary: string;
|
||||
colorError: string;
|
||||
colorSuccess: string;
|
||||
},
|
||||
) => {
|
||||
const colorMap: Record<EdgeColorPreset, string> = {
|
||||
primary: token.colorPrimary,
|
||||
warning: token.colorWarning,
|
||||
info: token.colorInfo,
|
||||
textTertiary: token.colorTextTertiary,
|
||||
error: token.colorError,
|
||||
success: token.colorSuccess,
|
||||
};
|
||||
|
||||
return colorMap[colorPreset];
|
||||
};
|
||||
|
||||
const clampTextStyle = (rows: number): CSSProperties => ({
|
||||
margin: '4px 0 0',
|
||||
color: '#555',
|
||||
color: 'var(--ant-color-text-secondary)',
|
||||
fontSize: 14,
|
||||
lineHeight: '22px',
|
||||
display: '-webkit-box',
|
||||
@@ -386,54 +411,65 @@ const getCategoryColor = (
|
||||
relationshipName: string,
|
||||
isActive: boolean,
|
||||
relationshipTypes: RelationshipType[],
|
||||
token: {
|
||||
colorPrimary: string;
|
||||
colorWarning: string;
|
||||
colorInfo: string;
|
||||
colorTextTertiary: string;
|
||||
colorError: string;
|
||||
colorSuccess: string;
|
||||
colorBorder: string;
|
||||
},
|
||||
) => {
|
||||
const inactiveColor = token.colorBorder;
|
||||
|
||||
if (relationshipName.startsWith('组织成员·')) {
|
||||
return isActive ? '#722ed1' : '#cdb7f6';
|
||||
return isActive ? token.colorPrimary : inactiveColor;
|
||||
}
|
||||
|
||||
if (relationshipName.startsWith('主职业·')) {
|
||||
return isActive ? '#faad14' : '#ffe7ba';
|
||||
return isActive ? token.colorWarning : inactiveColor;
|
||||
}
|
||||
|
||||
if (relationshipName.startsWith('副职业·')) {
|
||||
return isActive ? '#13c2c2' : '#b5f5ec';
|
||||
return isActive ? token.colorInfo : inactiveColor;
|
||||
}
|
||||
|
||||
if (relationshipName.startsWith('职业分类·')) {
|
||||
return isActive ? '#8c8c8c' : '#d9d9d9';
|
||||
return isActive ? token.colorTextTertiary : inactiveColor;
|
||||
}
|
||||
|
||||
const relType = relationshipTypes.find((rt) => rt.name === relationshipName);
|
||||
const category = relType?.category || 'default';
|
||||
|
||||
const categoryColors: Record<string, { active: string; inactive: string }> = {
|
||||
family: { active: '#f39c12', inactive: '#fcd59e' },
|
||||
hostile: { active: '#e74c3c', inactive: '#f5a49a' },
|
||||
professional: { active: '#3498db', inactive: '#a9d4ed' },
|
||||
social: { active: '#27ae60', inactive: '#a3d9b5' },
|
||||
default: { active: '#95a5a6', inactive: '#c8d0d2' },
|
||||
const categoryColors: Record<string, string> = {
|
||||
family: token.colorWarning,
|
||||
hostile: token.colorError,
|
||||
professional: token.colorInfo,
|
||||
social: token.colorSuccess,
|
||||
default: token.colorTextTertiary,
|
||||
};
|
||||
|
||||
const colors = categoryColors[category] || categoryColors.default;
|
||||
return isActive ? colors.active : colors.inactive;
|
||||
const activeColor = categoryColors[category] || categoryColors.default;
|
||||
return isActive ? activeColor : inactiveColor;
|
||||
};
|
||||
|
||||
const getCharacterNodeStyle = (roleType: string): CSSProperties => {
|
||||
const roleColorMap: Record<string, string> = {
|
||||
protagonist: '#e74c3c',
|
||||
antagonist: '#9b59b6',
|
||||
supporting: '#3498db',
|
||||
protagonist: 'var(--ant-color-error)',
|
||||
antagonist: 'var(--ant-color-primary)',
|
||||
supporting: 'var(--ant-color-info)',
|
||||
};
|
||||
|
||||
const baseColor = roleColorMap[roleType] || '#3498db';
|
||||
const baseColor = roleColorMap[roleType] || 'var(--ant-color-info)';
|
||||
|
||||
return {
|
||||
width: 130,
|
||||
height: 130,
|
||||
border: `2px solid ${baseColor}`,
|
||||
borderRadius: '50%',
|
||||
background: `linear-gradient(135deg, #ffffff, ${baseColor}15)`,
|
||||
boxShadow: `0 4px 16px ${baseColor}25`,
|
||||
background: `linear-gradient(135deg, var(--ant-color-bg-container), color-mix(in srgb, ${baseColor} 12%, var(--ant-color-bg-container)))`,
|
||||
boxShadow: `0 4px 16px color-mix(in srgb, ${baseColor} 25%, transparent)`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -445,10 +481,10 @@ const getCharacterNodeStyle = (roleType: string): CSSProperties => {
|
||||
const getOrganizationNodeStyle = (): CSSProperties => ({
|
||||
width: 160,
|
||||
height: 90,
|
||||
border: '2px solid #27ae60',
|
||||
border: '2px solid var(--ant-color-success)',
|
||||
borderRadius: 12,
|
||||
background: 'linear-gradient(135deg, #ffffff, #27ae6015)',
|
||||
boxShadow: '0 4px 16px rgba(39, 174, 96, 0.15)',
|
||||
background: 'linear-gradient(135deg, var(--ant-color-bg-container), color-mix(in srgb, var(--ant-color-success) 12%, var(--ant-color-bg-container)))',
|
||||
boxShadow: '0 4px 16px color-mix(in srgb, var(--ant-color-success) 15%, transparent)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -457,15 +493,15 @@ const getOrganizationNodeStyle = (): CSSProperties => ({
|
||||
});
|
||||
|
||||
const getCareerNodeStyle = (type: 'main' | 'sub'): CSSProperties => {
|
||||
const color = type === 'main' ? '#faad14' : '#13c2c2';
|
||||
const color = type === 'main' ? 'var(--ant-color-warning)' : 'var(--ant-color-info)';
|
||||
|
||||
return {
|
||||
width: 150,
|
||||
height: 72,
|
||||
border: `2px solid ${color}`,
|
||||
borderRadius: 12,
|
||||
background: `linear-gradient(135deg, #ffffff, ${color}15)`,
|
||||
boxShadow: `0 4px 12px ${color}20`,
|
||||
background: `linear-gradient(135deg, var(--ant-color-bg-container), color-mix(in srgb, ${color} 12%, var(--ant-color-bg-container)))`,
|
||||
boxShadow: `0 4px 12px color-mix(in srgb, ${color} 20%, transparent)`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -475,15 +511,15 @@ const getCareerNodeStyle = (type: 'main' | 'sub'): CSSProperties => {
|
||||
};
|
||||
|
||||
const getCareerGroupStyle = (type: 'main' | 'sub'): CSSProperties => {
|
||||
const color = type === 'main' ? '#d48806' : '#08979c';
|
||||
const color = type === 'main' ? 'var(--ant-color-warning)' : 'var(--ant-color-info)';
|
||||
|
||||
return {
|
||||
width: 130,
|
||||
height: 52,
|
||||
border: `2px dashed ${color}`,
|
||||
borderRadius: 26,
|
||||
backgroundColor: '#ffffff',
|
||||
boxShadow: `0 2px 8px ${color}15`,
|
||||
backgroundColor: 'var(--ant-color-bg-container)',
|
||||
boxShadow: `0 2px 8px color-mix(in srgb, ${color} 15%, transparent)`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -511,12 +547,12 @@ const InfoField = ({
|
||||
marginBottom: 12,
|
||||
padding: '12px 14px',
|
||||
borderRadius: 12,
|
||||
background: '#f8f9fa',
|
||||
border: '1px solid #eef0f2',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.02)',
|
||||
background: 'var(--ant-color-fill-quaternary)',
|
||||
border: '1px solid var(--ant-color-border-secondary)',
|
||||
boxShadow: '0 2px 4px color-mix(in srgb, var(--ant-color-text) 6%, transparent)',
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ fontSize: 14, color: '#333' }}>
|
||||
<Text strong style={{ fontSize: 14, color: 'var(--ant-color-text)' }}>
|
||||
{label}
|
||||
</Text>
|
||||
<div style={clampTextStyle(rows)}>{value}</div>
|
||||
@@ -527,6 +563,9 @@ const InfoField = ({
|
||||
export default function RelationshipGraph() {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { token } = theme.useToken();
|
||||
const alphaColor = (color: string, alpha: number) =>
|
||||
`color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||
|
||||
const [graphData, setGraphData] = useState<GraphData | null>(null);
|
||||
const [, setLoading] = useState(false);
|
||||
@@ -564,11 +603,13 @@ export default function RelationshipGraph() {
|
||||
return {
|
||||
category,
|
||||
count,
|
||||
...meta,
|
||||
label: meta.label,
|
||||
color: resolveEdgePresetColor(meta.colorPreset, token),
|
||||
order: meta.order,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.order - b.order || a.label.localeCompare(b.label, 'zh-CN'));
|
||||
}, [edges]);
|
||||
}, [edges, token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (edgeCategoryOptions.length === 0) {
|
||||
@@ -625,7 +666,15 @@ export default function RelationshipGraph() {
|
||||
layoutWeight?: number;
|
||||
},
|
||||
): Edge => {
|
||||
const edgeColor = getCategoryColor(relationship, status === 'active', relationshipTypes);
|
||||
const edgeColor = getCategoryColor(relationship, status === 'active', relationshipTypes, {
|
||||
colorPrimary: token.colorPrimary,
|
||||
colorWarning: token.colorWarning,
|
||||
colorInfo: token.colorInfo,
|
||||
colorTextTertiary: token.colorTextTertiary,
|
||||
colorError: token.colorError,
|
||||
colorSuccess: token.colorSuccess,
|
||||
colorBorder: token.colorBorder,
|
||||
});
|
||||
const isOrgMemberLink = relationship.startsWith('组织成员·');
|
||||
const isCareerMainLink = relationship.startsWith('主职业·');
|
||||
const isCareerSubLink = relationship.startsWith('副职业·');
|
||||
@@ -645,12 +694,12 @@ export default function RelationshipGraph() {
|
||||
opacity: isCareerClassLink ? 0.5 : (isCareerMainLink || isCareerSubLink ? 0.6 : 1),
|
||||
},
|
||||
labelStyle: {
|
||||
fill: '#666',
|
||||
fill: token.colorTextSecondary,
|
||||
fontSize: 10,
|
||||
fontWeight: isCareerMainLink || isCareerSubLink ? 600 : 500,
|
||||
},
|
||||
labelBgStyle: {
|
||||
fill: '#fff',
|
||||
fill: token.colorBgContainer,
|
||||
fillOpacity: 0.9,
|
||||
},
|
||||
markerEnd: {
|
||||
@@ -673,7 +722,18 @@ export default function RelationshipGraph() {
|
||||
},
|
||||
};
|
||||
},
|
||||
[relationshipTypes],
|
||||
[
|
||||
relationshipTypes,
|
||||
token.colorBgContainer,
|
||||
token.colorBorder,
|
||||
token.colorError,
|
||||
token.colorInfo,
|
||||
token.colorPrimary,
|
||||
token.colorSuccess,
|
||||
token.colorTextSecondary,
|
||||
token.colorTextTertiary,
|
||||
token.colorWarning,
|
||||
],
|
||||
);
|
||||
|
||||
const loadGraphData = useCallback(async () => {
|
||||
@@ -705,28 +765,28 @@ export default function RelationshipGraph() {
|
||||
const detail = detailMap[node.id];
|
||||
|
||||
const roleColorMap: Record<string, string> = {
|
||||
protagonist: '#e74c3c',
|
||||
antagonist: '#9b59b6',
|
||||
supporting: '#3498db',
|
||||
protagonist: token.colorError,
|
||||
antagonist: token.colorPrimary,
|
||||
supporting: token.colorInfo,
|
||||
};
|
||||
const baseColor = roleColorMap[node.role_type] || '#3498db';
|
||||
const baseColor = roleColorMap[node.role_type] || token.colorInfo;
|
||||
|
||||
const labelContent = node.type === 'organization' ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
|
||||
<ApartmentOutlined style={{ fontSize: 24, color: '#27ae60', marginBottom: 4 }} />
|
||||
<div style={{ fontWeight: 600, fontSize: 14, color: '#333', maxWidth: '90%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{node.name}</div>
|
||||
<div style={{ fontSize: 11, color: '#666', marginTop: 2 }}>{detail?.organization_type || '组织'}</div>
|
||||
<ApartmentOutlined style={{ fontSize: 24, color: token.colorSuccess, marginBottom: 4 }} />
|
||||
<div style={{ fontWeight: 600, fontSize: 14, color: token.colorText, maxWidth: '90%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{node.name}</div>
|
||||
<div style={{ fontSize: 11, color: token.colorTextSecondary, marginTop: 2 }}>{detail?.organization_type || '组织'}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
|
||||
{detail?.avatar_url ? (
|
||||
<img src={detail.avatar_url} alt={node.name} style={{ width: 56, height: 56, borderRadius: '50%', objectFit: 'cover', border: '2px solid #fff', boxShadow: '0 2px 6px rgba(0,0,0,0.1)', marginBottom: 6 }} />
|
||||
<img src={detail.avatar_url} alt={node.name} style={{ width: 56, height: 56, borderRadius: '50%', objectFit: 'cover', border: `2px solid ${token.colorBgContainer}`, boxShadow: `0 2px 6px ${alphaColor(token.colorTextBase, 0.18)}`, marginBottom: 6 }} />
|
||||
) : (
|
||||
<div style={{ width: 56, height: 56, borderRadius: '50%', backgroundColor: '#f0f2f5', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '2px solid #fff', boxShadow: '0 2px 6px rgba(0,0,0,0.1)', marginBottom: 6 }}>
|
||||
<div style={{ width: 56, height: 56, borderRadius: '50%', backgroundColor: token.colorFillSecondary, display: 'flex', alignItems: 'center', justifyContent: 'center', border: `2px solid ${token.colorBgContainer}`, boxShadow: `0 2px 6px ${alphaColor(token.colorTextBase, 0.18)}`, marginBottom: 6 }}>
|
||||
<UserOutlined style={{ fontSize: 28, color: baseColor }} />
|
||||
</div>
|
||||
)}
|
||||
<div style={{ fontWeight: 600, fontSize: 13, color: '#333', maxWidth: '90%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{node.name}</div>
|
||||
<div style={{ fontWeight: 600, fontSize: 13, color: token.colorText, maxWidth: '90%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{node.name}</div>
|
||||
<div style={{ fontSize: 11, color: baseColor, marginTop: 2, transform: 'scale(0.9)' }}>
|
||||
{node.role_type === 'protagonist' ? '主角' : node.role_type === 'antagonist' ? '反派' : '配角'}
|
||||
</div>
|
||||
@@ -753,8 +813,8 @@ export default function RelationshipGraph() {
|
||||
data: {
|
||||
label: (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ fontSize: 11, color: '#d48806', marginBottom: 2 }}>主职业</div>
|
||||
<div style={{ fontWeight: 600, fontSize: 13, color: '#333' }}>{career.name}</div>
|
||||
<div style={{ fontSize: 11, color: token.colorWarning, marginBottom: 2 }}>主职业</div>
|
||||
<div style={{ fontWeight: 600, fontSize: 13, color: token.colorText }}>{career.name}</div>
|
||||
</div>
|
||||
),
|
||||
type: 'career_main',
|
||||
@@ -769,8 +829,8 @@ export default function RelationshipGraph() {
|
||||
data: {
|
||||
label: (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ fontSize: 11, color: '#08979c', marginBottom: 2 }}>副职业</div>
|
||||
<div style={{ fontWeight: 600, fontSize: 13, color: '#333' }}>{career.name}</div>
|
||||
<div style={{ fontSize: 11, color: token.colorInfo, marginBottom: 2 }}>副职业</div>
|
||||
<div style={{ fontWeight: 600, fontSize: 13, color: token.colorText }}>{career.name}</div>
|
||||
</div>
|
||||
),
|
||||
type: 'career_sub',
|
||||
@@ -922,7 +982,23 @@ export default function RelationshipGraph() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId, relationshipTypes, buildFlowEdge, setNodes, setEdges]);
|
||||
}, [
|
||||
projectId,
|
||||
relationshipTypes,
|
||||
buildFlowEdge,
|
||||
setNodes,
|
||||
setEdges,
|
||||
token.colorBgContainer,
|
||||
token.colorError,
|
||||
token.colorFillSecondary,
|
||||
token.colorInfo,
|
||||
token.colorPrimary,
|
||||
token.colorSuccess,
|
||||
token.colorText,
|
||||
token.colorTextBase,
|
||||
token.colorTextSecondary,
|
||||
token.colorWarning,
|
||||
]);
|
||||
|
||||
// 当 relationshipTypes 加载完成后再加载图数据
|
||||
useEffect(() => {
|
||||
@@ -1000,27 +1076,27 @@ export default function RelationshipGraph() {
|
||||
marginBottom: 12,
|
||||
padding: '12px 14px',
|
||||
borderRadius: 12,
|
||||
background: '#f8f9fa',
|
||||
border: '1px solid #eef0f2',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.02)',
|
||||
background: token.colorFillQuaternary,
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
boxShadow: `0 2px 4px ${alphaColor(token.colorTextBase, 0.06)}`,
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ fontSize: 14, color: '#333' }}>
|
||||
<Text strong style={{ fontSize: 14, color: token.colorText }}>
|
||||
职业体系
|
||||
</Text>
|
||||
<div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{nodeDetail.main_career_id ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Tag color="gold" style={{ margin: 0, borderRadius: 12, padding: '0 10px', fontWeight: 500 }}>主职业</Tag>
|
||||
<span style={{ fontSize: 14, color: '#444' }}>
|
||||
<span style={{ fontSize: 14, color: token.colorText }}>
|
||||
{careerNameMap[nodeDetail.main_career_id]?.name || nodeDetail.main_career_id}
|
||||
{nodeDetail.main_career_stage ? <span style={{ color: '#888', marginLeft: 4 }}>第{nodeDetail.main_career_stage}阶</span> : ''}
|
||||
{nodeDetail.main_career_stage ? <span style={{ color: token.colorTextTertiary, marginLeft: 4 }}>第{nodeDetail.main_career_stage}阶</span> : ''}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Tag style={{ margin: 0, borderRadius: 12, padding: '0 10px' }}>主职业</Tag>
|
||||
<span style={{ fontSize: 14, color: '#888' }}>未设置</span>
|
||||
<span style={{ fontSize: 14, color: token.colorTextTertiary }}>未设置</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1029,9 +1105,9 @@ export default function RelationshipGraph() {
|
||||
<Tag color="cyan" style={{ margin: 0, borderRadius: 12, padding: '0 10px', fontWeight: 500 }}>副职业</Tag>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, flex: 1 }}>
|
||||
{subCareerData.map((sub, index) => (
|
||||
<span key={`${sub.career_id}-${index}`} style={{ fontSize: 14, color: '#444', background: '#fff', border: '1px solid #e8e8e8', borderRadius: 4, padding: '0 6px' }}>
|
||||
<span key={`${sub.career_id}-${index}`} style={{ fontSize: 14, color: token.colorText, background: token.colorBgContainer, border: `1px solid ${token.colorBorderSecondary}`, borderRadius: token.borderRadiusSM, padding: '0 6px' }}>
|
||||
{careerNameMap[sub.career_id]?.name || sub.career_id}
|
||||
{sub.stage ? <span style={{ color: '#888', marginLeft: 4 }}>阶{sub.stage}</span> : ''}
|
||||
{sub.stage ? <span style={{ color: token.colorTextTertiary, marginLeft: 4 }}>阶{sub.stage}</span> : ''}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -1039,7 +1115,7 @@ export default function RelationshipGraph() {
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Tag style={{ margin: 0, borderRadius: 12, padding: '0 10px' }}>副职业</Tag>
|
||||
<span style={{ fontSize: 14, color: '#888' }}>未设置</span>
|
||||
<span style={{ fontSize: 14, color: token.colorTextTertiary }}>未设置</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1057,7 +1133,7 @@ export default function RelationshipGraph() {
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#f5f5f5',
|
||||
backgroundColor: token.colorBgLayout,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
@@ -1093,35 +1169,35 @@ export default function RelationshipGraph() {
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 14, fontSize: 12, flexWrap: 'wrap' }}>
|
||||
{/* 节点图例 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ color: '#3498db', fontWeight: 'bold' }}>●</span>
|
||||
<span style={{ color: token.colorInfo, fontWeight: 'bold' }}>●</span>
|
||||
<span>角色(圆形)</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ color: '#27ae60', fontWeight: 'bold' }}>■</span>
|
||||
<span style={{ color: token.colorSuccess, fontWeight: 'bold' }}>■</span>
|
||||
<span>组织(方形)</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ color: '#faad14', fontWeight: 'bold' }}>▭</span>
|
||||
<span style={{ color: token.colorWarning, fontWeight: 'bold' }}>▭</span>
|
||||
<span>主职业</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ color: '#13c2c2', fontWeight: 'bold' }}>▭</span>
|
||||
<span style={{ color: token.colorInfo, fontWeight: 'bold' }}>▭</span>
|
||||
<span>副职业</span>
|
||||
</div>
|
||||
|
||||
<span style={{ color: '#d9d9d9' }}>|</span>
|
||||
<span style={{ color: token.colorBorder }}>|</span>
|
||||
|
||||
{/* 连线图例 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ color: '#722ed1', fontWeight: 'bold' }}>- -</span>
|
||||
<span style={{ color: token.colorPrimary, fontWeight: 'bold' }}>- -</span>
|
||||
<span>组织成员</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ color: '#faad14', fontWeight: 'bold' }}>—</span>
|
||||
<span style={{ color: token.colorWarning, fontWeight: 'bold' }}>—</span>
|
||||
<span>主职业关联</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ color: '#13c2c2', fontWeight: 'bold' }}>- -</span>
|
||||
<span style={{ color: token.colorInfo, fontWeight: 'bold' }}>- -</span>
|
||||
<span>副职业关联</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1141,8 +1217,8 @@ export default function RelationshipGraph() {
|
||||
onClick={() => toggleEdgeCategoryVisibility(option.category)}
|
||||
style={
|
||||
isVisible
|
||||
? { backgroundColor: option.color, borderColor: option.color, color: '#fff' }
|
||||
: { color: '#666' }
|
||||
? { backgroundColor: option.color, borderColor: option.color, color: token.colorWhite }
|
||||
: { color: token.colorTextSecondary }
|
||||
}
|
||||
>
|
||||
{option.label}({option.count})
|
||||
@@ -1154,7 +1230,68 @@ export default function RelationshipGraph() {
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div style={{ flex: 1, minHeight: 0 }}>
|
||||
<div style={{ flex: 1, minHeight: 0 }} className="relationship-graph-flow">
|
||||
<style>
|
||||
{`
|
||||
.relationship-graph-flow .react-flow__handle {
|
||||
opacity: 0 !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.relationship-graph-flow .react-flow__node {
|
||||
outline: 1px solid var(--ant-color-border-secondary);
|
||||
outline-offset: 0;
|
||||
}
|
||||
|
||||
.relationship-graph-flow .react-flow__controls {
|
||||
border: 1px solid var(--ant-color-border-secondary);
|
||||
border-radius: var(--ant-border-radius-lg);
|
||||
overflow: hidden;
|
||||
background: var(--ant-color-bg-elevated);
|
||||
box-shadow: 0 6px 16px color-mix(in srgb, var(--ant-color-text) 12%, transparent);
|
||||
}
|
||||
|
||||
.relationship-graph-flow .react-flow__controls-button {
|
||||
background: var(--ant-color-bg-elevated);
|
||||
border-bottom: 1px solid var(--ant-color-border-secondary);
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
|
||||
.relationship-graph-flow .react-flow__controls-button:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.relationship-graph-flow .react-flow__controls-button:hover {
|
||||
background: var(--ant-color-fill-secondary);
|
||||
}
|
||||
|
||||
.relationship-graph-flow .react-flow__controls-button:disabled {
|
||||
background: var(--ant-color-fill-quaternary);
|
||||
color: var(--ant-color-text-quaternary);
|
||||
}
|
||||
|
||||
.relationship-graph-flow .react-flow__controls-button svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.relationship-graph-flow .react-flow__attribution {
|
||||
background: var(--ant-color-bg-elevated);
|
||||
border: 1px solid var(--ant-color-border-secondary);
|
||||
border-radius: var(--ant-border-radius-sm);
|
||||
box-shadow: 0 2px 8px color-mix(in srgb, var(--ant-color-text) 10%, transparent);
|
||||
}
|
||||
|
||||
.relationship-graph-flow .react-flow__attribution a {
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
|
||||
.relationship-graph-flow .react-flow__attribution a:hover {
|
||||
color: var(--ant-color-primary);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={visibleEdges}
|
||||
@@ -1192,7 +1329,7 @@ export default function RelationshipGraph() {
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 12px 32px rgba(0,0,0,0.12)',
|
||||
boxShadow: `0 12px 32px ${alphaColor(token.colorTextBase, 0.22)}`,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -1238,8 +1375,8 @@ export default function RelationshipGraph() {
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
objectFit: 'cover',
|
||||
border: '3px solid #fff',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||
border: `3px solid ${token.colorBgContainer}`,
|
||||
boxShadow: `0 4px 12px ${alphaColor(token.colorTextBase, 0.18)}`,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -1248,14 +1385,14 @@ export default function RelationshipGraph() {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: nodeDetail.color || (nodeDetail.is_organization ? '#27ae60' : '#1890ff'),
|
||||
backgroundColor: nodeDetail.color || (nodeDetail.is_organization ? token.colorSuccess : token.colorPrimary),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 32,
|
||||
color: '#fff',
|
||||
border: '3px solid #fff',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||
color: token.colorWhite,
|
||||
border: `3px solid ${token.colorBgContainer}`,
|
||||
boxShadow: `0 4px 12px ${alphaColor(token.colorTextBase, 0.18)}`,
|
||||
}}
|
||||
>
|
||||
{nodeDetail.is_organization ? <TeamOutlined /> : <UserOutlined />}
|
||||
@@ -1266,23 +1403,23 @@ export default function RelationshipGraph() {
|
||||
position: 'absolute',
|
||||
bottom: -4,
|
||||
right: -4,
|
||||
background: nodeDetail.is_organization ? '#27ae60' : (nodeDetail.role_type === 'protagonist' ? '#e74c3c' : nodeDetail.role_type === 'antagonist' ? '#9b59b6' : '#3498db'),
|
||||
background: nodeDetail.is_organization ? token.colorSuccess : (nodeDetail.role_type === 'protagonist' ? token.colorError : nodeDetail.role_type === 'antagonist' ? token.colorPrimary : token.colorInfo),
|
||||
borderRadius: '50%',
|
||||
width: 28,
|
||||
height: 28,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '2px solid #fff',
|
||||
color: '#fff',
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||
border: `2px solid ${token.colorBgContainer}`,
|
||||
color: token.colorWhite,
|
||||
boxShadow: `0 2px 6px ${alphaColor(token.colorTextBase, 0.22)}`,
|
||||
}}
|
||||
>
|
||||
{nodeDetail.is_organization ? <ApartmentOutlined style={{ fontSize: 14 }} /> : <UserOutlined style={{ fontSize: 14 }} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 20, fontWeight: 600, color: '#333', marginBottom: 8 }}>{nodeDetail.name}</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600, color: token.colorText, marginBottom: 8 }}>{nodeDetail.name}</div>
|
||||
<Space size={6} wrap style={{ justifyContent: 'center' }}>
|
||||
{!nodeDetail.is_organization && (
|
||||
<Tag
|
||||
@@ -1321,12 +1458,12 @@ export default function RelationshipGraph() {
|
||||
marginBottom: 12,
|
||||
padding: '12px 14px',
|
||||
borderRadius: 12,
|
||||
background: '#f8f9fa',
|
||||
border: '1px solid #eef0f2',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.02)',
|
||||
background: token.colorFillQuaternary,
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
boxShadow: `0 2px 4px ${alphaColor(token.colorTextBase, 0.06)}`,
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ fontSize: 14, color: '#333' }}>
|
||||
<Text strong style={{ fontSize: 14, color: token.colorText }}>
|
||||
特征标签
|
||||
</Text>
|
||||
<Space size={[6, 8]} wrap style={{ marginTop: 10 }}>
|
||||
@@ -1352,16 +1489,16 @@ export default function RelationshipGraph() {
|
||||
marginBottom: 12,
|
||||
padding: '12px 14px',
|
||||
borderRadius: 12,
|
||||
background: '#f8f9fa',
|
||||
border: '1px solid #eef0f2',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.02)',
|
||||
background: token.colorFillQuaternary,
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
boxShadow: `0 2px 4px ${alphaColor(token.colorTextBase, 0.06)}`,
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ fontSize: 14, color: '#333' }}>
|
||||
<Text strong style={{ fontSize: 14, color: token.colorText }}>
|
||||
势力等级
|
||||
</Text>
|
||||
<div style={{ ...clampTextStyle(1), fontSize: 18, color: '#f39c12', fontWeight: 'bold' }}>
|
||||
{nodeDetail.power_level}<span style={{ fontSize: 14, color: '#888', fontWeight: 'normal' }}>/100</span>
|
||||
<div style={{ ...clampTextStyle(1), fontSize: 18, color: token.colorWarning, fontWeight: 'bold' }}>
|
||||
{nodeDetail.power_level}<span style={{ fontSize: 14, color: token.colorTextTertiary, fontWeight: 'normal' }}>/100</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1372,12 +1509,12 @@ export default function RelationshipGraph() {
|
||||
marginBottom: 12,
|
||||
padding: '12px 14px',
|
||||
borderRadius: 12,
|
||||
background: '#f8f9fa',
|
||||
border: '1px solid #eef0f2',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.02)',
|
||||
background: token.colorFillQuaternary,
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
boxShadow: `0 2px 4px ${alphaColor(token.colorTextBase, 0.06)}`,
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ fontSize: 14, color: '#333' }}>
|
||||
<Text strong style={{ fontSize: 14, color: token.colorText }}>
|
||||
组织成员
|
||||
</Text>
|
||||
<Space size={[6, 8]} wrap style={{ marginTop: 10 }}>
|
||||
@@ -1407,9 +1544,9 @@ export default function RelationshipGraph() {
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<Card size="small" style={{ width: 300, borderRadius: 10, boxShadow: '0 6px 18px rgba(0,0,0,0.12)' }}>
|
||||
<Card size="small" style={{ width: 300, borderRadius: 10, boxShadow: `0 6px 18px ${alphaColor(token.colorTextBase, 0.2)}` }}>
|
||||
<Space align="start">
|
||||
<TrophyOutlined style={{ color: '#faad14', marginTop: 4 }} />
|
||||
<TrophyOutlined style={{ color: token.colorWarning, marginTop: 4 }} />
|
||||
<div>
|
||||
<Text strong>职业节点</Text>
|
||||
<p style={{ ...clampTextStyle(2), marginTop: 2 }}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, Slider, Input, Tabs, AutoComplete } from 'antd';
|
||||
import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, Slider, Input, Tabs, AutoComplete, theme } from 'antd';
|
||||
import { PlusOutlined, ApartmentOutlined, UserOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import axios from 'axios';
|
||||
@@ -45,6 +45,7 @@ export default function Relationships() {
|
||||
const [editingRelationship, setEditingRelationship] = useState<Relationship | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const { token } = theme.useToken();
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@@ -393,7 +394,7 @@ export default function Relationships() {
|
||||
key={category}
|
||||
size="small"
|
||||
title={categoryLabels[category] || category}
|
||||
headStyle={{ backgroundColor: '#f5f5f5' }}
|
||||
headStyle={{ backgroundColor: token.colorFillAlter }}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{types.map(type => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, Form, Input, Button, Select, Slider, InputNumber, message, Space, Typography, Spin, Modal, Alert, Grid, Tabs, List, Tag, Popconfirm, Empty, Row, Col } from 'antd';
|
||||
import { Card, Form, Input, Button, Select, Slider, InputNumber, message, Space, Typography, Spin, Modal, Alert, Grid, Tabs, List, Tag, Popconfirm, Empty, Row, Col, theme } from 'antd';
|
||||
import { SaveOutlined, DeleteOutlined, ReloadOutlined, InfoCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined, PlusOutlined, EditOutlined, CopyOutlined, WarningOutlined } from '@ant-design/icons';
|
||||
import { settingsApi, mcpPluginApi } from '../services/api';
|
||||
import type { SettingsUpdate, APIKeyPreset, PresetCreateRequest, APIKeyPresetConfig } from '../types';
|
||||
@@ -11,6 +11,7 @@ const { useBreakpoint } = Grid;
|
||||
const { TextArea } = Input;
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { token } = theme.useToken();
|
||||
const screens = useBreakpoint();
|
||||
const isMobile = !screens.md; // md断点是768px
|
||||
const [form] = Form.useForm();
|
||||
@@ -51,6 +52,9 @@ export default function SettingsPage() {
|
||||
const [presetModelsFetched, setPresetModelsFetched] = useState(false);
|
||||
const [presetModelSearchText, setPresetModelSearchText] = useState('');
|
||||
|
||||
const pageBackground = `linear-gradient(180deg, ${token.colorBgLayout} 0%, ${token.colorFillSecondary} 100%)`;
|
||||
const headerBackground = `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`;
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
if (activeTab === 'presets') {
|
||||
@@ -181,7 +185,7 @@ export default function SettingsPage() {
|
||||
modal.warning({
|
||||
title: (
|
||||
<Space>
|
||||
<WarningOutlined style={{ color: '#faad14' }} />
|
||||
<WarningOutlined style={{ color: token.colorWarning }} />
|
||||
<span>API 配置已更改</span>
|
||||
</Space>
|
||||
),
|
||||
@@ -196,8 +200,8 @@ export default function SettingsPage() {
|
||||
/>
|
||||
<div style={{
|
||||
padding: 12,
|
||||
background: 'var(--color-info-bg)',
|
||||
border: '1px solid var(--color-info-border)',
|
||||
background: token.colorInfoBg,
|
||||
border: `1px solid ${token.colorInfoBorder}`,
|
||||
borderRadius: 8
|
||||
}}>
|
||||
<Text strong style={{ display: 'block', marginBottom: 8 }}>请完成以下步骤:</Text>
|
||||
@@ -600,7 +604,7 @@ export default function SettingsPage() {
|
||||
modal.warning({
|
||||
title: (
|
||||
<Space>
|
||||
<WarningOutlined style={{ color: '#faad14' }} />
|
||||
<WarningOutlined style={{ color: token.colorWarning }} />
|
||||
<span>API 配置已更改</span>
|
||||
</Space>
|
||||
),
|
||||
@@ -615,8 +619,8 @@ export default function SettingsPage() {
|
||||
/>
|
||||
<div style={{
|
||||
padding: 12,
|
||||
background: 'var(--color-info-bg)',
|
||||
border: '1px solid var(--color-info-border)',
|
||||
background: token.colorInfoBg,
|
||||
border: `1px solid ${token.colorInfoBorder}`,
|
||||
borderRadius: 8
|
||||
}}>
|
||||
<Text strong style={{ display: 'block', marginBottom: 8 }}>请完成以下步骤:</Text>
|
||||
@@ -657,15 +661,15 @@ export default function SettingsPage() {
|
||||
width: isMobile ? '90%' : 600,
|
||||
content: (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{ marginBottom: 24, padding: 16, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
|
||||
<Typography.Text strong style={{ color: 'var(--color-success)' }}>
|
||||
<div style={{ marginBottom: 24, padding: 16, background: token.colorSuccessBg, border: `1px solid ${token.colorSuccessBorder}`, borderRadius: 8 }}>
|
||||
<Typography.Text strong style={{ color: token.colorSuccess }}>
|
||||
✓ API 连接正常
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: 'var(--color-bg-layout)',
|
||||
background: token.colorBgLayout,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
@@ -711,13 +715,13 @@ export default function SettingsPage() {
|
||||
{result.error && (
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: 'var(--color-error-bg)',
|
||||
border: '1px solid var(--color-error-border)',
|
||||
background: token.colorErrorBg,
|
||||
border: `1px solid ${token.colorErrorBorder}`,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>错误信息:</Text>
|
||||
<Text style={{ fontSize: 13, color: 'var(--color-error)', fontFamily: 'monospace', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
<Text style={{ fontSize: 13, color: token.colorError, fontFamily: 'monospace', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
{result.error}
|
||||
</Text>
|
||||
</div>
|
||||
@@ -726,8 +730,8 @@ export default function SettingsPage() {
|
||||
{result.suggestions && result.suggestions.length > 0 && (
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: 'var(--color-warning-bg)',
|
||||
border: '1px solid var(--color-warning-border)',
|
||||
background: token.colorWarningBg,
|
||||
border: `1px solid ${token.colorWarningBorder}`,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
@@ -817,10 +821,10 @@ export default function SettingsPage() {
|
||||
<List.Item
|
||||
key={preset.id}
|
||||
style={{
|
||||
background: isActive ? '#f0f5ff' : 'transparent',
|
||||
background: isActive ? token.colorInfoBg : 'transparent',
|
||||
padding: '16px',
|
||||
marginBottom: '8px',
|
||||
border: isActive ? '2px solid #1890ff' : '1px solid #f0f0f0',
|
||||
border: isActive ? `2px solid ${token.colorPrimary}` : `1px solid ${token.colorBorderSecondary}`,
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
actions={[
|
||||
@@ -870,7 +874,7 @@ export default function SettingsPage() {
|
||||
avatar={
|
||||
isActive && (
|
||||
<CheckCircleOutlined
|
||||
style={{ fontSize: '24px', color: '#52c41a' }}
|
||||
style={{ fontSize: '24px', color: token.colorSuccess }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -883,7 +887,7 @@ export default function SettingsPage() {
|
||||
description={
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
{preset.description && (
|
||||
<div style={{ color: '#666' }}>{preset.description}</div>
|
||||
<div style={{ color: token.colorTextSecondary }}>{preset.description}</div>
|
||||
)}
|
||||
<Space wrap>
|
||||
<Tag color={getProviderColor(preset.config.api_provider)}>
|
||||
@@ -893,7 +897,7 @@ export default function SettingsPage() {
|
||||
<Tag>温度: {preset.config.temperature}</Tag>
|
||||
<Tag>Tokens: {preset.config.max_tokens}</Tag>
|
||||
</Space>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
<div style={{ fontSize: '12px', color: token.colorTextTertiary }}>
|
||||
创建于: {new Date(preset.created_at).toLocaleString()}
|
||||
</div>
|
||||
</Space>
|
||||
@@ -913,7 +917,7 @@ export default function SettingsPage() {
|
||||
{contextHolder}
|
||||
<div style={{
|
||||
minHeight: '90vh',
|
||||
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)',
|
||||
background: pageBackground,
|
||||
padding: isMobile ? '20px 16px 70px' : '24px 24px 70px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -930,9 +934,9 @@ export default function SettingsPage() {
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, #5A9BA5 50%, var(--color-primary-hover) 100%)',
|
||||
background: headerBackground,
|
||||
borderRadius: isMobile ? 16 : 24,
|
||||
boxShadow: '0 12px 40px rgba(77, 128, 136, 0.25), 0 4px 12px rgba(0, 0, 0, 0.06)',
|
||||
boxShadow: token.boxShadowSecondary,
|
||||
marginBottom: isMobile ? 20 : 24,
|
||||
border: 'none',
|
||||
position: 'relative',
|
||||
@@ -940,17 +944,17 @@ export default function SettingsPage() {
|
||||
}}
|
||||
>
|
||||
{/* 装饰性背景元素 */}
|
||||
<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' }} />
|
||||
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: token.colorWhite, opacity: 0.08, pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: token.colorWhite, opacity: 0.05, pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: token.colorWhite, opacity: 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, color: '#fff', textShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
|
||||
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: token.colorWhite, textShadow: `0 2px 4px ${token.colorBgMask}` }}>
|
||||
AI API 设置
|
||||
</Title>
|
||||
<Text style={{ fontSize: isMobile ? 12 : 14, color: 'rgba(255,255,255,0.85)', marginLeft: isMobile ? 40 : 48 }}>
|
||||
<Text style={{ fontSize: isMobile ? 12 : 14, color: token.colorTextLightSolid, marginLeft: isMobile ? 40 : 48, opacity: 0.85 }}>
|
||||
配置AI接口参数,管理多个API配置预设
|
||||
</Text>
|
||||
</Space>
|
||||
@@ -965,9 +969,9 @@ export default function SettingsPage() {
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
background: token.colorBgContainer,
|
||||
borderRadius: isMobile ? 12 : 16,
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
||||
boxShadow: token.boxShadowSecondary,
|
||||
flex: 1,
|
||||
}}
|
||||
styles={{
|
||||
@@ -1030,7 +1034,7 @@ export default function SettingsPage() {
|
||||
<span>API 提供商</span>
|
||||
<InfoCircleOutlined
|
||||
title="选择你的AI服务提供商"
|
||||
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
|
||||
style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
@@ -1052,7 +1056,7 @@ export default function SettingsPage() {
|
||||
<span>API 密钥</span>
|
||||
<InfoCircleOutlined
|
||||
title="你的API密钥,将加密存储"
|
||||
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
|
||||
style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
@@ -1072,7 +1076,7 @@ export default function SettingsPage() {
|
||||
<span>API 地址</span>
|
||||
<InfoCircleOutlined
|
||||
title="API的基础URL地址"
|
||||
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
|
||||
style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
@@ -1094,7 +1098,7 @@ export default function SettingsPage() {
|
||||
<span>模型名称</span>
|
||||
<InfoCircleOutlined
|
||||
title="AI模型的名称,如 gpt-4, gpt-3.5-turbo"
|
||||
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
|
||||
style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
@@ -1121,17 +1125,17 @@ export default function SettingsPage() {
|
||||
<>
|
||||
{menu}
|
||||
{fetchingModels && (
|
||||
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||
<div style={{ padding: '8px 12px', color: token.colorTextSecondary, textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||
<Spin size="small" /> 正在获取模型列表...
|
||||
</div>
|
||||
)}
|
||||
{!fetchingModels && modelOptions.length === 0 && modelsFetched && !modelSearchText && (
|
||||
<div style={{ padding: '8px 12px', color: '#ff4d4f', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||
<div style={{ padding: '8px 12px', color: token.colorError, textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||
未能获取到模型列表,可直接输入模型名称
|
||||
</div>
|
||||
)}
|
||||
{!fetchingModels && modelOptions.length === 0 && !modelsFetched && !modelSearchText && (
|
||||
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||
<div style={{ padding: '8px 12px', color: token.colorTextSecondary, textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||
点击输入框自动获取,或直接输入模型名称
|
||||
</div>
|
||||
)}
|
||||
@@ -1200,13 +1204,13 @@ export default function SettingsPage() {
|
||||
<div style={{ fontWeight: 500, fontSize: isMobile ? '13px' : '14px' }}>
|
||||
{option.data.description === '手动输入的模型名称' ? (
|
||||
<Space size={4}>
|
||||
<EditOutlined style={{ color: 'var(--color-primary)' }} />
|
||||
<EditOutlined style={{ color: token.colorPrimary }} />
|
||||
<span>使用 "{option.data.label}"</span>
|
||||
</Space>
|
||||
) : option.data.label}
|
||||
</div>
|
||||
{option.data.description && option.data.description !== '手动输入的模型名称' && (
|
||||
<div style={{ fontSize: isMobile ? '11px' : '12px', color: '#8c8c8c', marginTop: '2px' }}>
|
||||
<div style={{ fontSize: isMobile ? '11px' : '12px', color: token.colorTextTertiary, marginTop: '2px' }}>
|
||||
{option.data.description}
|
||||
</div>
|
||||
)}
|
||||
@@ -1221,7 +1225,7 @@ export default function SettingsPage() {
|
||||
<span>温度参数</span>
|
||||
<InfoCircleOutlined
|
||||
title="控制输出的随机性,值越高越随机(0.0-2.0)"
|
||||
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
|
||||
style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
@@ -1246,7 +1250,7 @@ export default function SettingsPage() {
|
||||
<span>最大 Token 数</span>
|
||||
<InfoCircleOutlined
|
||||
title="单次请求的最大token数量"
|
||||
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
|
||||
style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
@@ -1270,7 +1274,7 @@ export default function SettingsPage() {
|
||||
<span>系统提示词</span>
|
||||
<InfoCircleOutlined
|
||||
title="设置全局系统提示词,每次AI调用时都会自动使用。可用于设定AI的角色、语言风格等"
|
||||
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
|
||||
style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
@@ -1291,9 +1295,9 @@ export default function SettingsPage() {
|
||||
message={
|
||||
<Space>
|
||||
{testResult.success ? (
|
||||
<CheckCircleOutlined style={{ color: 'var(--color-success)', fontSize: isMobile ? '16px' : '18px' }} />
|
||||
<CheckCircleOutlined style={{ color: token.colorSuccess, fontSize: isMobile ? '16px' : '18px' }} />
|
||||
) : (
|
||||
<CloseCircleOutlined style={{ color: 'var(--color-error)', fontSize: isMobile ? '16px' : '18px' }} />
|
||||
<CloseCircleOutlined style={{ color: token.colorError, fontSize: isMobile ? '16px' : '18px' }} />
|
||||
)}
|
||||
<span style={{ fontSize: isMobile ? '14px' : '16px', fontWeight: 500 }}>
|
||||
{testResult.message}
|
||||
@@ -1313,16 +1317,16 @@ export default function SettingsPage() {
|
||||
<div style={{
|
||||
fontSize: isMobile ? '12px' : '13px',
|
||||
padding: '8px 12px',
|
||||
background: '#f6ffed',
|
||||
background: token.colorSuccessBg,
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #b7eb8f',
|
||||
border: `1px solid ${token.colorSuccessBorder}`,
|
||||
marginTop: '8px'
|
||||
}}>
|
||||
<div style={{ marginBottom: '4px', fontWeight: 500 }}>AI 响应预览:</div>
|
||||
<div style={{ color: '#595959' }}>{testResult.response_preview}</div>
|
||||
<div style={{ color: token.colorTextSecondary }}>{testResult.response_preview}</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ color: 'var(--color-success)', fontSize: isMobile ? '12px' : '13px', marginTop: '4px' }}>
|
||||
<div style={{ color: token.colorSuccess, fontSize: isMobile ? '12px' : '13px', marginTop: '4px' }}>
|
||||
✓ API 配置正确,可以正常使用
|
||||
</div>
|
||||
</Space>
|
||||
@@ -1332,16 +1336,16 @@ export default function SettingsPage() {
|
||||
<div style={{
|
||||
fontSize: isMobile ? '12px' : '13px',
|
||||
padding: '8px 12px',
|
||||
background: '#fff2e8',
|
||||
background: token.colorErrorBg,
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ffbb96',
|
||||
color: '#d4380d'
|
||||
border: `1px solid ${token.colorErrorBorder}`,
|
||||
color: token.colorError
|
||||
}}>
|
||||
<strong>错误信息:</strong> {testResult.error}
|
||||
</div>
|
||||
)}
|
||||
{testResult.error_type && (
|
||||
<div style={{ fontSize: isMobile ? '11px' : '12px', color: 'var(--color-text-secondary)' }}>
|
||||
<div style={{ fontSize: isMobile ? '11px' : '12px', color: token.colorTextSecondary }}>
|
||||
错误类型: {testResult.error_type}
|
||||
</div>
|
||||
)}
|
||||
@@ -1354,7 +1358,7 @@ export default function SettingsPage() {
|
||||
margin: 0,
|
||||
paddingLeft: isMobile ? '16px' : '20px',
|
||||
fontSize: isMobile ? '12px' : '13px',
|
||||
color: '#595959'
|
||||
color: token.colorTextSecondary
|
||||
}}>
|
||||
{testResult.suggestions.map((suggestion, index) => (
|
||||
<li key={index} style={{ marginBottom: '4px' }}>{suggestion}</li>
|
||||
@@ -1386,7 +1390,7 @@ export default function SettingsPage() {
|
||||
loading={loading}
|
||||
block
|
||||
style={{
|
||||
background: 'var(--color-primary)',
|
||||
background: token.colorPrimary,
|
||||
border: 'none',
|
||||
height: '44px'
|
||||
}}
|
||||
@@ -1400,8 +1404,8 @@ export default function SettingsPage() {
|
||||
loading={testingApi}
|
||||
block
|
||||
style={{
|
||||
borderColor: 'var(--color-success)',
|
||||
color: 'var(--color-success)',
|
||||
borderColor: token.colorSuccess,
|
||||
color: token.colorSuccess,
|
||||
fontWeight: 500,
|
||||
height: '44px'
|
||||
}}
|
||||
@@ -1466,8 +1470,8 @@ export default function SettingsPage() {
|
||||
onClick={handleTestConnection}
|
||||
loading={testingApi}
|
||||
style={{
|
||||
borderColor: 'var(--color-success)',
|
||||
color: 'var(--color-success)',
|
||||
borderColor: token.colorSuccess,
|
||||
color: token.colorSuccess,
|
||||
fontWeight: 500,
|
||||
minWidth: '100px'
|
||||
}}
|
||||
@@ -1491,7 +1495,7 @@ export default function SettingsPage() {
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
style={{
|
||||
background: 'var(--color-primary)',
|
||||
background: token.colorPrimary,
|
||||
border: 'none',
|
||||
minWidth: '120px',
|
||||
fontWeight: 500
|
||||
@@ -1611,7 +1615,7 @@ export default function SettingsPage() {
|
||||
<span>模型名称</span>
|
||||
<InfoCircleOutlined
|
||||
title="AI模型的名称,点击下拉框自动获取可用模型"
|
||||
style={{ color: 'var(--color-text-secondary)', fontSize: '12px' }}
|
||||
style={{ color: token.colorTextSecondary, fontSize: '12px' }}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
@@ -1637,17 +1641,17 @@ export default function SettingsPage() {
|
||||
<>
|
||||
{menu}
|
||||
{fetchingPresetModels && (
|
||||
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: '12px' }}>
|
||||
<div style={{ padding: '8px 12px', color: token.colorTextSecondary, textAlign: 'center', fontSize: '12px' }}>
|
||||
<Spin size="small" /> 正在获取模型列表...
|
||||
</div>
|
||||
)}
|
||||
{!fetchingPresetModels && presetModelOptions.length === 0 && presetModelsFetched && !presetModelSearchText && (
|
||||
<div style={{ padding: '8px 12px', color: '#ff4d4f', textAlign: 'center', fontSize: '12px' }}>
|
||||
<div style={{ padding: '8px 12px', color: token.colorError, textAlign: 'center', fontSize: '12px' }}>
|
||||
未能获取到模型列表,可直接输入模型名称
|
||||
</div>
|
||||
)}
|
||||
{!fetchingPresetModels && presetModelOptions.length === 0 && !presetModelsFetched && !presetModelSearchText && (
|
||||
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: '12px' }}>
|
||||
<div style={{ padding: '8px 12px', color: token.colorTextSecondary, textAlign: 'center', fontSize: '12px' }}>
|
||||
点击输入框自动获取,或直接输入模型名称
|
||||
</div>
|
||||
)}
|
||||
@@ -1714,13 +1718,13 @@ export default function SettingsPage() {
|
||||
<div style={{ fontWeight: 500, fontSize: '13px' }}>
|
||||
{option.data.description === '手动输入的模型名称' ? (
|
||||
<Space size={4}>
|
||||
<EditOutlined style={{ color: 'var(--color-primary)' }} />
|
||||
<EditOutlined style={{ color: token.colorPrimary }} />
|
||||
<span>使用 "{option.data.label}"</span>
|
||||
</Space>
|
||||
) : option.data.label}
|
||||
</div>
|
||||
{option.data.description && option.data.description !== '手动输入的模型名称' && (
|
||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginTop: '2px' }}>
|
||||
<div style={{ fontSize: '11px', color: token.colorTextTertiary, marginTop: '2px' }}>
|
||||
{option.data.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { Card, Row, Col, Typography, Image, Divider, Modal, Button } from 'antd';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import { Card, Row, Col, Typography, Image, Divider, Modal, Button, theme } from 'antd';
|
||||
import {
|
||||
HeartOutlined,
|
||||
CheckCircleOutlined,
|
||||
FileTextOutlined,
|
||||
RocketOutlined,
|
||||
MessageOutlined,
|
||||
StarOutlined
|
||||
// StarOutlined,
|
||||
WechatOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
@@ -18,6 +19,13 @@ interface SponsorOption {
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface SponsorBenefit {
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
price?: string;
|
||||
}
|
||||
|
||||
const sponsorOptions: SponsorOption[] = [
|
||||
{ amount: 5, label: '🌶️ 一包辣条', image: '/5.png', description: '¥5' },
|
||||
{ amount: 10, label: '🍱 一顿拼好饭', image: '/10.png', description: '¥10' },
|
||||
@@ -26,27 +34,39 @@ const sponsorOptions: SponsorOption[] = [
|
||||
{ amount: 99, label: '🍲 一顿海底捞', image: '/99.png', description: '¥99' },
|
||||
];
|
||||
|
||||
const benefits = [
|
||||
const benefits: SponsorBenefit[] = [
|
||||
{
|
||||
icon: <FileTextOutlined style={{ fontSize: '32px', color: 'var(--color-primary)' }} />,
|
||||
icon: <WechatOutlined style={{ fontSize: '32px', color: 'var(--ant-color-primary)' }} />,
|
||||
title: '加入赞助群',
|
||||
description: '进入内部群,获取项目第一手更新消息',
|
||||
price: '(🌶️ 一包辣条)'
|
||||
},
|
||||
{
|
||||
icon: <FileTextOutlined style={{ fontSize: '32px', color: 'var(--ant-color-primary)' }} />,
|
||||
title: '优先需求响应',
|
||||
description: '您的功能需求和问题反馈将获得优先处理'
|
||||
description: '您的功能需求和问题反馈将获得优先处理',
|
||||
price: '(🌶️ 一包辣条)'
|
||||
},
|
||||
{
|
||||
icon: <RocketOutlined style={{ fontSize: '32px', color: 'var(--color-success)' }} />,
|
||||
icon: <RocketOutlined style={{ fontSize: '32px', color: 'var(--ant-color-success)' }} />,
|
||||
title: 'Windows一键启动',
|
||||
description: '获取免安装一键启动包,开箱即可使用'
|
||||
description: '获取免安装一键启动包,开箱即可使用',
|
||||
price: '(🌶️ 一包辣条)'
|
||||
},
|
||||
{
|
||||
icon: <MessageOutlined style={{ fontSize: '32px', color: 'var(--color-warning)' }} />,
|
||||
icon: <MessageOutlined style={{ fontSize: '32px', color: 'var(--ant-color-warning)' }} />,
|
||||
title: '专属技术支持',
|
||||
description: '加入赞助者群,获得远程协助和配置指导(☕ 一杯咖啡)'
|
||||
description: '获得远程协助和配置指导',
|
||||
price: '(☕ 一杯咖啡)'
|
||||
}
|
||||
];
|
||||
|
||||
export default function Sponsor() {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState<SponsorOption | null>(null);
|
||||
const { token } = theme.useToken();
|
||||
const alphaColor = (color: string, alpha: number) =>
|
||||
`color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||
|
||||
const handleCardClick = (option: SponsorOption) => {
|
||||
setSelectedOption(option);
|
||||
@@ -64,10 +84,11 @@ export default function Sponsor() {
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
padding: 'clamp(16px, 3vh, 24px) clamp(12px, 2vw, 16px)'
|
||||
// padding: 'clamp(16px, 3vh, 24px) clamp(12px, 2vw, 16px)'
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: '1200px',
|
||||
// maxWidth: '1200px',
|
||||
height: '100%',
|
||||
margin: '0 auto',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
@@ -76,46 +97,45 @@ export default function Sponsor() {
|
||||
}}>
|
||||
{/* 头部标题区域 */}
|
||||
<div style={{ textAlign: 'center', marginBottom: 'clamp(20px, 4vh, 32px)' }}>
|
||||
<Title level={1} style={{ marginBottom: '8px', fontSize: 'clamp(24px, 5vw, 32px)', fontWeight: 'bold' }}>
|
||||
赞助 MuMuAINovel
|
||||
</Title>
|
||||
<Text type="secondary" style={{ fontSize: 'clamp(11px, 2vw, 13px)', letterSpacing: '2px' }}>
|
||||
SUPPORT AI NOVEL CREATION
|
||||
</Text>
|
||||
|
||||
<div style={{
|
||||
marginTop: 'clamp(12px, 2vh, 16px)',
|
||||
padding: 'clamp(12px, 2vh, 16px)',
|
||||
background: 'var(--color-primary)',
|
||||
background: token.colorPrimary,
|
||||
borderRadius: '12px',
|
||||
color: '#fff'
|
||||
color: token.colorWhite
|
||||
}}>
|
||||
<Title level={4} style={{ color: '#fff', marginBottom: '8px' }}>
|
||||
<Title level={1} style={{ color: token.colorWhite, marginBottom: '8px', fontSize: 'clamp(24px, 5vw, 32px)', fontWeight: 'bold' }}>
|
||||
赞助 MuMuAINovel
|
||||
</Title>
|
||||
<Text type="secondary" style={{ color: token.colorWhite, fontSize: 'clamp(11px, 2vw, 13px)', letterSpacing: '2px' }}>
|
||||
SUPPORT MuMuAINovel
|
||||
</Text>
|
||||
<Title level={4} style={{ color: token.colorWhite, marginTop: '8px', marginBottom: '8px' }}>
|
||||
📚 MuMuAINovel - 基于 AI 的智能小说创作助手
|
||||
</Title>
|
||||
<Paragraph style={{ color: '#fff', fontSize: '14px', margin: 0 }}>
|
||||
支持多AI模型、智能向导、角色管理、章节编辑等强大功能
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 赞助专属权益 */}
|
||||
<div style={{ marginBottom: 'clamp(24px, 4vh, 32px)' }}>
|
||||
<Title level={3} style={{ textAlign: 'center', marginBottom: 'clamp(16px, 3vh, 20px)', fontSize: 'clamp(18px, 3vw, 24px)' }}>
|
||||
<CheckCircleOutlined style={{ color: 'var(--color-success)', marginRight: '8px' }} />
|
||||
<CheckCircleOutlined style={{ color: token.colorSuccess, marginRight: '8px' }} />
|
||||
赞助专属权益
|
||||
</Title>
|
||||
|
||||
<Row gutter={[{ xs: 8, sm: 12, md: 16 }, { xs: 8, sm: 12, md: 16 }]}>
|
||||
<Row
|
||||
gutter={[{ xs: 8, sm: 12, md: 16 }, { xs: 8, sm: 12, md: 16 }]}
|
||||
wrap={false}
|
||||
style={{ overflowX: 'auto', paddingBottom: '4px' }}
|
||||
>
|
||||
{benefits.map((benefit, index) => (
|
||||
<Col xs={24} md={8} key={index}>
|
||||
<Col key={index} flex="1" style={{ minWidth: '200px' }}>
|
||||
<Card
|
||||
hoverable
|
||||
style={{
|
||||
height: '100%',
|
||||
textAlign: 'center',
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)'
|
||||
boxShadow: `0 2px 8px ${alphaColor(token.colorTextBase, 0.12)}`
|
||||
}}
|
||||
styles={{
|
||||
body: { padding: 'clamp(16px, 3vh, 20px) clamp(12px, 2vw, 16px)' }
|
||||
@@ -125,9 +145,14 @@ export default function Sponsor() {
|
||||
{benefit.icon}
|
||||
</div>
|
||||
<Title level={5} style={{ marginBottom: '8px', fontSize: 'clamp(14px, 2.5vw, 16px)' }}>{benefit.title}</Title>
|
||||
<Paragraph style={{ color: '#666', marginBottom: 0, fontSize: 'clamp(12px, 2vw, 13px)' }}>
|
||||
<Paragraph style={{ color: token.colorTextSecondary, marginBottom: 0, fontSize: 'clamp(12px, 2vw, 13px)' }}>
|
||||
{benefit.description}
|
||||
</Paragraph>
|
||||
{benefit.price && (
|
||||
<Paragraph style={{ color: token.colorWarning, margin: '4px 0 0', fontSize: 'clamp(12px, 2vw, 13px)', fontWeight: 600 }}>
|
||||
{benefit.price}
|
||||
</Paragraph>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
@@ -135,9 +160,9 @@ export default function Sponsor() {
|
||||
</div>
|
||||
|
||||
{/* 选择金额 */}
|
||||
<div style={{ marginBottom: 'clamp(24px, 4vh, 32px)' }}>
|
||||
<div>
|
||||
<Title level={3} style={{ textAlign: 'center', marginBottom: 'clamp(16px, 3vh, 20px)', fontSize: 'clamp(18px, 3vw, 24px)' }}>
|
||||
<HeartOutlined style={{ color: '#f5222d', marginRight: '8px' }} />
|
||||
<HeartOutlined style={{ color: token.colorError, marginRight: '8px' }} />
|
||||
选择金额
|
||||
</Title>
|
||||
|
||||
@@ -150,34 +175,34 @@ export default function Sponsor() {
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
borderRadius: '10px',
|
||||
boxShadow: 'var(--shadow-card)',
|
||||
boxShadow: `0 2px 8px ${alphaColor(token.colorTextBase, 0.12)}`,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s',
|
||||
border: '2px solid var(--color-border)'
|
||||
border: `2px solid ${token.colorBorder}`
|
||||
}}
|
||||
styles={{
|
||||
body: { padding: 'clamp(16px, 3vh, 20px) clamp(10px, 2vw, 12px)' }
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-8px)';
|
||||
e.currentTarget.style.boxShadow = 'var(--shadow-elevated)';
|
||||
e.currentTarget.style.borderColor = 'var(--color-primary)';
|
||||
e.currentTarget.style.boxShadow = `0 8px 24px ${alphaColor(token.colorPrimary, 0.3)}`;
|
||||
e.currentTarget.style.borderColor = token.colorPrimary;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = 'var(--shadow-card)';
|
||||
e.currentTarget.style.borderColor = 'var(--color-border)';
|
||||
e.currentTarget.style.boxShadow = `0 2px 8px ${alphaColor(token.colorTextBase, 0.12)}`;
|
||||
e.currentTarget.style.borderColor = token.colorBorder;
|
||||
}}
|
||||
>
|
||||
<Title level={3} style={{
|
||||
color: 'var(--color-primary)',
|
||||
color: token.colorPrimary,
|
||||
marginBottom: '4px',
|
||||
fontSize: 'clamp(20px, 4vw, 28px)',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{option.description}
|
||||
</Title>
|
||||
<Text style={{ fontSize: 'clamp(12px, 2vw, 14px)', color: '#666' }}>
|
||||
<Text style={{ fontSize: 'clamp(12px, 2vw, 14px)', color: token.colorTextSecondary }}>
|
||||
{option.label}
|
||||
</Text>
|
||||
</Card>
|
||||
@@ -186,29 +211,29 @@ export default function Sponsor() {
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: 'clamp(16px, 3vh, 24px) 0' }} />
|
||||
<Divider style={{ margin: 'clamp(16px, 3vh, 18px) 0' }} />
|
||||
|
||||
{/* 感谢文案 */}
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: 'clamp(16px, 3vh, 24px) clamp(16px, 3vw, 20px)',
|
||||
background: '#f9f9f9',
|
||||
padding: 'clamp(16px, 3vw, 20px)',
|
||||
background: token.colorFillQuaternary,
|
||||
borderRadius: '10px',
|
||||
marginTop: 'auto'
|
||||
}}>
|
||||
<Title level={4} style={{ marginBottom: '12px', fontSize: 'clamp(16px, 3vw, 20px)' }}>
|
||||
💖 感谢您对 MuMuAINovel 项目的支持
|
||||
</Title>
|
||||
<Paragraph style={{ fontSize: 'clamp(12px, 2vw, 14px)', color: '#666', marginBottom: '12px' }}>
|
||||
您的赞助将是我持续更新项目的动力,为大家提供更好的AI小说创作体验
|
||||
<Paragraph style={{ fontSize: 'clamp(12px, 2vw, 14px)', color: token.colorTextSecondary, marginBottom: '12px' }}>
|
||||
您的赞助将是我持续更新项目的动力,为大家提供更好的AI小说创作体验!
|
||||
</Paragraph>
|
||||
<div style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>
|
||||
<StarOutlined style={{ color: '#faad14', margin: '0 4px' }} />
|
||||
<StarOutlined style={{ color: '#faad14', margin: '0 4px' }} />
|
||||
<StarOutlined style={{ color: '#faad14', margin: '0 4px' }} />
|
||||
<StarOutlined style={{ color: '#faad14', margin: '0 4px' }} />
|
||||
<StarOutlined style={{ color: '#faad14', margin: '0 4px' }} />
|
||||
</div>
|
||||
{/* <div style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>
|
||||
<StarOutlined style={{ color: token.colorWarning, margin: '0 4px' }} />
|
||||
<StarOutlined style={{ color: token.colorWarning, margin: '0 4px' }} />
|
||||
<StarOutlined style={{ color: token.colorWarning, margin: '0 4px' }} />
|
||||
<StarOutlined style={{ color: token.colorWarning, margin: '0 4px' }} />
|
||||
<StarOutlined style={{ color: token.colorWarning, margin: '0 4px' }} />
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -240,14 +265,14 @@ export default function Sponsor() {
|
||||
style={{
|
||||
maxWidth: '280px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #f0f0f0'
|
||||
border: `1px solid ${token.colorBorderSecondary}`
|
||||
}}
|
||||
preview={false}
|
||||
/>
|
||||
<Paragraph style={{ marginTop: '20px', color: '#666' }}>
|
||||
<Paragraph style={{ marginTop: '20px', color: token.colorTextSecondary }}>
|
||||
扫描二维码完成支付
|
||||
</Paragraph>
|
||||
<Paragraph style={{ color: '#999', fontSize: '12px' }}>
|
||||
<Paragraph style={{ color: token.colorTextTertiary, fontSize: '12px' }}>
|
||||
支付后可添加微信/QQ联系我们获取权益
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Col,
|
||||
Pagination,
|
||||
Dropdown,
|
||||
theme,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
@@ -72,6 +73,8 @@ export default function UserManagement() {
|
||||
const [form] = Form.useForm();
|
||||
const [editForm] = Form.useForm();
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const { token } = theme.useToken();
|
||||
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||
|
||||
// 过滤用户列表
|
||||
const filteredUsers = users.filter(user => {
|
||||
@@ -180,7 +183,7 @@ export default function UserManagement() {
|
||||
<div>
|
||||
<p>用户名:<Text strong>{values.username}</Text></p>
|
||||
<p>初始密码:<Text strong copyable>{res.default_password}</Text></p>
|
||||
<p style={{ color: '#ff4d4f', marginTop: 16 }}>
|
||||
<p style={{ color: token.colorError, marginTop: 16 }}>
|
||||
⚠️ 请复制密码并告知用户,此密码仅显示一次!
|
||||
</p>
|
||||
</div>
|
||||
@@ -270,7 +273,7 @@ export default function UserManagement() {
|
||||
<div>
|
||||
<p>用户:<Text strong>{currentUser.username}</Text></p>
|
||||
<p>新密码:<Text strong copyable>{res.new_password}</Text></p>
|
||||
<p style={{ color: '#ff4d4f', marginTop: 16 }}>
|
||||
<p style={{ color: token.colorError, marginTop: 16 }}>
|
||||
⚠️ 请复制密码并告知用户!
|
||||
</p>
|
||||
</div>
|
||||
@@ -312,7 +315,7 @@ export default function UserManagement() {
|
||||
sortOrder: sortField === 'username' ? sortOrder : null,
|
||||
render: (text: string) => (
|
||||
<Space>
|
||||
<UserOutlined style={{ color: 'var(--color-primary)' }} />
|
||||
<UserOutlined style={{ color: token.colorPrimary }} />
|
||||
<Text strong>{text}</Text>
|
||||
</Space>
|
||||
),
|
||||
@@ -508,7 +511,7 @@ export default function UserManagement() {
|
||||
return (
|
||||
<div style={{
|
||||
height: '100vh',
|
||||
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)',
|
||||
background: `linear-gradient(180deg, ${token.colorBgLayout} 0%, ${alphaColor(token.colorPrimary, 0.08)} 100%)`,
|
||||
padding: isMobile ? '20px 16px' : '40px 24px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -528,9 +531,9 @@ export default function UserManagement() {
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, #5A9BA5 50%, var(--color-primary-hover) 100%)',
|
||||
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${alphaColor(token.colorPrimary, 0.8)} 50%, ${token.colorPrimaryHover} 100%)`,
|
||||
borderRadius: isMobile ? 16 : 24,
|
||||
boxShadow: '0 12px 40px rgba(77, 128, 136, 0.25), 0 4px 12px rgba(0, 0, 0, 0.06)',
|
||||
boxShadow: `0 12px 40px ${alphaColor(token.colorPrimary, 0.25)}, 0 4px 12px ${alphaColor(token.colorText, 0.08)}`,
|
||||
marginBottom: isMobile ? 20 : 24,
|
||||
border: 'none',
|
||||
position: 'relative',
|
||||
@@ -538,18 +541,18 @@ export default function UserManagement() {
|
||||
}}
|
||||
>
|
||||
{/* 装饰性背景元素 */}
|
||||
<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' }} />
|
||||
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: alphaColor(token.colorWhite, 0.08), pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: alphaColor(token.colorWhite, 0.05), pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: alphaColor(token.colorWhite, 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, color: '#fff', textShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
|
||||
<TeamOutlined style={{ color: 'rgba(255,255,255,0.9)', marginRight: 12 }} />
|
||||
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: token.colorWhite, textShadow: `0 2px 4px ${alphaColor(token.colorText, 0.2)}` }}>
|
||||
<TeamOutlined style={{ color: alphaColor(token.colorWhite, 0.9), marginRight: 12 }} />
|
||||
用户管理
|
||||
</Title>
|
||||
<Text style={{ fontSize: isMobile ? 12 : 14, color: 'rgba(255,255,255,0.85)' }}>
|
||||
<Text style={{ fontSize: isMobile ? 12 : 14, color: alphaColor(token.colorWhite, 0.85) }}>
|
||||
管理系统用户和权限
|
||||
</Text>
|
||||
</Space>
|
||||
@@ -561,19 +564,19 @@ export default function UserManagement() {
|
||||
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',
|
||||
background: alphaColor(token.colorWhite, 0.15),
|
||||
border: `1px solid ${alphaColor(token.colorWhite, 0.3)}`,
|
||||
boxShadow: `0 2px 8px ${alphaColor(token.colorText, 0.15)}`,
|
||||
color: token.colorWhite,
|
||||
backdropFilter: 'blur(10px)',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
|
||||
e.currentTarget.style.background = alphaColor(token.colorWhite, 0.25);
|
||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
|
||||
e.currentTarget.style.background = alphaColor(token.colorWhite, 0.15);
|
||||
e.currentTarget.style.transform = 'none';
|
||||
}}
|
||||
>
|
||||
@@ -585,10 +588,10 @@ export default function UserManagement() {
|
||||
onClick={() => setModalVisible(true)}
|
||||
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',
|
||||
background: alphaColor(token.colorWarning, 0.95),
|
||||
border: `1px solid ${alphaColor(token.colorWhite, 0.3)}`,
|
||||
boxShadow: `0 4px 16px ${alphaColor(token.colorWarning, 0.4)}`,
|
||||
color: token.colorWhite,
|
||||
fontWeight: 600
|
||||
}}
|
||||
>
|
||||
@@ -604,11 +607,11 @@ export default function UserManagement() {
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
background: 'rgba(255, 255, 255, 0.7)',
|
||||
background: alphaColor(token.colorBgContainer, 0.72),
|
||||
borderRadius: isMobile ? 16 : 24,
|
||||
border: '1px solid rgba(255, 255, 255, 0.4)',
|
||||
border: `1px solid ${alphaColor(token.colorWhite, 0.45)}`,
|
||||
backdropFilter: 'blur(20px)',
|
||||
boxShadow: '0 4px 24px rgba(0, 0, 0, 0.04)',
|
||||
boxShadow: `0 4px 24px ${alphaColor(token.colorText, 0.06)}`,
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -625,11 +628,11 @@ export default function UserManagement() {
|
||||
{/* 搜索栏 */}
|
||||
<div style={{
|
||||
padding: '16px 24px 0 24px',
|
||||
borderBottom: '1px solid rgba(0, 0, 0, 0.03)',
|
||||
borderBottom: `1px solid ${alphaColor(token.colorText, 0.06)}`,
|
||||
}}>
|
||||
<Input
|
||||
placeholder="搜索用户名、显示名称或用户ID"
|
||||
prefix={<SearchOutlined style={{ color: '#999' }} />}
|
||||
prefix={<SearchOutlined style={{ color: token.colorTextTertiary }} />}
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
setSearchText(e.target.value);
|
||||
@@ -676,7 +679,7 @@ export default function UserManagement() {
|
||||
{/* 固定分页控件 */}
|
||||
<div style={{
|
||||
padding: '16px 24px 24px 24px',
|
||||
borderTop: '1px solid rgba(0, 0, 0, 0.03)',
|
||||
borderTop: `1px solid ${alphaColor(token.colorText, 0.06)}`,
|
||||
background: 'transparent',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Card, Descriptions, Empty, Typography, Button, Modal, Form, Input, message, Flex, InputNumber, Select } from 'antd';
|
||||
import { Card, Descriptions, Empty, Typography, Button, Modal, Form, Input, message, Flex, InputNumber, Select, theme } from 'antd';
|
||||
import { GlobalOutlined, EditOutlined, SyncOutlined, FormOutlined } from '@ant-design/icons';
|
||||
import { useState } from 'react';
|
||||
import { useStore } from '../store';
|
||||
import { cardStyles } from '../components/CardStyles';
|
||||
import { worldSettingCardStyles } from '../components/CardStyles';
|
||||
import { projectApi, wizardStreamApi } from '../services/api';
|
||||
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function WorldSetting() {
|
||||
} | null>(null);
|
||||
const [isSavingPreview, setIsSavingPreview] = useState(false);
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const { token } = theme.useToken();
|
||||
|
||||
// AI重新生成世界观
|
||||
const handleRegenerate = async () => {
|
||||
@@ -140,14 +141,14 @@ export default function WorldSetting() {
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: token.colorBgContainer,
|
||||
padding: '16px 0',
|
||||
marginBottom: 16,
|
||||
borderBottom: '1px solid var(--color-border-secondary)',
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: 'var(--color-primary)' }} />
|
||||
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: token.colorPrimary }} />
|
||||
<h2 style={{ margin: 0 }}>世界设定</h2>
|
||||
</div>
|
||||
|
||||
@@ -174,10 +175,10 @@ export default function WorldSetting() {
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: token.colorBgContainer,
|
||||
padding: '16px 0',
|
||||
marginBottom: 24,
|
||||
borderBottom: '1px solid #f0f0f0'
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`
|
||||
}}>
|
||||
<Flex
|
||||
justify="space-between"
|
||||
@@ -186,7 +187,7 @@ export default function WorldSetting() {
|
||||
wrap="wrap"
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', minWidth: 'fit-content' }}>
|
||||
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: 'var(--color-primary)' }} />
|
||||
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: token.colorPrimary }} />
|
||||
<h2 style={{ margin: 0, whiteSpace: 'nowrap' }}>世界设定</h2>
|
||||
</div>
|
||||
<Flex gap={8} wrap="wrap" style={{ flex: '0 1 auto' }}>
|
||||
@@ -249,7 +250,7 @@ export default function WorldSetting() {
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
<Card
|
||||
style={{
|
||||
...cardStyles.base,
|
||||
...worldSettingCardStyles.sectionCard,
|
||||
marginBottom: 16
|
||||
}}
|
||||
title={
|
||||
@@ -274,7 +275,7 @@ export default function WorldSetting() {
|
||||
|
||||
<Card
|
||||
style={{
|
||||
...cardStyles.base,
|
||||
...worldSettingCardStyles.sectionCard,
|
||||
marginBottom: 16
|
||||
}}
|
||||
title={
|
||||
@@ -287,16 +288,16 @@ export default function WorldSetting() {
|
||||
<div style={{ padding: '16px 0' }}>
|
||||
{currentProject.world_time_period && (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Title level={5} style={{ color: 'var(--color-primary)', marginBottom: 12 }}>
|
||||
<Title level={5} style={{ color: token.colorPrimary, marginBottom: 12 }}>
|
||||
时间设定
|
||||
</Title>
|
||||
<Paragraph style={{
|
||||
fontSize: 15,
|
||||
lineHeight: 1.8,
|
||||
padding: 16,
|
||||
background: 'var(--color-bg-layout)',
|
||||
background: token.colorBgLayout,
|
||||
borderRadius: 8,
|
||||
borderLeft: '4px solid var(--color-primary)'
|
||||
borderLeft: `4px solid ${token.colorPrimary}`
|
||||
}}>
|
||||
{currentProject.world_time_period}
|
||||
</Paragraph>
|
||||
@@ -305,16 +306,16 @@ export default function WorldSetting() {
|
||||
|
||||
{currentProject.world_location && (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Title level={5} style={{ color: 'var(--color-success)', marginBottom: 12 }}>
|
||||
<Title level={5} style={{ color: token.colorSuccess, marginBottom: 12 }}>
|
||||
地点设定
|
||||
</Title>
|
||||
<Paragraph style={{
|
||||
fontSize: 15,
|
||||
lineHeight: 1.8,
|
||||
padding: 16,
|
||||
background: 'var(--color-bg-layout)',
|
||||
background: token.colorBgLayout,
|
||||
borderRadius: 8,
|
||||
borderLeft: '4px solid var(--color-success)'
|
||||
borderLeft: `4px solid ${token.colorSuccess}`
|
||||
}}>
|
||||
{currentProject.world_location}
|
||||
</Paragraph>
|
||||
@@ -323,16 +324,16 @@ export default function WorldSetting() {
|
||||
|
||||
{currentProject.world_atmosphere && (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Title level={5} style={{ color: 'var(--color-warning)', marginBottom: 12 }}>
|
||||
<Title level={5} style={{ color: token.colorWarning, marginBottom: 12 }}>
|
||||
氛围设定
|
||||
</Title>
|
||||
<Paragraph style={{
|
||||
fontSize: 15,
|
||||
lineHeight: 1.8,
|
||||
padding: 16,
|
||||
background: 'var(--color-bg-layout)',
|
||||
background: token.colorBgLayout,
|
||||
borderRadius: 8,
|
||||
borderLeft: '4px solid var(--color-warning)'
|
||||
borderLeft: `4px solid ${token.colorWarning}`
|
||||
}}>
|
||||
{currentProject.world_atmosphere}
|
||||
</Paragraph>
|
||||
@@ -341,16 +342,16 @@ export default function WorldSetting() {
|
||||
|
||||
{currentProject.world_rules && (
|
||||
<div style={{ marginBottom: 0 }}>
|
||||
<Title level={5} style={{ color: 'var(--color-error)', marginBottom: 12 }}>
|
||||
<Title level={5} style={{ color: token.colorError, marginBottom: 12 }}>
|
||||
规则设定
|
||||
</Title>
|
||||
<Paragraph style={{
|
||||
fontSize: 15,
|
||||
lineHeight: 1.8,
|
||||
padding: 16,
|
||||
background: 'var(--color-bg-layout)',
|
||||
background: token.colorBgLayout,
|
||||
borderRadius: 8,
|
||||
borderLeft: '4px solid var(--color-error)'
|
||||
borderLeft: `4px solid ${token.colorError}`
|
||||
}}>
|
||||
{currentProject.world_rules}
|
||||
</Paragraph>
|
||||
@@ -616,71 +617,71 @@ export default function WorldSetting() {
|
||||
>
|
||||
{newWorldData && (
|
||||
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
||||
<div style={{ marginBottom: 24, padding: 16, background: 'var(--color-warning-bg)', border: '1px solid var(--color-warning-border)', borderRadius: 8 }}>
|
||||
<div style={{ marginBottom: 24, padding: 16, background: token.colorWarningBg, border: `1px solid ${token.colorWarningBorder}`, borderRadius: 8 }}>
|
||||
<Typography.Text type="warning" strong>
|
||||
⚠️ 注意:点击"确认替换"将会用新内容替换当前的世界观设定
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Title level={5} style={{ color: 'var(--color-primary)', marginBottom: 12 }}>
|
||||
<Title level={5} style={{ color: token.colorPrimary, marginBottom: 12 }}>
|
||||
时间设定
|
||||
</Title>
|
||||
<Paragraph style={{
|
||||
fontSize: 15,
|
||||
lineHeight: 1.8,
|
||||
padding: 16,
|
||||
background: '#f5f5f5',
|
||||
background: token.colorBgLayout,
|
||||
borderRadius: 8,
|
||||
borderLeft: '4px solid #1890ff'
|
||||
borderLeft: `4px solid ${token.colorPrimary}`
|
||||
}}>
|
||||
{newWorldData.time_period}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Title level={5} style={{ color: '#52c41a', marginBottom: 12 }}>
|
||||
<Title level={5} style={{ color: token.colorSuccess, marginBottom: 12 }}>
|
||||
地点设定
|
||||
</Title>
|
||||
<Paragraph style={{
|
||||
fontSize: 15,
|
||||
lineHeight: 1.8,
|
||||
padding: 16,
|
||||
background: '#f5f5f5',
|
||||
background: token.colorBgLayout,
|
||||
borderRadius: 8,
|
||||
borderLeft: '4px solid #52c41a'
|
||||
borderLeft: `4px solid ${token.colorSuccess}`
|
||||
}}>
|
||||
{newWorldData.location}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Title level={5} style={{ color: '#faad14', marginBottom: 12 }}>
|
||||
<Title level={5} style={{ color: token.colorWarning, marginBottom: 12 }}>
|
||||
氛围设定
|
||||
</Title>
|
||||
<Paragraph style={{
|
||||
fontSize: 15,
|
||||
lineHeight: 1.8,
|
||||
padding: 16,
|
||||
background: '#f5f5f5',
|
||||
background: token.colorBgLayout,
|
||||
borderRadius: 8,
|
||||
borderLeft: '4px solid #faad14'
|
||||
borderLeft: `4px solid ${token.colorWarning}`
|
||||
}}>
|
||||
{newWorldData.atmosphere}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 0 }}>
|
||||
<Title level={5} style={{ color: '#f5222d', marginBottom: 12 }}>
|
||||
<Title level={5} style={{ color: token.colorError, marginBottom: 12 }}>
|
||||
规则设定
|
||||
</Title>
|
||||
<Paragraph style={{
|
||||
fontSize: 15,
|
||||
lineHeight: 1.8,
|
||||
padding: 16,
|
||||
background: '#f5f5f5',
|
||||
background: token.colorBgLayout,
|
||||
borderRadius: 8,
|
||||
borderLeft: '4px solid #f5222d'
|
||||
borderLeft: `4px solid ${token.colorError}`
|
||||
}}>
|
||||
{newWorldData.rules}
|
||||
</Paragraph>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Typography,
|
||||
Row,
|
||||
Col,
|
||||
theme,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
@@ -38,6 +39,8 @@ export default function WritingStyles() {
|
||||
const [createForm] = Form.useForm();
|
||||
const [editForm] = Form.useForm();
|
||||
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
|
||||
// 卡片网格配置
|
||||
@@ -170,10 +173,10 @@ export default function WritingStyles() {
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: token.colorBgContainer,
|
||||
padding: isMobile ? '12px 0' : '16px 0',
|
||||
marginBottom: isMobile ? 12 : 16,
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
@@ -220,7 +223,7 @@ export default function WritingStyles() {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: 12,
|
||||
border: style.is_default ? '2px solid #1890ff' : '1px solid #f0f0f0',
|
||||
border: style.is_default ? `2px solid ${token.colorPrimary}` : `1px solid ${token.colorBorderSecondary}`,
|
||||
}}
|
||||
bodyStyle={{
|
||||
flex: 1,
|
||||
@@ -235,7 +238,7 @@ export default function WritingStyles() {
|
||||
style={{ cursor: style.is_default ? 'default' : 'pointer' }}
|
||||
>
|
||||
{style.is_default ? (
|
||||
<StarFilled style={{ color: '#faad14', fontSize: 18 }} />
|
||||
<StarFilled style={{ color: token.colorWarning, fontSize: 18 }} />
|
||||
) : (
|
||||
<StarOutlined style={{ fontSize: 18 }} />
|
||||
)}
|
||||
@@ -246,7 +249,7 @@ export default function WritingStyles() {
|
||||
style={{
|
||||
fontSize: 18,
|
||||
cursor: style.user_id === null ? 'not-allowed' : 'pointer',
|
||||
color: style.user_id === null ? '#ccc' : undefined
|
||||
color: style.user_id === null ? token.colorTextQuaternary : undefined
|
||||
}}
|
||||
/>,
|
||||
<Popconfirm
|
||||
@@ -261,7 +264,7 @@ export default function WritingStyles() {
|
||||
<DeleteOutlined
|
||||
style={{
|
||||
fontSize: 18,
|
||||
color: style.user_id === null ? '#ccc' : undefined,
|
||||
color: style.user_id === null ? token.colorTextQuaternary : undefined,
|
||||
cursor: style.user_id === null ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
/>
|
||||
@@ -292,7 +295,7 @@ export default function WritingStyles() {
|
||||
style={{
|
||||
fontSize: 12,
|
||||
marginBottom: 0,
|
||||
backgroundColor: '#fafafa',
|
||||
backgroundColor: token.colorFillAlter,
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
flex: 1,
|
||||
|
||||
Reference in New Issue
Block a user