refactor: 大量前端页面/组件样式从硬编码颜色迁移到 antd token 主题变量

This commit is contained in:
xiamuceer-j
2026-03-06 14:14:57 +08:00
parent 7c9716b485
commit f1d7975ea4
40 changed files with 1755 additions and 1375 deletions
+13 -12
View File
@@ -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';
}}
>
+28 -26
View File
@@ -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>
+27 -25
View File
@@ -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>
+3 -2
View File
@@ -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)',
+29 -22
View File
@@ -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];
// 更新设置的便捷函数
+13 -11
View File
@@ -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 />}
+22 -18
View File
@@ -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',
+15 -13
View File
@@ -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
}}>
,
+7 -4
View File
@@ -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>
+12 -10
View File
@@ -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
}}>
+27 -23
View File
@@ -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',
}}>
+140 -329
View File
@@ -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;
}
}
+12 -10
View File
@@ -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>
</>
+160 -13
View File
@@ -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>
);
}
+9 -8
View File
@@ -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}
+9 -7
View File
@@ -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
+42 -41
View File
@@ -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>
+32 -31
View File
@@ -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>
+12 -11
View File
@@ -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>
)}
+27 -26
View File
@@ -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 ? '重新' : '重新开始'}
+89 -63
View File
@@ -16,6 +16,7 @@ import {
Alert,
Row,
Col,
theme,
} from 'antd';
import {
PlusOutlined,
@@ -39,6 +40,31 @@ 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(() => {
@@ -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
}}
>
+12 -11
View File
@@ -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
View File
@@ -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'
-76
View File
@@ -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>
);
}
+283 -51
View File
@@ -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',
+18 -17
View File
@@ -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 }} />
(11)
</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 }} />
(1N)
</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 }} />
+40 -34
View File
@@ -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>
+17 -15
View File
@@ -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,
+252 -115
View File
@@ -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 }}>
+3 -2
View File
@@ -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 => (
+69 -65
View File
@@ -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>
)}
+81 -56
View File
@@ -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>
+31 -28
View File
@@ -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',
+36 -35
View File
@@ -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>
+10 -7
View File
@@ -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,