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 React, { useMemo, useEffect, useRef } from 'react';
import { theme } from 'antd';
// 标注数据类型 // 标注数据类型
export interface MemoryAnnotation { export interface MemoryAnnotation {
@@ -35,14 +36,6 @@ interface AnnotatedTextProps {
style?: React.CSSProperties; style?: React.CSSProperties;
} }
// 类型颜色映射
const TYPE_COLORS = {
hook: '#ff6b6b',
foreshadow: '#6b7bff',
plot_point: '#51cf66',
character_event: '#ffd93d',
};
// 类型图标映射 // 类型图标映射
const TYPE_ICONS = { const TYPE_ICONS = {
hook: '🎣', hook: '🎣',
@@ -65,6 +58,14 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
}) => { }) => {
const annotationRefs = useRef<Record<string, HTMLSpanElement | null>>({}); 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(() => { useEffect(() => {
if (scrollToAnnotation && annotationRefs.current[scrollToAnnotation]) { if (scrollToAnnotation && annotationRefs.current[scrollToAnnotation]) {
@@ -214,7 +215,7 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
const { annotation, annotations } = segment; const { annotation, annotations } = segment;
if (!annotation) return null; if (!annotation) return null;
const color = TYPE_COLORS[annotation.type]; const color = typeColors[annotation.type];
const icon = TYPE_ICONS[annotation.type]; const icon = TYPE_ICONS[annotation.type];
const isActive = activeAnnotationId === annotation.id; const isActive = activeAnnotationId === annotation.id;
@@ -238,17 +239,17 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
position: 'relative', position: 'relative',
borderBottom: `2px solid ${color}`, borderBottom: `2px solid ${color}`,
cursor: 'pointer', cursor: 'pointer',
backgroundColor: isActive ? `${color}22` : 'transparent', backgroundColor: isActive ? `color-mix(in srgb, ${color} 13%, transparent)` : 'transparent',
transition: 'all 0.2s', transition: 'all 0.2s',
padding: '2px 0', padding: '2px 0',
}} }}
onClick={() => onAnnotationClick?.(annotation)} onClick={() => onAnnotationClick?.(annotation)}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = `${color}33`; e.currentTarget.style.backgroundColor = `color-mix(in srgb, ${color} 20%, transparent)`;
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = isActive e.currentTarget.style.backgroundColor = isActive
? `${color}22` ? `color-mix(in srgb, ${color} 13%, transparent)`
: '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'; import { useEffect, useState } from 'react';
interface AnnouncementModalProps { interface AnnouncementModalProps {
@@ -11,6 +11,8 @@ interface AnnouncementModalProps {
export default function AnnouncementModal({ visible, onClose, onDoNotShowToday, onNeverShow }: AnnouncementModalProps) { export default function AnnouncementModal({ visible, onClose, onDoNotShowToday, onNeverShow }: AnnouncementModalProps) {
const [qqImageError, setQqImageError] = useState(false); const [qqImageError, setQqImageError] = useState(false);
const [wxImageError, setWxImageError] = 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(() => { useEffect(() => {
if (visible) { if (visible) {
@@ -35,7 +37,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
<div style={{ <div style={{
fontSize: '20px', fontSize: '20px',
fontWeight: 600, fontWeight: 600,
color: 'var(--color-primary)', color: token.colorPrimary,
textAlign: 'center', textAlign: 'center',
}}> }}>
🎉 使 AI小说创作助手 🎉 使 AI小说创作助手
@@ -64,9 +66,9 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
borderRadius: '8px', borderRadius: '8px',
height: '40px', height: '40px',
fontSize: '14px', fontSize: '14px',
background: 'var(--color-primary)', background: token.colorPrimary,
borderColor: 'var(--color-primary)', borderColor: token.colorPrimary,
boxShadow: 'var(--shadow-primary)', boxShadow: `0 8px 20px ${alphaColor(token.colorPrimary, 0.32)}`,
}} }}
> >
@@ -78,16 +80,16 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
styles={{ styles={{
body: { body: {
padding: '20px', padding: '20px',
background: 'var(--color-bg-container)', background: token.colorBgContainer,
}, },
header: { header: {
background: 'linear-gradient(135deg, rgba(77, 128, 136, 0.08) 0%, rgba(248, 246, 241, 0.95) 100%)', background: `linear-gradient(135deg, ${alphaColor(token.colorPrimary, 0.1)} 0%, ${alphaColor(token.colorBgContainer, 0.98)} 100%)`,
borderBottom: '1px solid var(--color-border-secondary)', borderBottom: `1px solid ${token.colorBorderSecondary}`,
padding: '16px 24px', padding: '16px 24px',
}, },
footer: { footer: {
background: 'var(--color-bg-container)', background: token.colorBgContainer,
borderTop: '1px solid var(--color-border-secondary)', borderTop: `1px solid ${token.colorBorderSecondary}`,
padding: '16px 24px', padding: '16px 24px',
}, },
}} }}
@@ -96,7 +98,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
<div style={{ <div style={{
marginBottom: '12px', marginBottom: '12px',
fontSize: '15px', fontSize: '15px',
color: 'var(--color-text-secondary)', color: token.colorTextSecondary,
lineHeight: '1.5', lineHeight: '1.5',
}}> }}>
<p style={{ marginBottom: '8px' }}>👋 </p> <p style={{ marginBottom: '8px' }}>👋 </p>
@@ -111,7 +113,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
<li>🐛 </li> <li>🐛 </li>
<li>📚 </li> <li>📚 </li>
</ul> </ul>
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '12px' }}> <p style={{ fontWeight: 600, color: token.colorText, marginBottom: '12px' }}>
</p> </p>
</div> </div>
@@ -122,7 +124,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
alignItems: 'flex-start', alignItems: 'flex-start',
gap: '24px', gap: '24px',
padding: '16px', padding: '16px',
background: 'var(--color-bg-layout)', background: token.colorBgLayout,
borderRadius: '8px', borderRadius: '8px',
flexWrap: 'wrap', flexWrap: 'wrap',
}}> }}>
@@ -133,7 +135,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
alignItems: 'center', alignItems: 'center',
minWidth: '200px', 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交流群 QQ交流群
</p> </p>
{!qqImageError ? ( {!qqImageError ? (
@@ -141,10 +143,10 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
background: 'var(--color-bg-container)', background: token.colorBgContainer,
borderRadius: '8px', borderRadius: '8px',
padding: '6px', padding: '6px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', boxShadow: `0 2px 8px ${alphaColor(token.colorText, 0.12)}`,
}}> }}>
<img <img
src="/qq.jpg" src="/qq.jpg"
@@ -167,9 +169,9 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
background: 'var(--color-bg-container)', background: token.colorBgContainer,
borderRadius: '8px', borderRadius: '8px',
color: '#999', color: token.colorTextTertiary,
}}> }}>
<p></p> <p></p>
</div> </div>
@@ -183,7 +185,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
alignItems: 'center', alignItems: 'center',
minWidth: '200px', 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> </p>
{!wxImageError ? ( {!wxImageError ? (
@@ -191,10 +193,10 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
background: 'var(--color-bg-container)', background: token.colorBgContainer,
borderRadius: '8px', borderRadius: '8px',
padding: '6px', padding: '6px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', boxShadow: `0 2px 8px ${alphaColor(token.colorText, 0.12)}`,
}}> }}>
<img <img
src="/WX.png" src="/WX.png"
@@ -217,9 +219,9 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
background: 'var(--color-bg-container)', background: token.colorBgContainer,
borderRadius: '8px', borderRadius: '8px',
color: '#999', color: token.colorTextTertiary,
}}> }}>
<p></p> <p></p>
</div> </div>
@@ -230,11 +232,11 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
<div style={{ <div style={{
marginTop: '16px', marginTop: '16px',
padding: '10px', padding: '10px',
background: 'var(--color-warning-bg)', background: token.colorWarningBg,
borderRadius: '8px', borderRadius: '8px',
border: '1px solid var(--color-warning-border)', border: `1px solid ${token.colorWarningBorder}`,
fontSize: '13px', fontSize: '13px',
color: 'var(--color-warning)', color: token.colorWarning,
}}> }}>
💡 "今日内不再展示""永不再展示" 💡 "今日内不再展示""永不再展示"
</div> </div>
+27 -25
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; 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 { GithubOutlined, CopyrightOutlined, HeartFilled, ClockCircleOutlined, GiftOutlined } from '@ant-design/icons';
import { VERSION_INFO, getVersionString } from '../config/version'; import { VERSION_INFO, getVersionString } from '../config/version';
import { checkLatestVersion } from '../services/versionService'; import { checkLatestVersion } from '../services/versionService';
@@ -17,6 +17,8 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
const [hasUpdate, setHasUpdate] = useState(false); const [hasUpdate, setHasUpdate] = useState(false);
const [latestVersion, setLatestVersion] = useState(''); const [latestVersion, setLatestVersion] = useState('');
const [releaseUrl, setReleaseUrl] = useState(''); const [releaseUrl, setReleaseUrl] = useState('');
const { token } = theme.useToken();
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
useEffect(() => { useEffect(() => {
// 检查版本更新(每次都重新检查) // 检查版本更新(每次都重新检查)
@@ -55,11 +57,11 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
right: 0, right: 0,
backdropFilter: 'blur(20px) saturate(180%)', backdropFilter: 'blur(20px) saturate(180%)',
WebkitBackdropFilter: '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', padding: isMobile ? '8px 12px' : '10px 16px',
zIndex: 100, zIndex: 100,
boxShadow: 'var(--shadow-card)', boxShadow: `0 -2px 16px ${alphaColor(token.colorText, 0.08)}`,
backgroundColor: 'rgba(255, 255, 255, 0.8)', // 半透明背景以支持 backdrop-filter backgroundColor: alphaColor(token.colorBgContainer, 0.82), // 半透明背景以支持 backdrop-filter
transition: 'left 0.3s ease', // 平滑过渡 transition: 'left 0.3s ease', // 平滑过渡
}} }}
> >
@@ -87,23 +89,23 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 4, gap: 4,
color: 'var(--color-primary)', color: token.colorPrimary,
cursor: hasUpdate ? 'pointer' : 'default', cursor: hasUpdate ? 'pointer' : 'default',
}} }}
title={hasUpdate ? `发现新版本 v${latestVersion},点击查看` : '当前版本'} 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> <span>{getVersionString()}</span>
</Text> </Text>
</Badge> </Badge>
<Divider type="vertical" style={{ margin: '0 4px', borderColor: 'var(--color-border)' }} /> <Divider type="vertical" style={{ margin: '0 4px', borderColor: token.colorBorder }} />
<Button <Button
type="text" type="text"
size="small" size="small"
icon={<GiftOutlined />} icon={<GiftOutlined />}
onClick={() => window.open('https://mumuverse.space:1588/', '_blank')} onClick={() => window.open('https://mumuverse.space:1588/', '_blank')}
style={{ style={{
color: 'var(--color-text-secondary)', color: token.colorTextSecondary,
fontSize: 11, fontSize: 11,
height: 24, height: 24,
padding: '0 4px', padding: '0 4px',
@@ -114,7 +116,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
> >
</Button> </Button>
<Divider type="vertical" style={{ margin: '0 4px', borderColor: 'var(--color-border)' }} /> <Divider type="vertical" style={{ margin: '0 4px', borderColor: token.colorBorder }} />
<Link <Link
href={VERSION_INFO.githubUrl} href={VERSION_INFO.githubUrl}
target="_blank" target="_blank"
@@ -124,7 +126,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 4, gap: 4,
color: 'var(--color-text-secondary)', color: token.colorTextSecondary,
}} }}
> >
<GithubOutlined style={{ fontSize: 12 }} /> <GithubOutlined style={{ fontSize: 12 }} />
@@ -132,7 +134,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
<Text <Text
style={{ style={{
fontSize: 10, fontSize: 10,
color: 'var(--color-text-tertiary)', color: token.colorTextTertiary,
}} }}
> >
<ClockCircleOutlined style={{ fontSize: 10, marginRight: 4 }} /> <ClockCircleOutlined style={{ fontSize: 10, marginRight: 4 }} />
@@ -144,7 +146,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
<Space <Space
direction="horizontal" direction="horizontal"
size={12} size={12}
split={<Divider type="vertical" style={{ borderColor: 'var(--color-border)' }} />} split={<Divider type="vertical" style={{ borderColor: token.colorBorder }} />}
style={{ style={{
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
@@ -160,7 +162,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 6, gap: 6,
color: 'var(--color-text-secondary)', color: token.colorTextSecondary,
textShadow: 'none', textShadow: 'none',
cursor: hasUpdate ? 'pointer' : 'default', cursor: hasUpdate ? 'pointer' : 'default',
transition: 'all 0.3s', transition: 'all 0.3s',
@@ -177,7 +179,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
}} }}
title={hasUpdate ? `发现新版本 v${latestVersion},点击查看` : '当前版本'} 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> <span>{getVersionString()}</span>
</Text> </Text>
</Badge> </Badge>
@@ -192,7 +194,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 6, gap: 6,
color: 'var(--color-text-secondary)', color: token.colorTextSecondary,
}} }}
> >
<GithubOutlined style={{ fontSize: 13 }} /> <GithubOutlined style={{ fontSize: 13 }} />
@@ -206,7 +208,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ style={{
fontSize: 12, fontSize: 12,
color: 'var(--color-text-secondary)', color: token.colorTextSecondary,
}} }}
> >
LinuxDO LinuxDO
@@ -218,9 +220,9 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
icon={<GiftOutlined style={{ fontSize: 14 }} />} icon={<GiftOutlined style={{ fontSize: 14 }} />}
onClick={() => window.open('https://mumuverse.space:1588/', '_blank')} onClick={() => window.open('https://mumuverse.space:1588/', '_blank')}
style={{ style={{
background: 'var(--color-primary)', background: token.colorPrimary,
border: 'none', border: 'none',
boxShadow: '0 4px 12px rgba(77, 128, 136, 0.3)', boxShadow: `0 4px 12px ${alphaColor(token.colorPrimary, 0.35)}`,
fontSize: 13, fontSize: 13,
height: 32, height: 32,
padding: '0 20px', padding: '0 20px',
@@ -232,11 +234,11 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)'; 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) => { onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)'; 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', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 6, gap: 6,
color: 'var(--color-text-secondary)', color: token.colorTextSecondary,
}} }}
> >
<CopyrightOutlined style={{ fontSize: 11 }} /> <CopyrightOutlined style={{ fontSize: 11 }} />
@@ -266,7 +268,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 4, gap: 4,
color: 'var(--color-text-tertiary)', color: token.colorTextTertiary,
}} }}
> >
<ClockCircleOutlined style={{ fontSize: 12 }} /> <ClockCircleOutlined style={{ fontSize: 12 }} />
@@ -280,12 +282,12 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 4, gap: 4,
color: 'var(--color-text-secondary)', color: token.colorTextSecondary,
textShadow: '0 1px 3px rgba(0, 0, 0, 0.05)', textShadow: `0 1px 3px ${alphaColor(token.colorText, 0.08)}`,
}} }}
> >
<span>Made with</span> <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> <span>by {VERSION_INFO.author}</span>
</Text> </Text>
</Space> </Space>
+3 -2
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; 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 { import {
ThunderboltOutlined, ThunderboltOutlined,
BulbOutlined, BulbOutlined,
@@ -27,6 +27,7 @@ interface ChapterAnalysisProps {
} }
export default function ChapterAnalysis({ chapterId, visible, onClose }: ChapterAnalysisProps) { export default function ChapterAnalysis({ chapterId, visible, onClose }: ChapterAnalysisProps) {
const { token } = theme.useToken();
const [task, setTask] = useState<AnalysisTask | null>(null); const [task, setTask] = useState<AnalysisTask | null>(null);
const [analysis, setAnalysis] = useState<ChapterAnalysisResponse | null>(null); const [analysis, setAnalysis] = useState<ChapterAnalysisResponse | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -258,7 +259,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
borderRadius: 6, borderRadius: 6,
boxShadow: task.progress > 0 && task.status !== 'failed' 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' : 'none'
}} /> }} />
</div> </div>
@@ -1,5 +1,5 @@
import React, { useState } from 'react'; 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 { CheckOutlined, CloseOutlined, SwapOutlined } from '@ant-design/icons';
import ReactDiffViewer from 'react-diff-viewer-continued'; import ReactDiffViewer from 'react-diff-viewer-continued';
@@ -26,6 +26,7 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
onApply, onApply,
onDiscard onDiscard
}) => { }) => {
const { token } = theme.useToken();
const [applying, setApplying] = useState(false); const [applying, setApplying] = useState(false);
const [viewMode, setViewMode] = useState<'split' | 'unified'>('split'); const [viewMode, setViewMode] = useState<'split' | 'unified'>('split');
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
@@ -195,7 +196,7 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
styles={{ styles={{
variables: { variables: {
light: { light: {
diffViewerBackground: '#fff', // Keep white for diff viewer readability diffViewerBackground: token.colorBgContainer,
addedBackground: 'var(--color-success-bg)', addedBackground: 'var(--color-success-bg)',
addedColor: 'var(--color-text-primary)', addedColor: 'var(--color-text-primary)',
removedBackground: 'var(--color-error-bg)', removedBackground: 'var(--color-error-bg)',
+29 -22
View File
@@ -3,7 +3,7 @@
* 提供沉浸式阅读体验,支持主题切换、字体调节、翻页导航等功能 * 提供沉浸式阅读体验,支持主题切换、字体调节、翻页导航等功能
*/ */
import { useState, useEffect, useCallback } from 'react'; 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 { import {
LeftOutlined, LeftOutlined,
RightOutlined, RightOutlined,
@@ -37,27 +37,12 @@ interface NavigationInfo {
current: { id: string; chapter_number: number; title: string }; current: { id: string; chapter_number: number; title: string };
} }
// 主题样式配置 interface ReaderThemeStyle {
const themeStyles = { bg: string;
light: { text: string;
bg: '#ffffff', headerBg: string;
text: '#333333', border: string;
headerBg: '#fafafa', }
border: '#e8e8e8'
},
sepia: {
bg: '#f5e6c8',
text: '#5b4636',
headerBg: '#e8d9b8',
border: '#d4c5a5'
},
dark: {
bg: '#1a1a1a',
text: '#cccccc',
headerBg: '#252525',
border: '#333333'
}
};
// 本地存储key // 本地存储key
const SETTINGS_STORAGE_KEY = 'chapter-reader-settings'; const SETTINGS_STORAGE_KEY = 'chapter-reader-settings';
@@ -94,6 +79,8 @@ export default function ChapterReader({
onClose, onClose,
onChapterChange onChapterChange
}: ChapterReaderProps) { }: ChapterReaderProps) {
const { token } = theme.useToken();
// 阅读器设置 // 阅读器设置
const [settings, setSettings] = useState<ReaderSettings>(loadSettings); const [settings, setSettings] = useState<ReaderSettings>(loadSettings);
@@ -200,6 +187,26 @@ export default function ChapterReader({
}, [chapter?.id]); }, [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]; 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 { EditOutlined, DeleteOutlined, UserOutlined, BankOutlined, ExportOutlined } from '@ant-design/icons';
import { cardStyles } from './CardStyles'; import { characterCardStyles } from './CardStyles';
import type { Character } from '../types'; import type { Character } from '../types';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
@@ -13,6 +13,8 @@ interface CharacterCardProps {
} }
export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit, onDelete, onExport }) => { export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit, onDelete, onExport }) => {
const { token } = theme.useToken();
const getRoleTypeColor = (roleType?: string) => { const getRoleTypeColor = (roleType?: string) => {
const roleColors: Record<string, string> = { const roleColors: Record<string, string> = {
'protagonist': 'blue', 'protagonist': 'blue',
@@ -37,10 +39,10 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
const getStatusTag = () => { const getStatusTag = () => {
const statusConfig: Record<string, { color: string; label: string }> = { const statusConfig: Record<string, { color: string; label: string }> = {
deceased: { color: '#000000', label: '💀 已死亡' }, deceased: { color: token.colorTextBase, label: '💀 已死亡' },
missing: { color: '#faad14', label: '❓ 已失踪' }, missing: { color: token.colorWarning, label: '❓ 已失踪' },
retired: { color: '#8c8c8c', label: '📤 已退场' }, retired: { color: token.colorTextTertiary, label: '📤 已退场' },
destroyed: { color: '#000000', label: '💀 已覆灭' }, destroyed: { color: token.colorTextBase, label: '💀 已覆灭' },
}; };
const config = statusConfig[charStatus]; const config = statusConfig[charStatus];
if (!config) return null; if (!config) return null;
@@ -51,7 +53,7 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
<Card <Card
hoverable hoverable
style={{ style={{
...(isOrganization ? cardStyles.organization : cardStyles.character), ...(isOrganization ? characterCardStyles.organizationCard : characterCardStyles.characterCard),
...(isInactive ? { opacity: 0.6, filter: 'grayscale(40%)' } : {}), ...(isInactive ? { opacity: 0.6, filter: 'grayscale(40%)' } : {}),
}} }}
styles={{ styles={{
@@ -82,14 +84,14 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
<Card.Meta <Card.Meta
avatar={ avatar={
isOrganization ? ( 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={ title={
<Space> <Space>
<span style={cardStyles.ellipsis}>{character.name}</span> <span style={characterCardStyles.nameEllipsis}>{character.name}</span>
{isOrganization ? ( {isOrganization ? (
<Tag color="green"></Tag> <Tag color="green"></Tag>
) : ( ) : (
@@ -103,7 +105,7 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
</Space> </Space>
} }
description={ description={
<div style={cardStyles.description}> <div style={characterCardStyles.descriptionBlock}>
{/* 角色特有字段 */} {/* 角色特有字段 */}
{!isOrganization && ( {!isOrganization && (
<> <>
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { Card, Button, Modal, Form, Select, InputNumber, Input, message, Progress, Tag, Space, Divider, Typography } from 'antd'; import { Card, Button, Modal, Form, Select, InputNumber, Input, message, Progress, Tag, Space, Divider, Typography, theme } from 'antd';
import { EditOutlined, PlusOutlined, DeleteOutlined, TrophyOutlined } from '@ant-design/icons'; import { EditOutlined, PlusOutlined, DeleteOutlined, TrophyOutlined } from '@ant-design/icons';
import axios from 'axios'; import axios from 'axios';
@@ -44,6 +44,7 @@ export const CharacterCareerCard: React.FC<Props> = ({
editable = false, editable = false,
onUpdate onUpdate
}) => { }) => {
const { token } = theme.useToken();
const [mainCareer, setMainCareer] = useState<CareerDetail | null>(null); const [mainCareer, setMainCareer] = useState<CareerDetail | null>(null);
const [subCareers, setSubCareers] = useState<CareerDetail[]>([]); const [subCareers, setSubCareers] = useState<CareerDetail[]>([]);
const [allCareers, setAllCareers] = useState<Career[]>([]); const [allCareers, setAllCareers] = useState<Career[]>([]);
@@ -190,7 +191,7 @@ export const CharacterCareerCard: React.FC<Props> = ({
<div key={career.id} style={{ marginBottom: 16 }}> <div key={career.id} style={{ marginBottom: 16 }}>
<Space style={{ width: '100%', justifyContent: 'space-between' }}> <Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Space> <Space>
<TrophyOutlined style={{ color: isMain ? '#1890ff' : '#8c8c8c' }} /> <TrophyOutlined style={{ color: isMain ? token.colorPrimary : token.colorTextTertiary }} />
<Text strong={isMain}>{career.career_name}</Text> <Text strong={isMain}>{career.career_name}</Text>
{isMain && <Tag color="blue"></Tag>} {isMain && <Tag color="blue"></Tag>}
</Space> </Space>
@@ -1,5 +1,5 @@
import { useState, useMemo } from 'react'; 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 { SearchOutlined } from '@ant-design/icons';
import type { Chapter } from '../types'; import type { Chapter } from '../types';
@@ -24,6 +24,7 @@ export default function FloatingIndexPanel({
groupedChapters, groupedChapters,
onChapterSelect, onChapterSelect,
}: FloatingIndexPanelProps) { }: FloatingIndexPanelProps) {
const { token } = theme.useToken();
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const filteredGroups = useMemo(() => { const filteredGroups = useMemo(() => {
@@ -56,7 +57,7 @@ export default function FloatingIndexPanel({
body: { padding: 0 }, body: { padding: 0 },
}} }}
> >
<div style={{ padding: '16px', borderBottom: '1px solid #f0f0f0' }}> <div style={{ padding: '16px', borderBottom: `1px solid ${token.colorBorderSecondary}` }}>
<Input <Input
placeholder="搜索章节标题" placeholder="搜索章节标题"
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
+22 -18
View File
@@ -1,5 +1,5 @@
import React, { useMemo, useEffect, useRef } from 'react'; 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 { import {
FireOutlined, FireOutlined,
StarOutlined, StarOutlined,
@@ -22,22 +22,18 @@ const TYPE_CONFIG = {
hook: { hook: {
label: '钩子', label: '钩子',
icon: <FireOutlined />, icon: <FireOutlined />,
color: '#ff6b6b',
}, },
foreshadow: { foreshadow: {
label: '伏笔', label: '伏笔',
icon: <StarOutlined />, icon: <StarOutlined />,
color: '#6b7bff',
}, },
plot_point: { plot_point: {
label: '情节点', label: '情节点',
icon: <ThunderboltOutlined />, icon: <ThunderboltOutlined />,
color: '#51cf66',
}, },
character_event: { character_event: {
label: '角色事件', label: '角色事件',
icon: <UserOutlined />, icon: <UserOutlined />,
color: '#ffd93d',
}, },
}; };
@@ -51,7 +47,14 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
onAnnotationClick, onAnnotationClick,
scrollToAnnotation, scrollToAnnotation,
}) => { }) => {
const { token } = theme.useToken();
const cardRefs = useRef<Record<string, HTMLDivElement | null>>({}); 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(() => { useEffect(() => {
@@ -100,6 +103,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
// 渲染单个记忆卡片 // 渲染单个记忆卡片
const renderMemoryCard = (annotation: MemoryAnnotation) => { const renderMemoryCard = (annotation: MemoryAnnotation) => {
const config = TYPE_CONFIG[annotation.type]; const config = TYPE_CONFIG[annotation.type];
const color = typeColors[annotation.type];
const isActive = activeAnnotationId === annotation.id; const isActive = activeAnnotationId === annotation.id;
return ( return (
@@ -115,8 +119,8 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
onClick={() => onAnnotationClick?.(annotation)} onClick={() => onAnnotationClick?.(annotation)}
style={{ style={{
marginBottom: 12, marginBottom: 12,
borderLeft: `4px solid ${config.color}`, borderLeft: `4px solid ${color}`,
backgroundColor: isActive ? `${config.color}11` : 'transparent', backgroundColor: isActive ? `color-mix(in srgb, ${color} 8%, transparent)` : 'transparent',
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.2s', transition: 'all 0.2s',
}} }}
@@ -126,7 +130,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
<Badge <Badge
count={`${(annotation.importance * 10).toFixed(1)}`} count={`${(annotation.importance * 10).toFixed(1)}`}
style={{ style={{
backgroundColor: config.color, backgroundColor: color,
float: 'right', float: 'right',
}} }}
/> />
@@ -138,7 +142,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
<div <div
style={{ style={{
fontSize: 13, fontSize: 13,
color: '#666', color: token.colorTextSecondary,
lineHeight: 1.6, lineHeight: 1.6,
marginBottom: 8, marginBottom: 8,
}} }}
@@ -160,7 +164,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
{/* 特殊元数据 */} {/* 特殊元数据 */}
{annotation.metadata.strength && ( {annotation.metadata.strength && (
<div style={{ marginTop: 4, fontSize: 11, color: '#999' }}> <div style={{ marginTop: 4, fontSize: 11, color: token.colorTextTertiary }}>
: {annotation.metadata.strength}/10 : {annotation.metadata.strength}/10
</div> </div>
)} )}
@@ -192,27 +196,27 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
<div style={{ fontWeight: 600, marginBottom: 12 }}>📊 </div> <div style={{ fontWeight: 600, marginBottom: 12 }}>📊 </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
<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.hook.color }}> <div style={{ fontSize: 20, fontWeight: 600, color: typeColors.hook }}>
{stats.hooks} {stats.hooks}
</div> </div>
</div> </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.foreshadow.color }}> <div style={{ fontSize: 20, fontWeight: 600, color: typeColors.foreshadow }}>
{stats.foreshadows} {stats.foreshadows}
</div> </div>
</div> </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.plot_point.color }}> <div style={{ fontSize: 20, fontWeight: 600, color: typeColors.plot_point }}>
{stats.plotPoints} {stats.plotPoints}
</div> </div>
</div> </div>
<div> <div>
<div style={{ fontSize: 12, color: '#999' }}></div> <div style={{ fontSize: 12, color: token.colorTextTertiary }}></div>
<div <div
style={{ fontSize: 20, fontWeight: 600, color: TYPE_CONFIG.character_event.color }} style={{ fontSize: 20, fontWeight: 600, color: typeColors.character_event }}
> >
{stats.characterEvents} {stats.characterEvents}
</div> </div>
@@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react'; 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 { ThunderboltOutlined, CheckOutlined, ReloadOutlined, EditOutlined, LoadingOutlined } from '@ant-design/icons';
import { chapterApi } from '../services/api'; import { chapterApi } from '../services/api';
@@ -33,6 +33,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
onClose, onClose,
onApply, onApply,
}) => { }) => {
const { token } = theme.useToken();
const [userInstructions, setUserInstructions] = useState(''); const [userInstructions, setUserInstructions] = useState('');
const [lengthMode, setLengthMode] = useState<LengthMode>('similar'); const [lengthMode, setLengthMode] = useState<LengthMode>('similar');
const [customWordCount, setCustomWordCount] = useState<number>(selectedText.length); const [customWordCount, setCustomWordCount] = useState<number>(selectedText.length);
@@ -178,7 +179,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
<Modal <Modal
title={ title={
<Space> <Space>
<EditOutlined style={{ color: 'var(--color-primary)' }} /> <EditOutlined style={{ color: token.colorPrimary }} />
<span>AI局部重写</span> <span>AI局部重写</span>
</Space> </Space>
} }
@@ -202,9 +203,9 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
loading={isGenerating} loading={isGenerating}
disabled={!userInstructions.trim()} disabled={!userInstructions.trim()}
style={{ 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', border: 'none',
boxShadow: '0 4px 12px rgba(77, 128, 136, 0.3)', boxShadow: token.boxShadowSecondary,
}} }}
> >
{isGenerating ? '生成中...' : '开始重写'} {isGenerating ? '生成中...' : '开始重写'}
@@ -221,7 +222,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
type="primary" type="primary"
icon={<CheckOutlined />} icon={<CheckOutlined />}
onClick={handleAccept} onClick={handleAccept}
style={{ background: '#52c41a', borderColor: '#52c41a' }} style={{ background: token.colorSuccess, borderColor: token.colorSuccess }}
> >
</Button> </Button>
@@ -250,7 +251,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
body: { body: {
maxHeight: 150, maxHeight: 150,
overflowY: 'auto', overflowY: 'auto',
background: '#fafafa', background: token.colorFillAlter,
}, },
}} }}
> >
@@ -258,7 +259,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
style={{ style={{
margin: 0, margin: 0,
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
color: '#595959', color: token.colorText,
lineHeight: 1.8, lineHeight: 1.8,
}} }}
> >
@@ -352,7 +353,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
<div <div
style={{ style={{
height: 4, height: 4,
background: '#f0f0f0', background: token.colorFillTertiary,
borderRadius: 2, borderRadius: 2,
overflow: 'hidden', overflow: 'hidden',
}} }}
@@ -360,7 +361,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
<div <div
style={{ style={{
height: '100%', 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}%`, width: `${progress}%`,
transition: 'width 0.3s ease', transition: 'width 0.3s ease',
borderRadius: 2, borderRadius: 2,
@@ -374,8 +375,8 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
size="small" size="small"
ref={generatedTextRef} ref={generatedTextRef}
style={{ style={{
background: generatedText ? '#f6ffed' : '#fafafa', background: generatedText ? token.colorSuccessBg : token.colorFillAlter,
border: generatedText ? '1px solid #b7eb8f' : '1px solid #d9d9d9', border: generatedText ? `1px solid ${token.colorSuccessBorder}` : `1px solid ${token.colorBorder}`,
}} }}
styles={{ styles={{
body: { body: {
@@ -400,7 +401,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
display: 'inline-block', display: 'inline-block',
width: 8, width: 8,
height: 16, height: 16,
background: 'var(--color-primary)', background: token.colorPrimary,
marginLeft: 2, marginLeft: 2,
animation: 'blink 1s infinite', animation: 'blink 1s infinite',
}} }}
@@ -408,7 +409,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
)} )}
</Paragraph> </Paragraph>
) : ( ) : (
<div style={{ textAlign: 'center', padding: 20, color: '#8c8c8c' }}> <div style={{ textAlign: 'center', padding: 20, color: token.colorTextTertiary }}>
{isGenerating ? '正在生成内容...' : '等待生成...'} {isGenerating ? '正在生成内容...' : '等待生成...'}
</div> </div>
)} )}
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Button, Tooltip } from 'antd'; import { Button, Tooltip, theme } from 'antd';
import { EditOutlined } from '@ant-design/icons'; import { EditOutlined } from '@ant-design/icons';
interface PartialRegenerateToolbarProps { interface PartialRegenerateToolbarProps {
@@ -19,6 +19,8 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
onRegenerate, onRegenerate,
selectedText selectedText
}) => { }) => {
const { token } = theme.useToken();
if (!visible || !selectedText) return null; if (!visible || !selectedText) return null;
// 限制显示的选中文本长度 // 限制显示的选中文本长度
@@ -33,15 +35,15 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
top: position.top, top: position.top,
left: position.left, left: position.left,
zIndex: 10000, zIndex: 10000,
background: '#fff', background: token.colorBgElevated,
borderRadius: 8, borderRadius: 8,
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.15)', boxShadow: token.boxShadow,
padding: '6px 8px', padding: '6px 8px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 8, gap: 8,
animation: 'fadeIn 0.2s ease-out', animation: 'fadeIn 0.2s ease-out',
border: '1px solid #e8e8e8', border: `1px solid ${token.colorBorderSecondary}`,
}} }}
> >
<Tooltip <Tooltip
@@ -61,7 +63,7 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)', background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
border: 'none', border: 'none',
fontWeight: 500, fontWeight: 500,
boxShadow: '0 4px 12px rgba(77, 128, 136, 0.3)', boxShadow: token.boxShadowSecondary,
}} }}
> >
AI重写 AI重写
@@ -69,7 +71,7 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
</Tooltip> </Tooltip>
<span style={{ <span style={{
fontSize: 12, fontSize: 12,
color: '#8c8c8c', color: token.colorTextTertiary,
maxWidth: 150, maxWidth: 150,
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
+15 -13
View File
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Spin } from 'antd'; import { Spin, theme } from 'antd';
import { LoadingOutlined } from '@ant-design/icons'; import { LoadingOutlined } from '@ant-design/icons';
interface SSELoadingOverlayProps { interface SSELoadingOverlayProps {
@@ -13,6 +13,8 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
progress, progress,
message message
}) => { }) => {
const { token } = theme.useToken();
if (!loading) return null; if (!loading) return null;
return ( return (
@@ -22,19 +24,19 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
background: 'rgba(0, 0, 0, 0.45)', background: token.colorBgMask,
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
zIndex: 9999 zIndex: 9999
}}> }}>
<div style={{ <div style={{
background: '#fff', background: token.colorBgElevated,
borderRadius: 12, borderRadius: 12,
padding: '40px 60px', padding: '40px 60px',
minWidth: 400, minWidth: 400,
maxWidth: 600, maxWidth: 600,
boxShadow: '0 8px 32px rgba(0,0,0,0.2)' boxShadow: token.boxShadowSecondary
}}> }}>
{/* 标题和图标 */} {/* 标题和图标 */}
<div style={{ <div style={{
@@ -42,13 +44,13 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
marginBottom: 24 marginBottom: 24
}}> }}>
<Spin <Spin
indicator={<LoadingOutlined style={{ fontSize: 48, color: 'var(--color-primary)' }} spin />} indicator={<LoadingOutlined style={{ fontSize: 48, color: token.colorPrimary }} spin />}
/> />
<div style={{ <div style={{
fontSize: 20, fontSize: 20,
fontWeight: 'bold', fontWeight: 'bold',
marginTop: 16, marginTop: 16,
color: 'var(--color-text-primary)' color: token.colorTextHeading
}}> }}>
AI生成中... AI生成中...
</div> </div>
@@ -60,7 +62,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
}}> }}>
<div style={{ <div style={{
height: 12, height: 12,
background: 'var(--color-bg-layout)', background: token.colorFillTertiary,
borderRadius: 6, borderRadius: 6,
overflow: 'hidden', overflow: 'hidden',
marginBottom: 12 marginBottom: 12
@@ -68,12 +70,12 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
<div style={{ <div style={{
height: '100%', height: '100%',
background: progress === 100 background: progress === 100
? 'linear-gradient(90deg, var(--color-success) 0%, var(--color-success-active) 100%)' ? `linear-gradient(90deg, ${token.colorSuccess} 0%, ${token.colorSuccessActive} 100%)`
: 'linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-active) 100%)', : `linear-gradient(90deg, ${token.colorPrimary} 0%, ${token.colorPrimaryActive} 100%)`,
width: `${progress}%`, width: `${progress}%`,
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
borderRadius: 6, borderRadius: 6,
boxShadow: progress > 0 ? 'var(--shadow-card)' : 'none' boxShadow: progress > 0 ? token.boxShadow : 'none'
}} /> }} />
</div> </div>
@@ -82,7 +84,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
textAlign: 'center', textAlign: 'center',
fontSize: 32, fontSize: 32,
fontWeight: 'bold', fontWeight: 'bold',
color: progress === 100 ? 'var(--color-success)' : 'var(--color-primary)', color: progress === 100 ? token.colorSuccess : token.colorPrimary,
marginBottom: 8 marginBottom: 8
}}> }}>
{progress}% {progress}%
@@ -93,7 +95,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
<div style={{ <div style={{
textAlign: 'center', textAlign: 'center',
fontSize: 16, fontSize: 16,
color: '#595959', color: token.colorText,
minHeight: 24, minHeight: 24,
padding: '0 20px' padding: '0 20px'
}}> }}>
@@ -104,7 +106,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
<div style={{ <div style={{
textAlign: 'center', textAlign: 'center',
fontSize: 13, fontSize: 13,
color: '#8c8c8c', color: token.colorTextTertiary,
marginTop: 16 marginTop: 16
}}> }}>
, ,
+7 -4
View File
@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { theme } from 'antd';
interface SSEProgressBarProps { interface SSEProgressBarProps {
loading: boolean; loading: boolean;
@@ -11,6 +12,8 @@ export const SSEProgressBar: React.FC<SSEProgressBarProps> = ({
progress, progress,
message message
}) => { }) => {
const { token } = theme.useToken();
if (!loading) return null; if (!loading) return null;
return ( return (
@@ -18,14 +21,14 @@ export const SSEProgressBar: React.FC<SSEProgressBarProps> = ({
{/* 进度条 */} {/* 进度条 */}
<div style={{ <div style={{
height: 8, height: 8,
background: '#f0f0f0', background: token.colorFillTertiary,
borderRadius: 4, borderRadius: 4,
overflow: 'hidden', overflow: 'hidden',
marginBottom: 8 marginBottom: 8
}}> }}>
<div style={{ <div style={{
height: '100%', height: '100%',
background: progress === 100 ? '#52c41a' : '#1890ff', background: progress === 100 ? token.colorSuccess : token.colorPrimary,
width: `${progress}%`, width: `${progress}%`,
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
borderRadius: 4 borderRadius: 4
@@ -39,12 +42,12 @@ export const SSEProgressBar: React.FC<SSEProgressBarProps> = ({
alignItems: 'center', alignItems: 'center',
fontSize: 14 fontSize: 14
}}> }}>
<span style={{ color: '#666' }}> <span style={{ color: token.colorTextSecondary }}>
{message || '准备生成...'} {message || '准备生成...'}
</span> </span>
<span style={{ <span style={{
fontWeight: 'bold', fontWeight: 'bold',
color: progress === 100 ? '#52c41a' : '#1890ff' color: progress === 100 ? token.colorSuccess : token.colorPrimary
}}> }}>
{progress}% {progress}%
</span> </span>
+12 -10
View File
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Modal, Spin, Button } from 'antd'; import { Modal, Spin, Button, theme } from 'antd';
import { LoadingOutlined, StopOutlined } from '@ant-design/icons'; import { LoadingOutlined, StopOutlined } from '@ant-design/icons';
interface SSEProgressModalProps { interface SSEProgressModalProps {
@@ -27,6 +27,8 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
onCancel, onCancel,
cancelButtonText = '取消任务', cancelButtonText = '取消任务',
}) => { }) => {
const { token } = theme.useToken();
if (!visible) return null; if (!visible) return null;
return ( return (
@@ -53,13 +55,13 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
marginBottom: 24 marginBottom: 24
}}> }}>
<Spin <Spin
indicator={<LoadingOutlined style={{ fontSize: 48, color: 'var(--color-primary)' }} spin />} indicator={<LoadingOutlined style={{ fontSize: 48, color: token.colorPrimary }} spin />}
/> />
<div style={{ <div style={{
fontSize: 20, fontSize: 20,
fontWeight: 'bold', fontWeight: 'bold',
marginTop: 16, marginTop: 16,
color: 'var(--color-text-primary)' color: token.colorText
}}> }}>
{title} {title}
</div> </div>
@@ -72,7 +74,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
}}> }}>
<div style={{ <div style={{
height: 12, height: 12,
background: 'var(--color-bg-layout)', background: token.colorBgLayout,
borderRadius: 6, borderRadius: 6,
overflow: 'hidden', overflow: 'hidden',
marginBottom: showPercentage ? 12 : 0 marginBottom: showPercentage ? 12 : 0
@@ -80,12 +82,12 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
<div style={{ <div style={{
height: '100%', height: '100%',
background: progress === 100 background: progress === 100
? 'linear-gradient(90deg, var(--color-success) 0%, var(--color-success-active) 100%)' ? `linear-gradient(90deg, ${token.colorSuccess} 0%, ${token.colorSuccess} 100%)`
: 'linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-active) 100%)', : `linear-gradient(90deg, ${token.colorPrimary} 0%, ${token.colorPrimary} 100%)`,
width: `${progress}%`, width: `${progress}%`,
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
borderRadius: 6, borderRadius: 6,
boxShadow: progress > 0 ? 'var(--shadow-card)' : 'none' boxShadow: progress > 0 ? token.boxShadow : 'none'
}} /> }} />
</div> </div>
@@ -95,7 +97,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
textAlign: 'center', textAlign: 'center',
fontSize: 32, fontSize: 32,
fontWeight: 'bold', fontWeight: 'bold',
color: progress === 100 ? 'var(--color-success)' : 'var(--color-primary)', color: progress === 100 ? token.colorSuccess : token.colorPrimary,
marginBottom: 8 marginBottom: 8
}}> }}>
{progress}% {progress}%
@@ -107,7 +109,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
<div style={{ <div style={{
textAlign: 'center', textAlign: 'center',
fontSize: 16, fontSize: 16,
color: 'var(--color-text-secondary)', color: token.colorTextSecondary,
minHeight: 24, minHeight: 24,
padding: '0 20px', padding: '0 20px',
marginBottom: 16 marginBottom: 16
@@ -119,7 +121,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
<div style={{ <div style={{
textAlign: 'center', textAlign: 'center',
fontSize: 13, fontSize: 13,
color: 'var(--color-text-tertiary)', color: token.colorTextTertiary,
marginBottom: onCancel ? 16 : 0 marginBottom: onCancel ? 16 : 0
}}> }}>
+27 -23
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; 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 { UserOutlined, LogoutOutlined, TeamOutlined, CrownOutlined, LockOutlined } from '@ant-design/icons';
import { authApi } from '../services/api'; import { authApi } from '../services/api';
import type { User } from '../types'; import type { User } from '../types';
@@ -11,14 +11,18 @@ const { Text } = Typography;
interface UserMenuProps { interface UserMenuProps {
/** 是否总是显示完整信息(用于移动端侧边栏) */ /** 是否总是显示完整信息(用于移动端侧边栏) */
showFullInfo?: boolean; showFullInfo?: boolean;
/** 紧凑模式(用于折叠侧边栏,仅展示头像) */
compact?: boolean;
} }
export default function UserMenu({ showFullInfo = false }: UserMenuProps) { export default function UserMenu({ showFullInfo = false, compact = false }: UserMenuProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [currentUser, setCurrentUser] = useState<User | null>(null); const [currentUser, setCurrentUser] = useState<User | null>(null);
const [showChangePassword, setShowChangePassword] = useState(false); const [showChangePassword, setShowChangePassword] = useState(false);
const [changePasswordForm] = Form.useForm(); const [changePasswordForm] = Form.useForm();
const [changingPassword, setChangingPassword] = useState(false); 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(() => { useEffect(() => {
loadCurrentUser(); loadCurrentUser();
@@ -126,36 +130,36 @@ export default function UserMenu({ showFullInfo = false }: UserMenuProps) {
cursor: 'pointer', cursor: 'pointer',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 12, gap: compact ? 0 : 12,
padding: '8px 16px', padding: compact ? '4px' : '8px 16px',
background: 'rgba(255, 255, 255, 0.6)', // 保持半透明以配合 Backdrop background: alphaColor(token.colorBgContainer, 0.65), // 保持半透明以配合 Backdrop
backdropFilter: 'blur(10px)', backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)', WebkitBackdropFilter: 'blur(10px)',
borderRadius: 24, borderRadius: compact ? 16 : 24,
border: '1px solid var(--color-border)', border: `1px solid ${token.colorBorder}`,
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
boxShadow: 'var(--shadow-card)', boxShadow: `0 8px 20px ${alphaColor(token.colorText, 0.08)}`,
}} }}
onMouseEnter={(e) => { 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.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = 'var(--shadow-elevated)'; e.currentTarget.style.boxShadow = `0 12px 28px ${alphaColor(token.colorText, 0.14)}`;
}} }}
onMouseLeave={(e) => { 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.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' }}> <div style={{ position: 'relative' }}>
<Avatar <Avatar
src={currentUser.avatar_url} src={currentUser.avatar_url}
icon={<UserOutlined />} icon={<UserOutlined />}
size={40} size={compact ? 32 : 40}
style={{ style={{
backgroundColor: 'var(--color-primary)', backgroundColor: token.colorPrimary,
border: '3px solid #fff', border: `3px solid ${token.colorWhite}`,
boxShadow: 'var(--shadow-card)', boxShadow: `0 8px 20px ${alphaColor(token.colorText, 0.12)}`,
}} }}
/> />
{currentUser.is_admin && ( {currentUser.is_admin && (
@@ -165,28 +169,28 @@ export default function UserMenu({ showFullInfo = false }: UserMenuProps) {
right: -2, right: -2,
width: 18, width: 18,
height: 18, height: 18,
background: 'linear-gradient(135deg, #ffd700 0%, #ffaa00 100%)', background: `linear-gradient(135deg, ${token.colorWarning} 0%, ${token.colorWarningHover} 100%)`,
borderRadius: '50%', borderRadius: '50%',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
border: '2px solid white', border: `2px solid ${token.colorWhite}`,
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)', boxShadow: `0 2px 4px ${alphaColor(token.colorText, 0.2)}`,
}}> }}>
<CrownOutlined style={{ fontSize: 9, color: '#fff' }} /> <CrownOutlined style={{ fontSize: 9, color: token.colorWhite }} />
</div> </div>
)} )}
</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={{ <Text strong style={{
color: 'var(--color-text-primary)', color: token.colorText,
fontSize: 14, fontSize: 14,
lineHeight: '20px', lineHeight: '20px',
}}> }}>
{currentUser.display_name || currentUser.username} {currentUser.display_name || currentUser.username}
</Text> </Text>
<Text style={{ <Text style={{
color: 'var(--color-text-secondary)', color: token.colorTextSecondary,
fontSize: 12, fontSize: 12,
lineHeight: '18px', lineHeight: '18px',
}}> }}>
+141 -330
View File
@@ -1,70 +1,17 @@
html,
body,
#root {
min-height: 100%;
}
:root { :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; font-family: "PingFang SC", "Microsoft YaHei", "Heiti SC", Inter, system-ui, sans-serif;
line-height: 1.5715; line-height: 1.5715;
font-weight: 400; font-weight: 400;
color-scheme: light;
color: var(--color-text-base);
background-color: var(--color-bg-base);
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
/* 移动端视口适配 */
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%; -ms-text-size-adjust: 100%;
} }
@@ -72,43 +19,123 @@
body { body {
margin: 0; margin: 0;
min-height: 100vh; min-height: 100vh;
/* 禁止移动端双击缩放 */
touch-action: manipulation; touch-action: manipulation;
} }
#root {
min-height: 100vh;
}
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
/* 自定义滚动条样式 */ /* 自定义滚动条样式(与主题弱耦合) */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
height: 8px; height: 8px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: #f1f1f1; background: rgba(0, 0, 0, 0.06);
border-radius: 4px; border-radius: 4px;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #c1c1c1; background: rgba(0, 0, 0, 0.24);
border-radius: 4px; border-radius: 4px;
transition: background 0.3s ease; transition: background 0.2s ease;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #a8a8a8; background: rgba(0, 0, 0, 0.34);
} }
/* Firefox 滚动条样式 */
* { * {
scrollbar-width: thin; 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; font-size: 14px;
} }
/* 移动端隐藏滚动条 */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 4px; width: 4px;
height: 4px; height: 4px;
} }
/* 移动端优化触摸区域 */
button, button,
a, a,
[role="button"] { [role='button'] {
min-height: 44px; min-height: 44px;
min-width: 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, img,
button { 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 { .ant-tabs-dropdown {
z-index: 2000 !important; z-index: 2000 !important;
} }
/* ===== 现代化侧边栏样式 (Modern Sidebar Styles) ===== */ .ant-tooltip {
max-width: min(420px, calc(100vw - 24px));
/* 侧边栏容器 - 毛玻璃效果 */
.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 .ant-tooltip-content,
.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-inner { .ant-tooltip .ant-tooltip-inner {
background: linear-gradient(135deg, max-width: inherit;
var(--color-primary) 0%, }
var(--color-primary-hover) 100%);
.ant-tooltip .ant-tooltip-inner {
background: var(--app-tooltip-bg, #884d5c);
border-radius: 8px; border-radius: 8px;
padding: 8px 16px; padding: 8px 16px;
font-weight: 500; 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 { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; 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 { authApi } from '../services/api';
import AnnouncementModal from '../components/AnnouncementModal'; import AnnouncementModal from '../components/AnnouncementModal';
@@ -10,6 +10,8 @@ export default function AuthCallback() {
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const [showAnnouncement, setShowAnnouncement] = useState(false); const [showAnnouncement, setShowAnnouncement] = useState(false);
const [showPasswordModal, setShowPasswordModal] = useState(false); const [showPasswordModal, setShowPasswordModal] = useState(false);
const { token } = theme.useToken();
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
interface PasswordStatus { interface PasswordStatus {
has_password: boolean; has_password: boolean;
has_custom_password: boolean; has_custom_password: boolean;
@@ -92,11 +94,11 @@ export default function AuthCallback() {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
minHeight: '100vh', minHeight: '100vh',
background: 'linear-gradient(135deg, #4D8088 0%, #5F9EA8 100%)', background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
}}> }}>
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<Spin size="large" /> <Spin size="large" />
<div style={{ marginTop: 20, color: 'white', fontSize: 16 }}> <div style={{ marginTop: 20, color: token.colorWhite, fontSize: 16 }}>
... ...
</div> </div>
</div> </div>
@@ -111,7 +113,7 @@ export default function AuthCallback() {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
minHeight: '100vh', minHeight: '100vh',
background: 'linear-gradient(135deg, #4D8088 0%, #5F9EA8 100%)', background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
}}> }}>
<Result <Result
status="error" status="error"
@@ -122,7 +124,7 @@ export default function AuthCallback() {
</Button> </Button>
} }
style={{ background: 'white', padding: 40, borderRadius: 8 }} style={{ background: token.colorBgContainer, padding: 40, borderRadius: 8 }}
/> />
</div> </div>
); );
@@ -257,17 +259,17 @@ export default function AuthCallback() {
<p>使</p> <p>使</p>
{passwordStatus?.default_password && ( {passwordStatus?.default_password && (
<div style={{ <div style={{
background: '#f0f2f5', background: token.colorFillTertiary,
padding: 12, padding: 12,
borderRadius: 4, borderRadius: 4,
marginTop: 12 marginTop: 12
}}> }}>
<strong></strong>{passwordStatus.username}<br /> <strong></strong>{passwordStatus.username}<br />
<strong></strong><code style={{ <strong></strong><code style={{
background: '#fff', background: token.colorBgContainer,
padding: '2px 8px', padding: '2px 8px',
borderRadius: 3, borderRadius: 3,
color: '#1890ff', color: token.colorPrimary,
fontSize: 14 fontSize: 14
}}>{passwordStatus.default_password}</code> }}>{passwordStatus.default_password}</code>
</div> </div>
@@ -301,13 +303,13 @@ export default function AuthCallback() {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
minHeight: '100vh', minHeight: '100vh',
background: 'linear-gradient(135deg, #4D8088 0%, #5F9EA8 100%)', background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
}}> }}>
<Result <Result
status="success" status="success"
title="登录成功" title="登录成功"
subTitle={showPasswordModal ? "请设置账号密码..." : (showAnnouncement ? "欢迎使用..." : "正在跳转...")} subTitle={showPasswordModal ? "请设置账号密码..." : (showAnnouncement ? "欢迎使用..." : "正在跳转...")}
style={{ background: 'white', padding: 40, borderRadius: 8 }} style={{ background: alphaColor(token.colorBgContainer, 0.96), padding: 40, borderRadius: 8 }}
/> />
</div> </div>
</> </>
+160 -13
View File
@@ -11,6 +11,7 @@ import {
InputNumber, InputNumber,
List, List,
message, message,
Popconfirm,
Progress, Progress,
Row, Row,
Select, Select,
@@ -20,6 +21,7 @@ import {
Tag, Tag,
Typography, Typography,
Upload, Upload,
theme,
} from 'antd'; } from 'antd';
import type { UploadFile } from 'antd/es/upload/interface'; import type { UploadFile } from 'antd/es/upload/interface';
import { InboxOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined, WarningOutlined, RedoOutlined } from '@ant-design/icons'; import { InboxOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined, WarningOutlined, RedoOutlined } from '@ant-design/icons';
@@ -31,7 +33,7 @@ import type {
BookImportTask, BookImportTask,
} from '../types'; } from '../types';
const { Text } = Typography; const { Text, Title } = Typography;
const { Dragger } = Upload; const { Dragger } = Upload;
const { TextArea } = Input; const { TextArea } = Input;
@@ -106,6 +108,8 @@ function isNotFoundError(error: unknown): boolean {
export default function BookImport() { export default function BookImport() {
const navigate = useNavigate(); const navigate = useNavigate();
const { token } = theme.useToken();
const isMobile = window.innerWidth <= 768;
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [taskId, setTaskId] = useState<string | null>(null); const [taskId, setTaskId] = useState<string | null>(null);
@@ -140,6 +144,40 @@ export default function BookImport() {
return 1; return 1;
}, [taskId, taskStatus, preview, applying, isApplyComplete]); }, [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(() => { useEffect(() => {
const cache = loadBookImportCache(); const cache = loadBookImportCache();
if (cache) { if (cache) {
@@ -487,6 +525,31 @@ export default function BookImport() {
} }
}, [navigate]); }, [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]>) => { const updateChapter = (index: number, patch: Partial<BookImportPreview['chapters'][number]>) => {
setPreview(prev => { setPreview(prev => {
if (!prev) return prev; if (!prev) return prev;
@@ -497,18 +560,100 @@ export default function BookImport() {
}; };
return ( return (
<div style={{ height: '100%', overflow: 'auto', paddingRight: 8 }}> <div
<Card style={{ marginBottom: 16 }}> style={{
<Steps minHeight: '90vh',
current={currentStep} overflow: 'auto',
items={[ background: `linear-gradient(180deg, ${token.colorBgLayout} 0%, ${token.colorFillSecondary} 100%)`,
{ title: '上传文件' }, padding: isMobile ? '20px 16px 70px' : '24px 24px 70px',
{ title: '解析中' }, }}
{ title: '预览修改' }, >
{ title: '生成导入' }, <div style={{ maxWidth: 1400, margin: '0 auto', width: '100%' }}>
]} <Card
/> variant="borderless"
</Card> 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 && ( {currentStep === 0 && (
<Card title="上传 TXT 并开始解析" style={{ marginBottom: 16 }}> <Card title="上传 TXT 并开始解析" style={{ marginBottom: 16 }}>
@@ -913,6 +1058,8 @@ export default function BookImport() {
</div> </div>
</Card> </Card>
)} )}
</div>
</div> </div>
); );
} }
+9 -8
View File
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; 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 { import {
EyeOutlined, EyeOutlined,
EyeInvisibleOutlined, EyeInvisibleOutlined,
@@ -77,6 +77,7 @@ const ChapterAnalysis: React.FC = () => {
const [scrollToContentAnnotation, setScrollToContentAnnotation] = useState<string | undefined>(); const [scrollToContentAnnotation, setScrollToContentAnnotation] = useState<string | undefined>();
const [scrollToSidebarAnnotation, setScrollToSidebarAnnotation] = useState<string | undefined>(); const [scrollToSidebarAnnotation, setScrollToSidebarAnnotation] = useState<string | undefined>();
const [isMobile, setIsMobile] = useState(window.innerWidth < 768); const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const { token } = theme.useToken();
// 监听窗口大小变化 // 监听窗口大小变化
useEffect(() => { useEffect(() => {
@@ -196,7 +197,7 @@ const ChapterAnalysis: React.FC = () => {
<div style={{ <div style={{
padding: '16px 0', padding: '16px 0',
marginBottom: 16, marginBottom: 16,
borderBottom: '1px solid #f0f0f0' borderBottom: `1px solid ${token.colorBorderSecondary}`
}}> }}>
<h2 style={{ margin: 0, fontSize: 24 }}> <h2 style={{ margin: 0, fontSize: 24 }}>
<FundOutlined style={{ marginRight: 8 }} /> <FundOutlined style={{ marginRight: 8 }} />
@@ -231,8 +232,8 @@ const ChapterAnalysis: React.FC = () => {
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
padding: '12px 16px', padding: '12px 16px',
background: selectedChapter?.id === chapter.id ? '#e6f7ff' : 'transparent', background: selectedChapter?.id === chapter.id ? token.colorPrimaryBg : 'transparent',
borderLeft: selectedChapter?.id === chapter.id ? '3px solid #1890ff' : '3px solid transparent', borderLeft: selectedChapter?.id === chapter.id ? `3px solid ${token.colorPrimary}` : '3px solid transparent',
}} }}
> >
<List.Item.Meta <List.Item.Meta
@@ -278,8 +279,8 @@ const ChapterAnalysis: React.FC = () => {
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
padding: '12px 16px', padding: '12px 16px',
background: selectedChapter?.id === chapter.id ? '#e6f7ff' : 'transparent', background: selectedChapter?.id === chapter.id ? token.colorPrimaryBg : 'transparent',
borderLeft: selectedChapter?.id === chapter.id ? '3px solid #1890ff' : '3px solid transparent', borderLeft: selectedChapter?.id === chapter.id ? `3px solid ${token.colorPrimary}` : '3px solid transparent',
}} }}
> >
<List.Item.Meta <List.Item.Meta
@@ -430,7 +431,7 @@ const ChapterAnalysis: React.FC = () => {
checkedChildren={<EyeOutlined />} checkedChildren={<EyeOutlined />}
unCheckedChildren={<EyeInvisibleOutlined />} unCheckedChildren={<EyeInvisibleOutlined />}
/> />
<span style={{ fontSize: 13, color: '#666' }}></span> <span style={{ fontSize: 13, color: token.colorTextSecondary }}></span>
</> </>
)} )}
</Space> </Space>
@@ -441,7 +442,7 @@ const ChapterAnalysis: React.FC = () => {
<div style={{ <div style={{
marginTop: 12, marginTop: 12,
fontSize: isMobile ? 11 : 12, fontSize: isMobile ? 11 : 12,
color: '#999', color: token.colorTextTertiary,
lineHeight: 1.5 lineHeight: 1.5
}}> }}>
{annotationsData.summary.total_annotations} {annotationsData.summary.total_annotations}
+9 -7
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { Card, Spin, Alert, Button, Space, Switch, Drawer, message, Progress } from 'antd'; import { Card, Spin, Alert, Button, Space, Switch, Drawer, message, Progress, theme } from 'antd';
import { import {
ArrowLeftOutlined, ArrowLeftOutlined,
EyeOutlined, EyeOutlined,
@@ -64,6 +64,8 @@ const ChapterReader: React.FC = () => {
const { chapterId } = useParams<{ chapterId: string }>(); const { chapterId } = useParams<{ chapterId: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const { token } = theme.useToken();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [chapter, setChapter] = useState<ChapterData | null>(null); const [chapter, setChapter] = useState<ChapterData | null>(null);
@@ -303,7 +305,7 @@ const ChapterReader: React.FC = () => {
checkedChildren={<EyeOutlined />} checkedChildren={<EyeOutlined />}
unCheckedChildren={<EyeInvisibleOutlined />} unCheckedChildren={<EyeInvisibleOutlined />}
/> />
<span style={{ fontSize: 13, color: '#666' }}></span> <span style={{ fontSize: 13, color: token.colorTextSecondary }}></span>
<Button <Button
icon={<MenuOutlined />} icon={<MenuOutlined />}
onClick={() => setSidebarVisible(true)} onClick={() => setSidebarVisible(true)}
@@ -319,14 +321,14 @@ const ChapterReader: React.FC = () => {
{analyzing && ( {analyzing && (
<div style={{ marginTop: 12 }}> <div style={{ marginTop: 12 }}>
<Progress percent={analysisProgress} size="small" status="active" /> <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> </span>
</div> </div>
)} )}
{!analyzing && hasAnnotations && annotationsData && ( {!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.total_annotations}
{annotationsData.summary.hooks > 0 && ` 🎣${annotationsData.summary.hooks}个钩子`} {annotationsData.summary.hooks > 0 && ` 🎣${annotationsData.summary.hooks}个钩子`}
{annotationsData.summary.foreshadows > 0 && {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' }}> <Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Button <Button
size="large" size="large"
@@ -418,9 +420,9 @@ const ChapterReader: React.FC = () => {
<div <div
style={{ style={{
width: 400, width: 400,
borderLeft: '1px solid #f0f0f0', borderLeft: `1px solid ${token.colorBorderSecondary}`,
overflowY: 'auto', overflowY: 'auto',
background: '#fafafa', background: token.colorBgLayout,
}} }}
> >
<MemorySidebar <MemorySidebar
+42 -41
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'; 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 { 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 { useStore } from '../store';
import { useChapterSync } from '../store/hooks'; import { useChapterSync } from '../store/hooks';
@@ -48,6 +48,7 @@ const setCachedWordCount = (value: number): void => {
export default function Chapters() { export default function Chapters() {
const { currentProject, chapters, outlines, setCurrentChapter, setCurrentProject } = useStore(); const { currentProject, chapters, outlines, setCurrentChapter, setCurrentProject } = useStore();
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
const { token } = theme.useToken();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditorOpen, setIsEditorOpen] = useState(false); const [isEditorOpen, setIsEditorOpen] = useState(false);
const [isContinuing, setIsContinuing] = useState(false); const [isContinuing, setIsContinuing] = useState(false);
@@ -900,11 +901,11 @@ export default function Chapters() {
<div style={{ <div style={{
marginTop: 16, marginTop: 16,
padding: 12, padding: 12,
background: 'var(--color-info-bg)', background: token.colorInfoBg,
borderRadius: 4, borderRadius: token.borderRadius,
border: '1px solid var(--color-info-border)' 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} 📚 {previousChapters.length}
</div> </div>
<div style={{ maxHeight: 150, overflowY: 'auto' }}> <div style={{ maxHeight: 150, overflowY: 'auto' }}>
@@ -914,13 +915,13 @@ export default function Chapters() {
</div> </div>
))} ))}
</div> </div>
<div style={{ marginTop: 8, fontSize: 12, color: '#666' }}> <div style={{ marginTop: 8, fontSize: 12, color: token.colorTextSecondary }}>
💡 AI会参考这些章节内容 💡 AI会参考这些章节内容
</div> </div>
</div> </div>
)} )}
<p style={{ color: '#ff4d4f', marginTop: 16, marginBottom: 0 }}> <p style={{ color: token.colorError, marginTop: 16, marginBottom: 0 }}>
</p> </p>
</div> </div>
@@ -1406,7 +1407,7 @@ export default function Chapters() {
// 显示冲突提示Modal // 显示冲突提示Modal
modal.confirm({ modal.confirm({
title: '章节序号冲突', title: '章节序号冲突',
icon: <InfoCircleOutlined style={{ color: '#ff4d4f' }} />, icon: <InfoCircleOutlined style={{ color: token.colorError }} />,
width: 500, width: 500,
centered: true, centered: true,
content: ( content: (
@@ -1416,9 +1417,9 @@ export default function Chapters() {
</p> </p>
<div style={{ <div style={{
padding: 12, padding: 12,
background: '#fff7e6', background: token.colorWarningBg,
borderRadius: 4, borderRadius: token.borderRadius,
border: '1px solid #ffd591', border: `1px solid ${token.colorWarningBorder}`,
marginBottom: 12 marginBottom: 12
}}> }}>
<div><strong></strong>{conflictChapter.title}</div> <div><strong></strong>{conflictChapter.title}</div>
@@ -1428,10 +1429,10 @@ export default function Chapters() {
<div><strong></strong>{conflictChapter.outline_title}</div> <div><strong></strong>{conflictChapter.outline_title}</div>
)} )}
</div> </div>
<p style={{ color: '#ff4d4f', marginBottom: 8 }}> <p style={{ color: token.colorError, marginBottom: 8 }}>
</p> </p>
<p style={{ fontSize: 12, color: '#666', marginBottom: 0 }}> <p style={{ fontSize: 12, color: token.colorTextSecondary, marginBottom: 0 }}>
</p> </p>
</div> </div>
@@ -1551,7 +1552,7 @@ export default function Chapters() {
modal.info({ modal.info({
title: ( title: (
<Space style={{ flexWrap: 'wrap' }}> <Space style={{ flexWrap: 'wrap' }}>
<InfoCircleOutlined style={{ color: 'var(--color-primary)' }} /> <InfoCircleOutlined style={{ color: token.colorPrimary }} />
<span style={{ wordBreak: 'break-word' }}>{chapter.chapter_number}</span> <span style={{ wordBreak: 'break-word' }}>{chapter.chapter_number}</span>
</Space> </Space>
), ),
@@ -1684,7 +1685,7 @@ export default function Chapters() {
key={idx} key={idx}
size="small" size="small"
style={{ style={{
backgroundColor: '#fafafa', backgroundColor: token.colorFillQuaternary,
maxWidth: '100%', maxWidth: '100%',
overflow: 'hidden' overflow: 'hidden'
}} }}
@@ -1875,10 +1876,10 @@ export default function Chapters() {
position: 'sticky', position: 'sticky',
top: 0, top: 0,
zIndex: 10, zIndex: 10,
backgroundColor: 'var(--color-bg-container)', backgroundColor: token.colorBgContainer,
padding: isMobile ? '12px 0' : '16px 0', padding: isMobile ? '12px 0' : '16px 0',
marginBottom: isMobile ? 12 : 16, marginBottom: isMobile ? 12 : 16,
borderBottom: '1px solid #f0f0f0', borderBottom: `1px solid ${token.colorBorderSecondary}`,
display: 'flex', display: 'flex',
flexDirection: isMobile ? 'column' : 'row', flexDirection: isMobile ? 'column' : 'row',
gap: isMobile ? 12 : 0, gap: isMobile ? 12 : 0,
@@ -1925,7 +1926,7 @@ export default function Chapters() {
disabled={chapters.length === 0 || batchAnalyzableChapterCount === 0} disabled={chapters.length === 0 || batchAnalyzableChapterCount === 0}
block={isMobile} block={isMobile}
size={isMobile ? 'middle' : 'middle'} size={isMobile ? 'middle' : 'middle'}
style={{ background: '#fa8c16', borderColor: '#fa8c16' }} style={{ background: token.colorWarning, borderColor: token.colorWarning }}
title={batchAnalyzableChapterCount === 0 ? '暂无可一键分析章节' : `可一键分析 ${batchAnalyzableChapterCount}`} title={batchAnalyzableChapterCount === 0 ? '暂无可一键分析章节' : `可一键分析 ${batchAnalyzableChapterCount}`}
> >
{batchAnalyzableChapterCount > 0 ? ` (${batchAnalyzableChapterCount})` : ''} {batchAnalyzableChapterCount > 0 ? ` (${batchAnalyzableChapterCount})` : ''}
@@ -1937,7 +1938,7 @@ export default function Chapters() {
disabled={chapters.length === 0} disabled={chapters.length === 0}
block={isMobile} block={isMobile}
size={isMobile ? 'middle' : 'middle'} size={isMobile ? 'middle' : 'middle'}
style={{ background: '#722ed1', borderColor: '#722ed1' }} style={{ background: token.colorInfo, borderColor: token.colorInfo }}
> >
</Button> </Button>
@@ -1969,9 +1970,9 @@ export default function Chapters() {
style={{ style={{
padding: '16px', padding: '16px',
marginBottom: 16, marginBottom: 16,
background: '#fff', background: token.colorBgContainer,
borderRadius: 8, borderRadius: token.borderRadius,
border: '1px solid #f0f0f0', border: `1px solid ${token.colorBorderSecondary}`,
flexDirection: isMobile ? 'column' : 'row', flexDirection: isMobile ? 'column' : 'row',
alignItems: isMobile ? 'flex-start' : 'center', alignItems: isMobile ? 'flex-start' : 'center',
}} }}
@@ -2025,7 +2026,7 @@ export default function Chapters() {
> >
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<List.Item.Meta <List.Item.Meta
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: 'var(--color-primary)' }} />} avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: token.colorPrimary }} />}
title={ title={
<div style={{ <div style={{
display: 'flex', display: 'flex',
@@ -2039,7 +2040,7 @@ export default function Chapters() {
</span> </span>
<Space wrap size={isMobile ? 4 : 8}> <Space wrap size={isMobile ? 4 : 8}>
<Tag color={getStatusColor(item.status)}>{getStatusText(item.status)}</Tag> <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)} {renderAnalysisStatus(item.id)}
{!canGenerateChapter(item) && ( {!canGenerateChapter(item) && (
<Tag icon={<LockOutlined />} color="warning" title={getGenerateDisabledReason(item)}> <Tag icon={<LockOutlined />} color="warning" title={getGenerateDisabledReason(item)}>
@@ -2051,12 +2052,12 @@ export default function Chapters() {
} }
description={ description={
item.content ? ( 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.substring(0, isMobile ? 80 : 150)}
{item.content.length > (isMobile ? 80 : 150) && '...'} {item.content.length > (isMobile ? 80 : 150) && '...'}
</div> </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> </span>
<Badge <Badge
count={`${group.chapters.length}`} count={`${group.chapters.length}`}
style={{ backgroundColor: 'var(--color-success)' }} style={{ backgroundColor: token.colorSuccess }}
/> />
<Badge <Badge
count={`${group.chapters.reduce((sum, ch) => sum + (ch.word_count || 0), 0)}`} count={`${group.chapters.reduce((sum, ch) => sum + (ch.word_count || 0), 0)}`}
style={{ backgroundColor: 'var(--color-primary)' }} style={{ backgroundColor: token.colorPrimary }}
/> />
</div> </div>
} }
style={{ style={{
marginBottom: 16, marginBottom: 16,
background: '#fff', background: token.colorBgContainer,
borderRadius: 8, borderRadius: token.borderRadius,
border: '1px solid #f0f0f0', border: `1px solid ${token.colorBorderSecondary}`,
}} }}
> >
<List <List
@@ -2230,7 +2231,7 @@ export default function Chapters() {
> >
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<List.Item.Meta <List.Item.Meta
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: 'var(--color-primary)' }} />} avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: token.colorPrimary }} />}
title={ title={
<div style={{ <div style={{
display: 'flex', display: 'flex',
@@ -2244,7 +2245,7 @@ export default function Chapters() {
</span> </span>
<Space wrap size={isMobile ? 4 : 8}> <Space wrap size={isMobile ? 4 : 8}>
<Tag color={getStatusColor(item.status)}>{getStatusText(item.status)}</Tag> <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)} {renderAnalysisStatus(item.id)}
{!canGenerateChapter(item) && ( {!canGenerateChapter(item) && (
<Tag icon={<LockOutlined />} color="warning" title={getGenerateDisabledReason(item)}> <Tag icon={<LockOutlined />} color="warning" title={getGenerateDisabledReason(item)}>
@@ -2255,7 +2256,7 @@ export default function Chapters() {
{item.expansion_plan && ( {item.expansion_plan && (
<InfoCircleOutlined <InfoCircleOutlined
title="查看展开详情" title="查看展开详情"
style={{ color: 'var(--color-primary)', cursor: 'pointer', fontSize: 16 }} style={{ color: token.colorPrimary, cursor: 'pointer', fontSize: 16 }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
showExpansionPlanModal(item); showExpansionPlanModal(item);
@@ -2264,7 +2265,7 @@ export default function Chapters() {
)} )}
<FormOutlined <FormOutlined
title={item.expansion_plan ? "编辑规划信息" : "创建规划信息"} title={item.expansion_plan ? "编辑规划信息" : "创建规划信息"}
style={{ color: 'var(--color-success)', cursor: 'pointer', fontSize: 16 }} style={{ color: token.colorSuccess, cursor: 'pointer', fontSize: 16 }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleOpenPlanEditor(item); handleOpenPlanEditor(item);
@@ -2276,12 +2277,12 @@ export default function Chapters() {
} }
description={ description={
item.content ? ( 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.substring(0, isMobile ? 80 : 150)}
{item.content.length > (isMobile ? 80 : 150) && '...'} {item.content.length > (isMobile ? 80 : 150) && '...'}
</div> </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> </Select>
{!selectedStyleId && ( {!selectedStyleId && (
<div style={{ color: '#ff4d4f', fontSize: 12, marginTop: 4 }}></div> <div style={{ color: token.colorError, fontSize: 12, marginTop: 4 }}></div>
)} )}
</Form.Item> </Form.Item>
@@ -2560,7 +2561,7 @@ export default function Chapters() {
<Select.Option value="全知视角"></Select.Option> <Select.Option value="全知视角"></Select.Option>
</Select> </Select>
{temporaryNarrativePerspective && ( {temporaryNarrativePerspective && (
<div style={{ color: 'var(--color-success)', fontSize: 12, marginTop: 4 }}> <div style={{ color: token.colorSuccess, fontSize: 12, marginTop: 4 }}>
{getNarrativePerspectiveText(temporaryNarrativePerspective)} {getNarrativePerspectiveText(temporaryNarrativePerspective)}
</div> </div>
)} )}
@@ -2703,7 +2704,7 @@ export default function Chapters() {
<Modal <Modal
title={ title={
<Space> <Space>
<RocketOutlined style={{ color: '#722ed1' }} /> <RocketOutlined style={{ color: token.colorInfo }} />
<span></span> <span></span>
</Space> </Space>
} }
@@ -2879,7 +2880,7 @@ export default function Chapters() {
> >
<Radio.Group disabled> <Radio.Group disabled>
<Radio value={true}> <Radio value={true}>
<span style={{ fontSize: 12, color: '#52c41a' }}> </span> <span style={{ fontSize: 12, color: token.colorSuccess }}> </span>
</Radio> </Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
+32 -31
View File
@@ -1,9 +1,9 @@
import { useState, useEffect, useRef } from 'react'; 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 { ThunderboltOutlined, UserOutlined, TeamOutlined, PlusOutlined, ExportOutlined, ImportOutlined, DownloadOutlined } from '@ant-design/icons';
import { useStore } from '../store'; import { useStore } from '../store';
import { useCharacterSync } from '../store/hooks'; import { useCharacterSync } from '../store/hooks';
import { characterGridConfig } from '../components/CardStyles'; import { charactersPageGridConfig } from '../components/CardStyles';
import { CharacterCard } from '../components/CharacterCard'; import { CharacterCard } from '../components/CharacterCard';
import { SSELoadingOverlay } from '../components/SSELoadingOverlay'; import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
import type { Character, ApiError } from '../types'; import type { Character, ApiError } from '../types';
@@ -94,6 +94,7 @@ interface CharacterUpdateData {
} }
export default function Characters() { export default function Characters() {
const { token } = theme.useToken();
const { currentProject, characters } = useStore(); const { currentProject, characters } = useStore();
const [isGenerating, setIsGenerating] = useState(false); const [isGenerating, setIsGenerating] = useState(false);
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
@@ -390,7 +391,7 @@ export default function Characters() {
content: ( content: (
<div> <div>
{validation.errors.map((error, index) => ( {validation.errors.map((error, index) => (
<div key={index} style={{ color: 'red' }}> {error}</div> <div key={index} style={{ color: token.colorError }}> {error}</div>
))} ))}
</div> </div>
), ),
@@ -415,10 +416,10 @@ export default function Characters() {
{validation.warnings.length > 0 && ( {validation.warnings.length > 0 && (
<> <>
<Divider style={{ margin: '12px 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 }}> <ul style={{ marginLeft: 20 }}>
{validation.warnings.map((warning, index) => ( {validation.warnings.map((warning, index) => (
<li key={index} style={{ color: '#faad14' }}>{warning}</li> <li key={index} style={{ color: token.colorWarning }}>{warning}</li>
))} ))}
</ul> </ul>
</> </>
@@ -463,10 +464,10 @@ export default function Characters() {
{result.statistics.skipped > 0 && ( {result.statistics.skipped > 0 && (
<> <>
<Divider style={{ margin: '12px 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 }}> <ul style={{ marginLeft: 20 }}>
{result.details.skipped.map((name, index) => ( {result.details.skipped.map((name, index) => (
<li key={index} style={{ color: '#faad14' }}>{name}</li> <li key={index} style={{ color: token.colorWarning }}>{name}</li>
))} ))}
</ul> </ul>
</> </>
@@ -474,10 +475,10 @@ export default function Characters() {
{result.warnings.length > 0 && ( {result.warnings.length > 0 && (
<> <>
<Divider style={{ margin: '12px 0' }} /> <Divider style={{ margin: '12px 0' }} />
<p style={{ color: '#faad14' }}> :</p> <p style={{ color: token.colorWarning }}> :</p>
<ul style={{ marginLeft: 20 }}> <ul style={{ marginLeft: 20 }}>
{result.warnings.map((warning, index) => ( {result.warnings.map((warning, index) => (
<li key={index} style={{ color: '#faad14' }}>{warning}</li> <li key={index} style={{ color: token.colorWarning }}>{warning}</li>
))} ))}
</ul> </ul>
</> </>
@@ -485,10 +486,10 @@ export default function Characters() {
{result.details.errors.length > 0 && ( {result.details.errors.length > 0 && (
<> <>
<Divider style={{ margin: '12px 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 }}> <ul style={{ marginLeft: 20 }}>
{result.details.errors.map((error, index) => ( {result.details.errors.map((error, index) => (
<li key={index} style={{ color: 'red' }}>{error}</li> <li key={index} style={{ color: token.colorError }}>{error}</li>
))} ))}
</ul> </ul>
</> </>
@@ -777,7 +778,7 @@ export default function Characters() {
<Empty description="还没有角色或组织,开始创建吧!" /> <Empty description="还没有角色或组织,开始创建吧!" />
) : ( ) : (
<> <>
<Row gutter={isMobile ? [8, 8] : characterGridConfig.gutter}> <Row gutter={isMobile ? [8, 8] : charactersPageGridConfig.gutter}>
{activeTab === 'all' && ( {activeTab === 'all' && (
<> <>
{characterList.length > 0 && ( {characterList.length > 0 && (
@@ -793,10 +794,10 @@ export default function Characters() {
{characterList.map((character) => ( {characterList.map((character) => (
<Col <Col
xs={24} xs={24}
sm={characterGridConfig.sm} sm={charactersPageGridConfig.sm}
md={characterGridConfig.md} md={charactersPageGridConfig.md}
lg={characterGridConfig.lg} lg={charactersPageGridConfig.lg}
xl={characterGridConfig.xl} xl={charactersPageGridConfig.xl}
key={character.id} key={character.id}
style={{ padding: isMobile ? '4px' : '8px' }} style={{ padding: isMobile ? '4px' : '8px' }}
> >
@@ -831,10 +832,10 @@ export default function Characters() {
{organizationList.map((org) => ( {organizationList.map((org) => (
<Col <Col
xs={24} xs={24}
sm={characterGridConfig.sm} sm={charactersPageGridConfig.sm}
md={characterGridConfig.md} md={charactersPageGridConfig.md}
lg={characterGridConfig.lg} lg={charactersPageGridConfig.lg}
xl={characterGridConfig.xl} xl={charactersPageGridConfig.xl}
key={org.id} key={org.id}
style={{ padding: isMobile ? '4px' : '8px' }} style={{ padding: isMobile ? '4px' : '8px' }}
> >
@@ -861,10 +862,10 @@ export default function Characters() {
{activeTab === 'character' && characterList.map((character) => ( {activeTab === 'character' && characterList.map((character) => (
<Col <Col
xs={24} xs={24}
sm={characterGridConfig.sm} sm={charactersPageGridConfig.sm}
md={characterGridConfig.md} md={charactersPageGridConfig.md}
lg={characterGridConfig.lg} lg={charactersPageGridConfig.lg}
xl={characterGridConfig.xl} xl={charactersPageGridConfig.xl}
key={character.id} key={character.id}
style={{ padding: isMobile ? '4px' : '8px' }} style={{ padding: isMobile ? '4px' : '8px' }}
> >
@@ -887,10 +888,10 @@ export default function Characters() {
{activeTab === 'organization' && organizationList.map((org) => ( {activeTab === 'organization' && organizationList.map((org) => (
<Col <Col
xs={24} xs={24}
sm={characterGridConfig.sm} sm={charactersPageGridConfig.sm}
md={characterGridConfig.md} md={charactersPageGridConfig.md}
lg={characterGridConfig.lg} lg={charactersPageGridConfig.lg}
xl={characterGridConfig.xl} xl={charactersPageGridConfig.xl}
key={org.id} key={org.id}
style={{ padding: isMobile ? '4px' : '8px' }} style={{ padding: isMobile ? '4px' : '8px' }}
> >
@@ -1020,7 +1021,7 @@ export default function Characters() {
value={editingCharacter.relationships} value={editingCharacter.relationships}
readOnly readOnly
autoSize={{ minRows: 1, maxRows: 3 }} autoSize={{ minRows: 1, maxRows: 3 }}
style={{ backgroundColor: '#f5f5f5', cursor: 'default' }} style={{ backgroundColor: token.colorFillTertiary, cursor: 'default' }}
/> />
</Form.Item> </Form.Item>
)} )}
@@ -1195,10 +1196,10 @@ export default function Characters() {
disabled disabled
autoSize={{ minRows: 1, maxRows: 4 }} autoSize={{ minRows: 1, maxRows: 4 }}
placeholder="暂无成员,请在组织管理中添加" placeholder="暂无成员,请在组织管理中添加"
style={{ color: '#333', backgroundColor: '#fafafa' }} style={{ color: token.colorText, backgroundColor: token.colorFillAlter }}
/> />
</Form.Item> </Form.Item>
<div style={{ marginBottom: 12, fontSize: 12, color: '#8c8c8c' }}> <div style={{ marginBottom: 12, fontSize: 12, color: token.colorTextTertiary }}>
💡 💡
</div> </div>
+12 -11
View File
@@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom';
import { import {
Card, Table, Button, Tag, Space, Modal, Form, Input, Select, Card, Table, Button, Tag, Space, Modal, Form, Input, Select,
InputNumber, Switch, message, Tooltip, Popconfirm, Statistic, 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'; } from 'antd';
import type { MenuProps } from 'antd'; import type { MenuProps } from 'antd';
import { import {
@@ -73,6 +73,7 @@ export default function Foreshadows() {
// 表格容器引用,用于计算滚动高度 // 表格容器引用,用于计算滚动高度
const tableContainerRef = useRef<HTMLDivElement>(null); const tableContainerRef = useRef<HTMLDivElement>(null);
const [tableScrollY, setTableScrollY] = useState<number>(400); const [tableScrollY, setTableScrollY] = useState<number>(400);
const { token } = theme.useToken();
// 加载伏笔列表 // 加载伏笔列表
const loadForeshadows = useCallback(async () => { const loadForeshadows = useCallback(async () => {
@@ -508,22 +509,22 @@ export default function Foreshadows() {
</Col> </Col>
<Col span={3}> <Col span={3}>
<Card size="small"> <Card size="small">
<Statistic title="待埋入" value={stats.pending} valueStyle={{ color: '#8c8c8c' }} /> <Statistic title="待埋入" value={stats.pending} valueStyle={{ color: token.colorTextSecondary }} />
</Card> </Card>
</Col> </Col>
<Col span={3}> <Col span={3}>
<Card size="small"> <Card size="small">
<Statistic title="已埋入" value={stats.planted} valueStyle={{ color: '#52c41a' }} /> <Statistic title="已埋入" value={stats.planted} valueStyle={{ color: token.colorSuccess }} />
</Card> </Card>
</Col> </Col>
<Col span={3}> <Col span={3}>
<Card size="small"> <Card size="small">
<Statistic title="已回收" value={stats.resolved} valueStyle={{ color: '#1890ff' }} /> <Statistic title="已回收" value={stats.resolved} valueStyle={{ color: token.colorPrimary }} />
</Card> </Card>
</Col> </Col>
<Col span={3}> <Col span={3}>
<Card size="small"> <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> </Card>
</Col> </Col>
<Col span={3}> <Col span={3}>
@@ -531,7 +532,7 @@ export default function Foreshadows() {
<Statistic <Statistic
title="超期未回收" title="超期未回收"
value={stats.overdue_count} 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} prefix={stats.overdue_count > 0 ? <WarningOutlined /> : null}
/> />
</Card> </Card>
@@ -659,12 +660,12 @@ export default function Foreshadows() {
{/* 分页器 - 固定在底部居中 */} {/* 分页器 - 固定在底部居中 */}
<div style={{ <div style={{
padding: '12px 0', padding: '12px 0',
borderTop: '1px solid #f0f0f0', borderTop: `1px solid ${token.colorBorderSecondary}`,
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
flexShrink: 0, flexShrink: 0,
background: '#fff', background: token.colorBgContainer,
}}> }}>
<Pagination <Pagination
current={currentPage} current={currentPage}
@@ -868,7 +869,7 @@ export default function Foreshadows() {
{currentForeshadow.hint_text && ( {currentForeshadow.hint_text && (
<Col span={24}> <Col span={24}>
<strong></strong> <strong></strong>
<p style={{ marginTop: 8, whiteSpace: 'pre-wrap', color: '#666' }}> <p style={{ marginTop: 8, whiteSpace: 'pre-wrap', color: token.colorTextSecondary }}>
{currentForeshadow.hint_text} {currentForeshadow.hint_text}
</p> </p>
</Col> </Col>
@@ -877,7 +878,7 @@ export default function Foreshadows() {
{currentForeshadow.resolution_text && ( {currentForeshadow.resolution_text && (
<Col span={24}> <Col span={24}>
<strong></strong> <strong></strong>
<p style={{ marginTop: 8, whiteSpace: 'pre-wrap', color: '#666' }}> <p style={{ marginTop: 8, whiteSpace: 'pre-wrap', color: token.colorTextSecondary }}>
{currentForeshadow.resolution_text} {currentForeshadow.resolution_text}
</p> </p>
</Col> </Col>
@@ -920,7 +921,7 @@ export default function Foreshadows() {
{currentForeshadow.notes && ( {currentForeshadow.notes && (
<Col span={24}> <Col span={24}>
<strong></strong> <strong></strong>
<p style={{ marginTop: 8, color: '#666' }}>{currentForeshadow.notes}</p> <p style={{ marginTop: 8, color: token.colorTextSecondary }}>{currentForeshadow.notes}</p>
</Col> </Col>
)} )}
+27 -26
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef, useCallback } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Card, Input, Button, Space, Typography, message, Spin, Modal } from 'antd'; import { Card, Input, Button, Space, Typography, message, Spin, Modal, theme } from 'antd';
import { SendOutlined, ArrowLeftOutlined, ReloadOutlined } from '@ant-design/icons'; import { SendOutlined, ArrowLeftOutlined, ReloadOutlined } from '@ant-design/icons';
import { inspirationApi } from '../services/api'; import { inspirationApi } from '../services/api';
import { AIProjectGenerator, type GenerationConfig } from '../components/AIProjectGenerator'; import { AIProjectGenerator, type GenerationConfig } from '../components/AIProjectGenerator';
@@ -52,6 +52,7 @@ const Inspiration: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [currentStep, setCurrentStep] = useState<Step>('idea'); const [currentStep, setCurrentStep] = useState<Step>('idea');
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const { token } = theme.useToken();
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
@@ -852,7 +853,7 @@ const Inspiration: React.FC = () => {
height: isMobile ? 'calc(100vh - 280px)' : 600, height: isMobile ? 'calc(100vh - 280px)' : 600,
overflowY: 'auto', overflowY: 'auto',
marginBottom: 16, 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' scrollBehavior: 'smooth'
}} }}
> >
@@ -873,16 +874,16 @@ const Inspiration: React.FC = () => {
maxWidth: '80%', maxWidth: '80%',
padding: '12px 16px', padding: '12px 16px',
borderRadius: 12, borderRadius: 12,
background: msg.type === 'ai' ? 'var(--color-bg-container)' : 'var(--color-primary)', background: msg.type === 'ai' ? token.colorBgContainer : token.colorPrimary,
color: msg.type === 'ai' ? 'var(--color-text-primary)' : '#fff', color: msg.type === 'ai' ? token.colorText : token.colorWhite,
boxShadow: msg.type === 'ai' boxShadow: msg.type === 'ai'
? 'var(--shadow-card)' ? `0 2px 10px color-mix(in srgb, ${token.colorTextBase} 12%, transparent)`
: 'var(--shadow-primary)', : `0 4px 14px color-mix(in srgb, ${token.colorPrimary} 30%, transparent)`,
}}> }}>
<Paragraph <Paragraph
style={{ style={{
margin: 0, margin: 0,
color: msg.type === 'ai' ? 'var(--color-text-primary)' : '#fff', color: msg.type === 'ai' ? token.colorText : token.colorWhite,
whiteSpace: 'pre-wrap' whiteSpace: 'pre-wrap'
}} }}
> >
@@ -904,13 +905,13 @@ const Inspiration: React.FC = () => {
style={{ style={{
cursor: msg.optionsDisabled ? 'not-allowed' : 'pointer', cursor: msg.optionsDisabled ? 'not-allowed' : 'pointer',
border: msg.isMultiSelect && selectedOptions.includes(option) border: msg.isMultiSelect && selectedOptions.includes(option)
? '2px solid var(--color-primary)' ? `2px solid ${token.colorPrimary}`
: '1px solid var(--color-border)', : `1px solid ${token.colorBorder}`,
background: msg.optionsDisabled background: msg.optionsDisabled
? 'var(--color-bg-layout)' ? token.colorBgLayout
: msg.isMultiSelect && selectedOptions.includes(option) : msg.isMultiSelect && selectedOptions.includes(option)
? 'var(--color-bg-spotlight)' ? token.colorPrimaryBg
: 'var(--color-bg-container)', : token.colorBgContainer,
opacity: msg.optionsDisabled ? 0.6 : 1, opacity: msg.optionsDisabled ? 0.6 : 1,
animation: 'floatIn 0.6s ease-out', animation: 'floatIn 0.6s ease-out',
animationDelay: `${optIndex * 0.1}s`, animationDelay: `${optIndex * 0.1}s`,
@@ -920,7 +921,7 @@ const Inspiration: React.FC = () => {
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (!msg.optionsDisabled) { if (!msg.optionsDisabled) {
e.currentTarget.style.transform = 'translateY(-2px) scale(1.02)'; 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) => { onMouseLeave={(e) => {
@@ -947,7 +948,7 @@ const Inspiration: React.FC = () => {
{/* 反馈优化区域 - 新增 */} {/* 反馈优化区域 - 新增 */}
{msg.canRefine && !msg.optionsDisabled && !msg.isMultiSelect && ( {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 ? ( {showFeedbackInput === index ? (
<Space direction="vertical" style={{ width: '100%' }} size="small"> <Space direction="vertical" style={{ width: '100%' }} size="small">
<TextArea <TextArea
@@ -1018,7 +1019,7 @@ const Inspiration: React.FC = () => {
</Card> </Card>
<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 } }} styles={{ body: { padding: 12 } }}
> >
<Space.Compact style={{ width: '100%' }}> <Space.Compact style={{ width: '100%' }}>
@@ -1059,7 +1060,7 @@ const Inspiration: React.FC = () => {
return ( return (
<div style={{ <div style={{
minHeight: '100dvh', minHeight: '100dvh',
background: 'var(--color-bg-base)', background: token.colorBgBase,
}}> }}>
{contextHolder} {contextHolder}
<style> <style>
@@ -1105,8 +1106,8 @@ const Inspiration: React.FC = () => {
position: 'sticky', position: 'sticky',
top: 0, top: 0,
zIndex: 100, zIndex: 100,
background: 'var(--color-primary)', background: token.colorPrimary,
boxShadow: 'var(--shadow-header)', boxShadow: `0 6px 20px color-mix(in srgb, ${token.colorPrimary} 30%, transparent)`,
}}> }}>
<div style={{ <div style={{
maxWidth: 1200, maxWidth: 1200,
@@ -1121,9 +1122,9 @@ const Inspiration: React.FC = () => {
onClick={handleBack} onClick={handleBack}
size={isMobile ? 'middle' : 'large'} size={isMobile ? 'middle' : 'large'}
style={{ style={{
background: 'rgba(255,255,255,0.2)', background: `color-mix(in srgb, ${token.colorWhite} 20%, transparent)`,
borderColor: 'rgba(255,255,255,0.3)', borderColor: `color-mix(in srgb, ${token.colorWhite} 30%, transparent)`,
color: '#fff', color: token.colorWhite,
}} }}
> >
{isMobile ? '返回' : '返回首页'} {isMobile ? '返回' : '返回首页'}
@@ -1134,8 +1135,8 @@ const Inspiration: React.FC = () => {
level={isMobile ? 4 : 2} level={isMobile ? 4 : 2}
style={{ style={{
margin: 0, margin: 0,
color: '#fff', color: token.colorWhite,
textShadow: '0 2px 4px rgba(0,0,0,0.1)', textShadow: '0 2px 4px color-mix(in srgb, var(--ant-color-black) 18%, transparent)',
lineHeight: 1.2 lineHeight: 1.2
}} }}
> >
@@ -1162,9 +1163,9 @@ const Inspiration: React.FC = () => {
}} }}
size={isMobile ? 'middle' : 'large'} size={isMobile ? 'middle' : 'large'}
style={{ style={{
background: 'rgba(255,255,255,0.2)', background: `color-mix(in srgb, ${token.colorWhite} 20%, transparent)`,
borderColor: 'rgba(255,255,255,0.3)', borderColor: `color-mix(in srgb, ${token.colorWhite} 30%, transparent)`,
color: '#fff', color: token.colorWhite,
}} }}
> >
{isMobile ? '重新' : '重新开始'} {isMobile ? '重新' : '重新开始'}
+90 -64
View File
@@ -16,6 +16,7 @@ import {
Alert, Alert,
Row, Row,
Col, Col,
theme,
} from 'antd'; } from 'antd';
import { import {
PlusOutlined, PlusOutlined,
@@ -39,7 +40,32 @@ const { TextArea } = Input;
export default function MCPPluginsPage() { export default function MCPPluginsPage() {
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const [form] = Form.useForm(); 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(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
@@ -261,25 +287,25 @@ export default function MCPPluginsPage() {
width: isMobile ? '95%' : 700, width: isMobile ? '95%' : 700,
content: ( content: (
<div style={{ padding: '8px 0' }}> <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 }}> <div style={{ marginBottom: 16, padding: 12, background: statusStyles.success.bg, border: `1px solid ${statusStyles.success.border}`, borderRadius: 8 }}>
<Typography.Text strong style={{ color: 'var(--color-success)', fontSize: 14 }}> <Typography.Text strong style={{ color: statusStyles.success.text, fontSize: 14 }}>
{result.message} {result.message}
</Typography.Text> </Typography.Text>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 16 }}> <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> <Text type="secondary" style={{ fontSize: 12 }}></Text>
<div><Text strong style={{ fontSize: 20 }}>{result.tools_count || 0}</Text></div> <div><Text strong style={{ fontSize: 20 }}>{result.tools_count || 0}</Text></div>
</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> <Text type="secondary" style={{ fontSize: 12 }}></Text>
<div><Text strong style={{ fontSize: 20 }}>{result.response_time_ms?.toFixed(0) || 0}ms</Text></div> <div><Text strong style={{ fontSize: 20 }}>{result.response_time_ms?.toFixed(0) || 0}ms</Text></div>
</div> </div>
</div> </div>
{aiChoice && ( {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 type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🤖 AI选择的工具</Text>
<Text code strong>{aiChoice}</Text> <Text code strong>{aiChoice}</Text>
{callTime && <Tag color="blue" style={{ marginLeft: 8 }}>{callTime}</Tag>} {callTime && <Tag color="blue" style={{ marginLeft: 8 }}>{callTime}</Tag>}
@@ -289,7 +315,7 @@ export default function MCPPluginsPage() {
{paramsStr && ( {paramsStr && (
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📝 </Text> <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; } })()} {(() => { try { return JSON.stringify(JSON.parse(paramsStr), null, 2); } catch { return paramsStr; } })()}
</pre> </pre>
</div> </div>
@@ -298,7 +324,7 @@ export default function MCPPluginsPage() {
{resultStr && ( {resultStr && (
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📊 </Text> <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} {resultStr}
</pre> </pre>
</div> </div>
@@ -326,13 +352,13 @@ export default function MCPPluginsPage() {
{result.error && ( {result.error && (
<div style={{ <div style={{
padding: 16, padding: 16,
background: 'var(--color-error-bg)', background: statusStyles.error.bg,
border: '1px solid var(--color-error-border)', border: `1px solid ${statusStyles.error.border}`,
borderRadius: 8, borderRadius: 8,
marginBottom: 16 marginBottom: 16
}}> }}>
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>:</Text> <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} {result.error}
</Text> </Text>
</div> </div>
@@ -341,8 +367,8 @@ export default function MCPPluginsPage() {
{result.suggestions && result.suggestions.length > 0 && ( {result.suggestions && result.suggestions.length > 0 && (
<div style={{ <div style={{
padding: 16, padding: 16,
background: 'var(--color-warning-bg)', background: statusStyles.warning.bg,
border: '1px solid var(--color-warning-border)', border: `1px solid ${statusStyles.warning.border}`,
borderRadius: 8, borderRadius: 8,
marginBottom: 16 marginBottom: 16
}}> }}>
@@ -418,24 +444,24 @@ export default function MCPPluginsPage() {
width: isMobile ? '95%' : 700, width: isMobile ? '95%' : 700,
content: ( content: (
<div style={{ padding: '8px 0' }}> <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 }}> <div style={{ marginBottom: 16, padding: 12, background: statusStyles.success.bg, border: `1px solid ${statusStyles.success.border}`, borderRadius: 8 }}>
<Typography.Text strong style={{ color: 'var(--color-success)', fontSize: 14 }}> <Typography.Text strong style={{ color: statusStyles.success.text, fontSize: 14 }}>
{result.message} {result.message}
</Typography.Text> </Typography.Text>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 16 }}> <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> <Text type="secondary" style={{ fontSize: 12 }}>API </Text>
<div><Text strong style={{ fontSize: 16 }}>{result.provider}</Text></div> <div><Text strong style={{ fontSize: 16 }}>{result.provider}</Text></div>
</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> <Text type="secondary" style={{ fontSize: 12 }}></Text>
<div><Text strong style={{ fontSize: 16 }}>{result.response_time_ms?.toFixed(0) || 0}ms</Text></div> <div><Text strong style={{ fontSize: 16 }}>{result.response_time_ms?.toFixed(0) || 0}ms</Text></div>
</div> </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 type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🔧 </Text>
<Text code strong>{result.model}</Text> <Text code strong>{result.model}</Text>
{result.details?.finish_reason && ( {result.details?.finish_reason && (
@@ -446,7 +472,7 @@ export default function MCPPluginsPage() {
{result.details && ( {result.details && (
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📊 </Text> <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.tool_call_count || 0}</div>
<div> : {result.details.test_tool || 'N/A'}</div> <div> : {result.details.test_tool || 'N/A'}</div>
<div> : {result.details.response_type || '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 && ( {result.tool_calls && result.tool_calls.length > 0 && (
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🔨 </Text> <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)} {JSON.stringify(result.tool_calls[0], null, 2)}
</pre> </pre>
</div> </div>
)} )}
{result.suggestions && result.suggestions.length > 0 && ( {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> <Text strong style={{ fontSize: 13, display: 'block', marginBottom: 8 }}>💡 </Text>
<ul style={{ margin: 0, paddingLeft: 20, fontSize: 12 }}> <ul style={{ margin: 0, paddingLeft: 20, fontSize: 12 }}>
{result.suggestions.map((s: string, i: number) => ( {result.suggestions.map((s: string, i: number) => (
@@ -495,8 +521,8 @@ export default function MCPPluginsPage() {
{result.error && ( {result.error && (
<div style={{ <div style={{
padding: 16, padding: 16,
background: 'var(--color-warning-bg)', background: statusStyles.warning.bg,
border: '1px solid var(--color-warning-border)', border: `1px solid ${statusStyles.warning.border}`,
borderRadius: 8, borderRadius: 8,
marginBottom: 16 marginBottom: 16
}}> }}>
@@ -510,7 +536,7 @@ export default function MCPPluginsPage() {
{result.response_preview && ( {result.response_preview && (
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📝 200</Text> <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} {result.response_preview}
</pre> </pre>
</div> </div>
@@ -519,8 +545,8 @@ export default function MCPPluginsPage() {
{result.suggestions && result.suggestions.length > 0 && ( {result.suggestions && result.suggestions.length > 0 && (
<div style={{ <div style={{
padding: 16, padding: 16,
background: 'var(--color-info-bg)', background: statusStyles.info.bg,
border: '1px solid var(--color-info-border)', border: `1px solid ${statusStyles.info.border}`,
borderRadius: 8 borderRadius: 8
}}> }}>
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>💡 :</Text> <Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>💡 :</Text>
@@ -599,7 +625,7 @@ export default function MCPPluginsPage() {
{contextHolder} {contextHolder}
<div style={{ <div style={{
minHeight: '90vh', 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', padding: isMobile ? '20px 16px 70px' : '24px 24px 70px',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -616,9 +642,9 @@ export default function MCPPluginsPage() {
<Card <Card
variant="borderless" variant="borderless"
style={{ 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, 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, marginBottom: isMobile ? 20 : 24,
border: 'none', border: 'none',
position: 'relative', 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', 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: 'rgba(255, 255, 255, 0.05)', 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: 'rgba(255, 255, 255, 0.06)', 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 }}> <Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
<Col xs={24} sm={12}> <Col xs={24} sm={12}>
<Space direction="vertical" size={4}> <Space direction="vertical" size={4}>
<Space align="center"> <Space align="center">
<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 ${alphaColor(token.colorText, 0.2)}` }}>
<ToolOutlined style={{ color: 'rgba(255,255,255,0.9)', marginRight: 8 }} /> <ToolOutlined style={{ color: alphaColor(token.colorWhite, 0.9), marginRight: 8 }} />
MCP插件管理 MCP插件管理
</Title> </Title>
</Space> </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能力 AI能力
</Text> </Text>
</Space> </Space>
@@ -652,10 +678,10 @@ export default function MCPPluginsPage() {
onClick={handleCreate} onClick={handleCreate}
style={{ style={{
borderRadius: 12, borderRadius: 12,
background: 'rgba(255, 193, 7, 0.95)', background: alphaColor(token.colorWarning, 0.95),
border: '1px solid rgba(255, 255, 255, 0.3)', border: `1px solid ${alphaColor(token.colorWhite, 0.3)}`,
boxShadow: '0 4px 16px rgba(255, 193, 7, 0.4)', boxShadow: `0 4px 16px ${alphaColor(token.colorWarning, 0.4)}`,
color: '#fff', color: token.colorWhite,
fontWeight: 600 fontWeight: 600
}} }}
> >
@@ -671,10 +697,10 @@ export default function MCPPluginsPage() {
style={{ style={{
flex: 1, flex: 1,
borderRadius: 12, borderRadius: 12,
background: 'rgba(255, 255, 255, 0.9)', background: alphaColor(token.colorBgContainer, 0.9),
border: '1px solid rgba(255, 255, 255, 0.6)', border: `1px solid ${alphaColor(token.colorBorder, 0.6)}`,
backdropFilter: 'blur(10px)', 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 } }} styles={{ body: { padding: isMobile ? 14 : 20 } }}
> >
@@ -690,21 +716,21 @@ export default function MCPPluginsPage() {
width: isMobile ? 36 : 40, width: isMobile ? 36 : 40,
height: isMobile ? 36 : 40, height: isMobile ? 36 : 40,
borderRadius: '50%', 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', 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 flexShrink: 0
}}> }}>
{modelSupportStatus === 'supported' ? ( {modelSupportStatus === 'supported' ? (
<CheckCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: 'var(--color-success)' }} /> <CheckCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: statusStyles.success.text }} />
) : modelSupportStatus === 'unsupported' ? ( ) : 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>
<div style={{ flex: 1, minWidth: 0 }}> <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 }}> <Text type="secondary" style={{ fontSize: isMobile ? 12 : 13, display: 'block', lineHeight: 1.5 }}>
{modelSupportStatus === 'supported' {modelSupportStatus === 'supported'
? '当前模型支持 Function Calling,可正常使用 MCP 插件' ? '当前模型支持 Function Calling,可正常使用 MCP 插件'
@@ -732,18 +758,18 @@ export default function MCPPluginsPage() {
style={{ style={{
flex: 1, flex: 1,
borderRadius: 12, borderRadius: 12,
background: 'rgba(230, 247, 255, 0.6)', background: alphaColor(token.colorInfoBg, 0.7),
border: '1px solid rgba(145, 213, 255, 0.6)', border: `1px solid ${alphaColor(token.colorInfoBorder, 0.8)}`,
backdropFilter: 'blur(10px)', 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 } }} styles={{ body: { padding: isMobile ? 14 : 20 } }}
> >
<Space align="start"> <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 }}> <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 strong style={{ fontSize: isMobile ? 14 : 16, display: 'block', color: token.colorText, marginBottom: 4 }}> MCP </Text>
<Text style={{ fontSize: isMobile ? 12 : 13, display: 'block', color: 'var(--color-text-secondary)', lineHeight: 1.6 }}> <Text style={{ fontSize: isMobile ? 12 : 13, display: 'block', color: token.colorTextSecondary, lineHeight: 1.6 }}>
MCP (Model Context Protocol) AI AI 访API MCP (Model Context Protocol) AI AI 访API
</Text> </Text>
</div> </div>
@@ -794,7 +820,7 @@ export default function MCPPluginsPage() {
size="small" size="small"
style={{ style={{
borderRadius: 8, borderRadius: 8,
border: '1px solid #f0f0f0', border: `1px solid ${token.colorBorderSecondary}`,
}} }}
styles={{ body: { padding: isMobile ? 12 : 16 } }} styles={{ body: { padding: isMobile ? 12 : 16 } }}
> >
@@ -932,7 +958,7 @@ export default function MCPPluginsPage() {
alignItems: 'center', alignItems: 'center',
gap: isMobile ? 8 : 8, gap: isMobile ? 8 : 8,
flexWrap: 'wrap', flexWrap: 'wrap',
borderTop: isMobile ? '1px solid #f0f0f0' : 'none', borderTop: isMobile ? `1px solid ${token.colorBorderSecondary}` : 'none',
paddingTop: isMobile ? 12 : 0 paddingTop: isMobile ? 12 : 0
}}> }}>
{/* 桌面端显示开关 */} {/* 桌面端显示开关 */}
@@ -1055,7 +1081,7 @@ export default function MCPPluginsPage() {
<Modal <Modal
title={ title={
<Space> <Space>
<ToolOutlined style={{ color: 'var(--color-primary)' }} /> <ToolOutlined style={{ color: token.colorPrimary }} />
<span></span> <span></span>
{viewingTools && viewingTools.tools.length > 0 && ( {viewingTools && viewingTools.tools.length > 0 && (
<Tag color="blue">{viewingTools.tools.length} </Tag> <Tag color="blue">{viewingTools.tools.length} </Tag>
@@ -1094,12 +1120,12 @@ export default function MCPPluginsPage() {
size="small" size="small"
style={{ style={{
borderRadius: 8, borderRadius: 8,
border: '1px solid var(--color-border-secondary)', border: `1px solid ${token.colorBorderSecondary}`,
boxShadow: '0 2px 4px rgba(0,0,0,0.05)' boxShadow: `0 2px 4px ${alphaColor(token.colorText, 0.08)}`
}} }}
title={ title={
<Space> <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} {tool.name}
</Text> </Text>
<Tag color="processing" style={{ fontSize: '11px' }}> <Tag color="processing" style={{ fontSize: '11px' }}>
@@ -1119,9 +1145,9 @@ export default function MCPPluginsPage() {
margin: 0, margin: 0,
fontSize: isMobile ? '12px' : '13px', fontSize: isMobile ? '12px' : '13px',
padding: '8px 12px', padding: '8px 12px',
background: 'var(--color-bg-layout)', background: token.colorBgLayout,
borderRadius: 4, borderRadius: 4,
borderLeft: '3px solid var(--color-info)' borderLeft: `3px solid ${token.colorInfo}`
}} }}
> >
{tool.description} {tool.description}
@@ -1137,12 +1163,12 @@ export default function MCPPluginsPage() {
style={{ style={{
margin: 0, margin: 0,
padding: isMobile ? '8px' : '12px', padding: isMobile ? '8px' : '12px',
background: 'var(--color-bg-layout)', background: token.colorBgLayout,
borderRadius: 4, borderRadius: 4,
fontSize: isMobile ? '11px' : '12px', fontSize: isMobile ? '11px' : '12px',
overflow: 'auto', overflow: 'auto',
maxHeight: '200px', maxHeight: '200px',
border: '1px solid var(--color-border-secondary)', border: `1px solid ${token.colorBorderSecondary}`,
lineHeight: 1.6 lineHeight: 1.6
}} }}
> >
+12 -11
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'react-router-dom'; 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 { PlusOutlined, UserOutlined, EditOutlined, DeleteOutlined, UnorderedListOutlined, BankOutlined } from '@ant-design/icons';
import { useStore } from '../store'; import { useStore } from '../store';
import { useCharacterSync } from '../store/hooks'; import { useCharacterSync } from '../store/hooks';
@@ -58,6 +58,7 @@ export default function Organizations() {
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
const [orgListVisible, setOrgListVisible] = useState(false); const [orgListVisible, setOrgListVisible] = useState(false);
const { token } = theme.useToken();
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
@@ -310,7 +311,7 @@ export default function Organizations() {
<div style={{ <div style={{
padding: '16px 0', padding: '16px 0',
marginBottom: 16, marginBottom: 16,
borderBottom: '1px solid #f0f0f0' borderBottom: `1px solid ${token.colorBorderSecondary}`
}}> }}>
<h2 style={{ margin: 0, fontSize: 24 }}> <h2 style={{ margin: 0, fontSize: 24 }}>
<BankOutlined style={{ marginRight: 8 }} /> <BankOutlined style={{ marginRight: 8 }} />
@@ -335,7 +336,7 @@ export default function Organizations() {
loading={loading} loading={loading}
> >
{organizations.length === 0 ? ( {organizations.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#999' }}> <div style={{ textAlign: 'center', padding: '40px 20px', color: token.colorTextTertiary }}>
</div> </div>
) : ( ) : (
@@ -347,15 +348,15 @@ export default function Organizations() {
hoverable hoverable
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
border: selectedOrg?.id === org.id ? '2px solid #1890ff' : '1px solid #d9d9d9', border: selectedOrg?.id === org.id ? `2px solid ${token.colorPrimary}` : `1px solid ${token.colorBorder}`,
background: selectedOrg?.id === org.id ? '#e6f7ff' : 'transparent' background: selectedOrg?.id === org.id ? token.colorPrimaryBg : 'transparent'
}} }}
onClick={() => handleSelectOrganization(org)} onClick={() => handleSelectOrganization(org)}
> >
<Space direction="vertical" size="small" style={{ width: '100%' }}> <Space direction="vertical" size="small" style={{ width: '100%' }}>
<strong style={{ fontSize: 14 }}>{org.name}</strong> <strong style={{ fontSize: 14 }}>{org.name}</strong>
<Tag color="blue">{org.type}</Tag> <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} : {org.member_count} | : {org.power_level}
</div> </div>
</Space> </Space>
@@ -377,7 +378,7 @@ export default function Organizations() {
styles={{ body: { padding: 0 } }} styles={{ body: { padding: 0 } }}
> >
{organizations.length === 0 ? ( {organizations.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#999' }}> <div style={{ textAlign: 'center', padding: '40px 20px', color: token.colorTextTertiary }}>
</div> </div>
) : ( ) : (
@@ -389,8 +390,8 @@ export default function Organizations() {
hoverable hoverable
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
border: selectedOrg?.id === org.id ? '2px solid #1890ff' : '1px solid #d9d9d9', border: selectedOrg?.id === org.id ? `2px solid ${token.colorPrimary}` : `1px solid ${token.colorBorder}`,
background: selectedOrg?.id === org.id ? '#e6f7ff' : 'transparent' background: selectedOrg?.id === org.id ? token.colorPrimaryBg : 'transparent'
}} }}
onClick={() => { onClick={() => {
handleSelectOrganization(org); handleSelectOrganization(org);
@@ -400,7 +401,7 @@ export default function Organizations() {
<Space direction="vertical" size="small" style={{ width: '100%' }}> <Space direction="vertical" size="small" style={{ width: '100%' }}>
<strong style={{ fontSize: 14 }}>{org.name}</strong> <strong style={{ fontSize: 14 }}>{org.name}</strong>
<Tag color="blue">{org.type}</Tag> <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} : {org.member_count} | : {org.power_level}
</div> </div>
</Space> </Space>
@@ -415,7 +416,7 @@ export default function Organizations() {
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}> <div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
{!selectedOrg ? ( {!selectedOrg ? (
<Card style={{ height: '100%' }}> <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 && ( {isMobile && organizations.length > 0 && (
<Button <Button
type="primary" type="primary"
+142 -131
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useMemo } from 'react'; 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 { EditOutlined, DeleteOutlined, ThunderboltOutlined, BranchesOutlined, AppstoreAddOutlined, CheckCircleOutlined, ExclamationCircleOutlined, PlusOutlined, FileTextOutlined } from '@ant-design/icons';
import { useStore } from '../store'; import { useStore } from '../store';
import { useOutlineSync } from '../store/hooks'; import { useOutlineSync } from '../store/hooks';
@@ -119,6 +119,9 @@ export default function Outline() {
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const [isExpanding, setIsExpanding] = useState(false); const [isExpanding, setIsExpanding] = useState(false);
const [projectCharacters, setProjectCharacters] = useState<Array<{ label: string; value: string }>>([]); 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>>({}); const [scenesExpandStatus, setScenesExpandStatus] = useState<Record<string, boolean>>({});
@@ -776,7 +779,7 @@ export default function Outline() {
console.log('已同步到Form,当前Form值:', generateForm.getFieldsValue()); 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}` : '未配置默认模型'} {defaultModel ? `当前默认模型: ${loadedModels.find(m => m.value === defaultModel)?.label || defaultModel}` : '未配置默认模型'}
</div> </div>
</Form.Item> </Form.Item>
@@ -853,19 +856,19 @@ export default function Outline() {
<p> <strong>{values.order_index}</strong> 使</p> <p> <strong>{values.order_index}</strong> 使</p>
<div style={{ <div style={{
padding: 12, padding: 12,
background: 'var(--color-warning-bg)', background: token.colorWarningBg,
borderRadius: 4, borderRadius: token.borderRadius,
border: '1px solid var(--color-warning-border)', border: `1px solid ${token.colorWarningBorder}`,
marginTop: 8 marginTop: 8
}}> }}>
<div style={{ fontWeight: 500, color: 'var(--color-warning)' }}> <div style={{ fontWeight: 500, color: token.colorWarning }}>
{currentProject?.outline_mode === 'one-to-one' {currentProject?.outline_mode === 'one-to-one'
? `${existingOutline.order_index}` ? `${existingOutline.order_index}`
: `${existingOutline.order_index}` : `${existingOutline.order_index}`
}{existingOutline.title} }{existingOutline.title}
</div> </div>
</div> </div>
<p style={{ marginTop: 12, color: 'var(--color-text-secondary)' }}> <p style={{ marginTop: 12, color: token.colorTextSecondary }}>
💡 使 <strong>{nextOrderIndex}</strong>使 💡 使 <strong>{nextOrderIndex}</strong>使
</p> </p>
</div> </div>
@@ -928,18 +931,18 @@ export default function Outline() {
</p> </p>
<div style={{ <div style={{
padding: 12, padding: 12,
background: 'var(--color-warning-bg)', background: token.colorWarningBg,
borderRadius: 4, borderRadius: token.borderRadius,
border: '1px solid var(--color-warning-border)' 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>
<div style={{ color: 'var(--color-text-secondary)' }}> <div style={{ color: token.colorTextSecondary }}>
{prevOutline.order_index}{prevOutline.title} {prevOutline.order_index}{prevOutline.title}
</div> </div>
</div> </div>
<p style={{ marginTop: 12, color: 'var(--color-text-secondary)', fontSize: 13 }}> <p style={{ marginTop: 12, color: token.colorTextSecondary, fontSize: 13 }}>
💡 使 💡 使
</p> </p>
</div> </div>
@@ -978,9 +981,9 @@ export default function Outline() {
centered: true, centered: true,
content: ( content: (
<div> <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={{ fontWeight: 500, marginBottom: 4 }}></div>
<div style={{ color: 'var(--color-text-secondary)' }}>{outlineTitle}</div> <div style={{ color: token.colorTextSecondary }}>{outlineTitle}</div>
</div> </div>
<Form <Form
form={expansionForm} form={expansionForm}
@@ -1132,7 +1135,7 @@ export default function Outline() {
modalApi.info({ modalApi.info({
title: ( title: (
<Space style={{ flexWrap: 'wrap' }}> <Space style={{ flexWrap: 'wrap' }}>
<CheckCircleOutlined style={{ color: 'var(--color-success)' }} /> <CheckCircleOutlined style={{ color: token.colorSuccess }} />
<span>{outlineTitle}</span> <span>{outlineTitle}</span>
</Space> </Space>
), ),
@@ -1164,10 +1167,10 @@ export default function Outline() {
content: ( content: (
<div> <div>
<p>{outlineTitle} <strong>{data.chapter_count}</strong> </p> <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>
<p style={{ color: '#ff4d4f', marginTop: 8 }}> <p style={{ color: token.colorError, marginTop: 8 }}>
</p> </p>
</div> </div>
@@ -1324,7 +1327,7 @@ export default function Outline() {
key={sceneIdx} key={sceneIdx}
size="small" size="small"
style={{ style={{
backgroundColor: '#fafafa', backgroundColor: token.colorFillQuaternary,
maxWidth: '100%', maxWidth: '100%',
overflow: 'hidden' overflow: 'hidden'
}} }}
@@ -1374,7 +1377,7 @@ export default function Outline() {
modalApi.confirm({ modalApi.confirm({
title: ( title: (
<Space> <Space>
<CheckCircleOutlined style={{ color: 'var(--color-success)' }} /> <CheckCircleOutlined style={{ color: token.colorSuccess }} />
<span></span> <span></span>
</Space> </Space>
), ),
@@ -1438,7 +1441,7 @@ export default function Outline() {
<Card size="small" title="场景"> <Card size="small" title="场景">
<Space direction="vertical" size="small" style={{ width: '100%' }}> <Space direction="vertical" size="small" style={{ width: '100%' }}>
{plan.scenes.map((scene, sceneIdx) => ( {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.location}</div>
<div><strong></strong>{scene.characters.join('、')}</div> <div><strong></strong>{scene.characters.join('、')}</div>
<div><strong></strong>{scene.purpose}</div> <div><strong></strong>{scene.purpose}</div>
@@ -1511,8 +1514,16 @@ export default function Outline() {
centered: true, centered: true,
content: ( content: (
<div> <div>
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-warning-bg)', borderRadius: 4 }}> <div
<div style={{ color: '#856404' }}> style={{
marginBottom: 16,
padding: 12,
background: token.colorWarningBg,
borderRadius: token.borderRadius,
border: `1px solid ${token.colorWarningBorder}`,
}}
>
<div style={{ color: token.colorWarningText }}>
{outlines.length} {outlines.length}
</div> </div>
</div> </div>
@@ -1639,16 +1650,16 @@ export default function Outline() {
<div style={{ <div style={{
marginBottom: 16, marginBottom: 16,
padding: 12, padding: 12,
background: 'var(--color-warning-bg)', background: token.colorWarningBg,
borderRadius: 4, borderRadius: token.borderRadius,
border: '1px solid #ffe58f' 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>
<Space direction="vertical" size="small" style={{ width: '100%' }}> <Space direction="vertical" size="small" style={{ width: '100%' }}>
{batchPreviewData.skipped_outlines.map((skipped: SkippedOutlineInfo, idx: number) => ( {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> {skipped.outline_title} <Tag color="default" style={{ fontSize: 11 }}>{skipped.reason}</Tag>
</div> </div>
))} ))}
@@ -1661,11 +1672,11 @@ export default function Outline() {
{/* 左栏:大纲列表 */} {/* 左栏:大纲列表 */}
<div style={{ <div style={{
width: 280, width: 280,
borderRight: '1px solid #f0f0f0', borderRight: `1px solid ${token.colorBorderSecondary}`,
paddingRight: 12, paddingRight: 12,
overflowY: 'auto' overflowY: 'auto'
}}> }}>
<div style={{ fontWeight: 500, marginBottom: 8, color: '#666' }}></div> <div style={{ fontWeight: 500, marginBottom: 8, color: token.colorTextSecondary }}></div>
<List <List
size="small" size="small"
dataSource={batchPreviewData.expansion_results} dataSource={batchPreviewData.expansion_results}
@@ -1679,10 +1690,10 @@ export default function Outline() {
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
padding: '8px 12px', padding: '8px 12px',
background: selectedOutlineIdx === idx ? '#e6f7ff' : 'transparent', background: selectedOutlineIdx === idx ? token.colorPrimaryBg : 'transparent',
borderRadius: 4, borderRadius: token.borderRadius,
marginBottom: 4, 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%' }}> <div style={{ width: '100%' }}>
@@ -1702,11 +1713,11 @@ export default function Outline() {
{/* 中栏:章节列表 */} {/* 中栏:章节列表 */}
<div style={{ <div style={{
width: 320, width: 320,
borderRight: '1px solid #f0f0f0', borderRight: `1px solid ${token.colorBorderSecondary}`,
paddingRight: 12, paddingRight: 12,
overflowY: 'auto' 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} ) ({batchPreviewData.expansion_results[selectedOutlineIdx]?.actual_chapter_count || 0} )
</div> </div>
{batchPreviewData.expansion_results[selectedOutlineIdx] && ( {batchPreviewData.expansion_results[selectedOutlineIdx] && (
@@ -1720,10 +1731,10 @@ export default function Outline() {
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
padding: '8px 12px', padding: '8px 12px',
background: selectedChapterIdx === idx ? '#e6f7ff' : 'transparent', background: selectedChapterIdx === idx ? token.colorPrimaryBg : 'transparent',
borderRadius: 4, borderRadius: token.borderRadius,
marginBottom: 4, 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%' }}> <div style={{ width: '100%' }}>
@@ -1744,7 +1755,7 @@ export default function Outline() {
{/* 右栏:章节详情 */} {/* 右栏:章节详情 */}
<div style={{ flex: 1, overflowY: 'auto', paddingLeft: 12 }}> <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] ? ( {batchPreviewData.expansion_results[selectedOutlineIdx]?.chapter_plans[selectedChapterIdx] ? (
<Space direction="vertical" size="middle" style={{ width: '100%' }}> <Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card size="small" title="情节概要" bordered={false}> <Card size="small" title="情节概要" bordered={false}>
@@ -1775,7 +1786,7 @@ export default function Outline() {
<Card size="small" title="场景" bordered={false}> <Card size="small" title="场景" bordered={false}>
<Space direction="vertical" size="small" style={{ width: '100%' }}> <Space direction="vertical" size="small" style={{ width: '100%' }}>
{batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].scenes!.map((scene: SceneInfo, sceneIdx: number) => ( {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.location}</div>
<div><strong></strong>{scene.characters.join('、')}</div> <div><strong></strong>{scene.characters.join('、')}</div>
<div><strong></strong>{scene.purpose}</div> <div><strong></strong>{scene.purpose}</div>
@@ -1876,7 +1887,7 @@ export default function Outline() {
<Modal <Modal
title={ title={
<Space> <Space>
<CheckCircleOutlined style={{ color: 'var(--color-success)' }} /> <CheckCircleOutlined style={{ color: token.colorSuccess }} />
<span></span> <span></span>
</Space> </Space>
} }
@@ -1907,10 +1918,10 @@ export default function Outline() {
position: 'sticky', position: 'sticky',
top: 0, top: 0,
zIndex: 10, zIndex: 10,
backgroundColor: 'var(--color-bg-container)', backgroundColor: token.colorBgContainer,
padding: isMobile ? '12px 0' : '16px 0', padding: isMobile ? '12px 0' : '16px 0',
marginBottom: isMobile ? 12 : 16, marginBottom: isMobile ? 12 : 16,
borderBottom: '1px solid #f0f0f0', borderBottom: `1px solid ${token.colorBorderSecondary}`,
display: 'flex', display: 'flex',
flexDirection: isMobile ? 'column' : 'row', flexDirection: isMobile ? 'column' : 'row',
gap: isMobile ? 12 : 0, gap: isMobile ? 12 : 0,
@@ -1995,8 +2006,8 @@ export default function Outline() {
style={{ style={{
width: '100%', width: '100%',
borderRadius: isMobile ? 6 : 8, borderRadius: isMobile ? 6 : 8,
border: '1px solid #f0f0f0', border: `1px solid ${token.colorBorderSecondary}`,
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.03)', boxShadow: `0 1px 2px ${alphaColor(token.colorTextBase, 0.08)}`,
transition: 'all 0.3s ease' transition: 'all 0.3s ease'
}} }}
bodyStyle={{ bodyStyle={{
@@ -2004,14 +2015,14 @@ export default function Outline() {
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (!isMobile) { if (!isMobile) {
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.08)'; e.currentTarget.style.boxShadow = `0 4px 12px ${alphaColor(token.colorTextBase, 0.16)}`;
e.currentTarget.style.borderColor = 'var(--color-primary)'; e.currentTarget.style.borderColor = token.colorPrimary;
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
if (!isMobile) { if (!isMobile) {
e.currentTarget.style.boxShadow = '0 1px 2px rgba(0, 0, 0, 0.03)'; e.currentTarget.style.boxShadow = `0 1px 2px ${alphaColor(token.colorTextBase, 0.08)}`;
e.currentTarget.style.borderColor = '#f0f0f0'; e.currentTarget.style.borderColor = token.colorBorderSecondary;
} }
}} }}
> >
@@ -2019,7 +2030,7 @@ export default function Outline() {
style={{ width: '100%' }} style={{ width: '100%' }}
title={ title={
<Space size="small" style={{ fontSize: isMobile ? 13 : 16, flexWrap: 'wrap', lineHeight: isMobile ? '1.4' : '1.5' }}> <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' {currentProject?.outline_mode === 'one-to-one'
? `${item.order_index || '?'}` ? `${item.order_index || '?'}`
: `${item.order_index || '?'}` : `${item.order_index || '?'}`
@@ -2042,16 +2053,16 @@ export default function Outline() {
<div style={{ <div style={{
marginBottom: isMobile ? 10 : 12, marginBottom: isMobile ? 10 : 12,
padding: isMobile ? '8px 10px' : '10px 12px', padding: isMobile ? '8px 10px' : '10px 12px',
background: 'linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%)', background: token.colorFillQuaternary,
borderLeft: '3px solid #8c8c8c', borderLeft: `3px solid ${token.colorBorderSecondary}`,
borderRadius: isMobile ? 4 : 6, borderRadius: token.borderRadius,
fontSize: isMobile ? 12 : 13, fontSize: isMobile ? 12 : 13,
color: '#262626', color: token.colorText,
lineHeight: '1.6' lineHeight: '1.6'
}}> }}>
<div style={{ <div style={{
fontWeight: 600, fontWeight: 600,
color: '#595959', color: token.colorTextSecondary,
marginBottom: isMobile ? 4 : 6, marginBottom: isMobile ? 4 : 6,
fontSize: isMobile ? 12 : 13 fontSize: isMobile ? 12 : 13
}}> }}>
@@ -2059,11 +2070,11 @@ export default function Outline() {
</div> </div>
<div style={{ <div style={{
padding: isMobile ? '6px 8px' : '6px 10px', padding: isMobile ? '6px 8px' : '6px 10px',
background: '#ffffff', background: token.colorBgContainer,
border: '1px solid #d9d9d9', border: `1px solid ${token.colorBorder}`,
borderRadius: 4, borderRadius: token.borderRadiusSM,
fontSize: isMobile ? 12 : 13, fontSize: isMobile ? 12 : 13,
color: '#262626', color: token.colorText,
lineHeight: '1.6' lineHeight: '1.6'
}}> }}>
{item.content} {item.content}
@@ -2075,9 +2086,9 @@ export default function Outline() {
<div style={{ <div style={{
marginTop: isMobile ? 10 : 12, marginTop: isMobile ? 10 : 12,
padding: isMobile ? '8px 10px' : '10px 12px', padding: isMobile ? '8px 10px' : '10px 12px',
background: 'linear-gradient(135deg, #f5f3ff 0%, #faf5ff 100%)', background: token.colorPrimaryBg,
borderLeft: '3px solid #9333ea', borderLeft: `3px solid ${token.colorPrimary}`,
borderRadius: isMobile ? 4 : 6 borderRadius: token.borderRadius
}}> }}>
<div style={{ <div style={{
display: 'flex', display: 'flex',
@@ -2088,7 +2099,7 @@ export default function Outline() {
<span style={{ <span style={{
fontSize: isMobile ? 12 : 13, fontSize: isMobile ? 12 : 13,
fontWeight: 600, fontWeight: 600,
color: '#7c3aed', color: token.colorPrimary,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 4 gap: 4
@@ -2118,9 +2129,9 @@ export default function Outline() {
padding: isMobile ? '2px 8px' : '3px 10px', padding: isMobile ? '2px 8px' : '3px 10px',
fontSize: isMobile ? 11 : 12, fontSize: isMobile ? 11 : 12,
fontWeight: 500, fontWeight: 500,
border: '1px solid #e9d5ff', border: `1px solid ${token.colorPrimaryBorder}`,
background: '#ffffff', background: token.colorBgContainer,
color: '#7c3aed', color: token.colorPrimary,
whiteSpace: 'normal', whiteSpace: 'normal',
wordBreak: 'break-word', wordBreak: 'break-word',
height: 'auto', height: 'auto',
@@ -2139,9 +2150,9 @@ export default function Outline() {
<div style={{ <div style={{
marginTop: isMobile ? 10 : 12, marginTop: isMobile ? 10 : 12,
padding: isMobile ? '8px 10px' : '10px 12px', padding: isMobile ? '8px 10px' : '10px 12px',
background: 'linear-gradient(135deg, #fff7ed 0%, #fffbeb 100%)', background: token.colorWarningBg,
borderLeft: '3px solid #ea580c', borderLeft: `3px solid ${token.colorWarning}`,
borderRadius: isMobile ? 4 : 6 borderRadius: token.borderRadius
}}> }}>
<div style={{ <div style={{
display: 'flex', display: 'flex',
@@ -2152,7 +2163,7 @@ export default function Outline() {
<span style={{ <span style={{
fontSize: isMobile ? 12 : 13, fontSize: isMobile ? 12 : 13,
fontWeight: 600, fontWeight: 600,
color: '#ea580c', color: token.colorWarning,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 4 gap: 4
@@ -2182,9 +2193,9 @@ export default function Outline() {
padding: isMobile ? '2px 8px' : '3px 10px', padding: isMobile ? '2px 8px' : '3px 10px',
fontSize: isMobile ? 11 : 12, fontSize: isMobile ? 11 : 12,
fontWeight: 500, fontWeight: 500,
border: '1px solid #fed7aa', border: `1px solid ${token.colorWarningBorder}`,
background: '#ffffff', background: token.colorBgContainer,
color: '#ea580c', color: token.colorWarning,
whiteSpace: 'normal', whiteSpace: 'normal',
wordBreak: 'break-word', wordBreak: 'break-word',
height: 'auto', height: 'auto',
@@ -2209,9 +2220,9 @@ export default function Outline() {
<div style={{ <div style={{
marginTop: isMobile ? 10 : 12, marginTop: isMobile ? 10 : 12,
padding: isMobile ? '8px 10px' : '10px 12px', padding: isMobile ? '8px 10px' : '10px 12px',
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)', background: token.colorInfoBg,
borderLeft: '3px solid #0ea5e9', borderLeft: `3px solid ${token.colorInfo}`,
borderRadius: isMobile ? 4 : 6 borderRadius: token.borderRadius
}}> }}>
<div style={{ <div style={{
display: 'flex', display: 'flex',
@@ -2224,7 +2235,7 @@ export default function Outline() {
<span style={{ <span style={{
fontSize: isMobile ? 12 : 13, fontSize: isMobile ? 12 : 13,
fontWeight: 600, fontWeight: 600,
color: '#0284c7', color: token.colorInfo,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 4 gap: 4
@@ -2254,7 +2265,7 @@ export default function Outline() {
fontSize: isMobile ? 10 : 11, fontSize: isMobile ? 10 : 11,
height: isMobile ? 20 : 22, height: isMobile ? 20 : 22,
padding: isMobile ? '0 6px' : '0 8px', padding: isMobile ? '0 6px' : '0 8px',
color: '#0284c7' color: token.colorInfo
}} }}
> >
{isExpanded ? '收起 ▲' : `展开 (${structureData.scenes!.length - maxVisibleScenes}+) ▼`} {isExpanded ? '收起 ▲' : `展开 (${structureData.scenes!.length - maxVisibleScenes}+) ▼`}
@@ -2278,11 +2289,11 @@ export default function Outline() {
key={idx} key={idx}
style={{ style={{
padding: isMobile ? '6px 8px' : '8px 10px', padding: isMobile ? '6px 8px' : '8px 10px',
background: '#ffffff', background: token.colorBgContainer,
border: '1px solid #bae6fd', border: `1px solid ${token.colorInfoBorder}`,
borderRadius: isMobile ? 4 : 6, borderRadius: token.borderRadius,
fontSize: isMobile ? 11 : 12, fontSize: isMobile ? 11 : 12,
color: '#0c4a6e', color: token.colorText,
display: 'flex', display: 'flex',
alignItems: 'flex-start', alignItems: 'flex-start',
gap: isMobile ? 6 : 8, gap: isMobile ? 6 : 8,
@@ -2294,13 +2305,13 @@ export default function Outline() {
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (!isMobile) { if (!isMobile) {
e.currentTarget.style.borderColor = '#0ea5e9'; e.currentTarget.style.borderColor = token.colorInfo;
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.15)'; e.currentTarget.style.boxShadow = `0 2px 8px ${alphaColor(token.colorInfo, 0.25)}`;
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
if (!isMobile) { if (!isMobile) {
e.currentTarget.style.borderColor = '#bae6fd'; e.currentTarget.style.borderColor = token.colorInfoBorder;
e.currentTarget.style.boxShadow = 'none'; e.currentTarget.style.boxShadow = 'none';
} }
}} }}
@@ -2332,9 +2343,9 @@ export default function Outline() {
key={idx} key={idx}
style={{ style={{
padding: isMobile ? '8px 10px' : '10px 12px', padding: isMobile ? '8px 10px' : '10px 12px',
background: '#ffffff', background: token.colorBgContainer,
border: '1px solid #bae6fd', border: `1px solid ${token.colorInfoBorder}`,
borderRadius: isMobile ? 4 : 6, borderRadius: token.borderRadius,
fontSize: isMobile ? 11 : 12, fontSize: isMobile ? 11 : 12,
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
cursor: 'default', cursor: 'default',
@@ -2344,13 +2355,13 @@ export default function Outline() {
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (!isMobile) { if (!isMobile) {
e.currentTarget.style.borderColor = '#0ea5e9'; e.currentTarget.style.borderColor = token.colorInfo;
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.15)'; e.currentTarget.style.boxShadow = `0 2px 8px ${alphaColor(token.colorInfo, 0.25)}`;
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
if (!isMobile) { if (!isMobile) {
e.currentTarget.style.borderColor = '#bae6fd'; e.currentTarget.style.borderColor = token.colorInfoBorder;
e.currentTarget.style.boxShadow = 'none'; e.currentTarget.style.boxShadow = 'none';
} }
}} }}
@@ -2374,7 +2385,7 @@ export default function Outline() {
</Tag> </Tag>
<span style={{ <span style={{
fontWeight: 600, fontWeight: 600,
color: '#0c4a6e', color: token.colorText,
fontSize: isMobile ? 12 : 13, fontSize: isMobile ? 12 : 13,
flex: 1, flex: 1,
overflow: 'hidden', overflow: 'hidden',
@@ -2387,7 +2398,7 @@ export default function Outline() {
{scene.characters && scene.characters.length > 0 && ( {scene.characters && scene.characters.length > 0 && (
<div style={{ <div style={{
fontSize: isMobile ? 10 : 11, fontSize: isMobile ? 10 : 11,
color: '#64748b', color: token.colorTextSecondary,
marginBottom: 4, marginBottom: 4,
paddingLeft: isMobile ? 2 : 4, paddingLeft: isMobile ? 2 : 4,
overflow: 'hidden', overflow: 'hidden',
@@ -2401,7 +2412,7 @@ export default function Outline() {
{scene.purpose && ( {scene.purpose && (
<div style={{ <div style={{
fontSize: isMobile ? 10 : 11, fontSize: isMobile ? 10 : 11,
color: '#64748b', color: token.colorTextSecondary,
paddingLeft: isMobile ? 2 : 4, paddingLeft: isMobile ? 2 : 4,
lineHeight: '1.5', lineHeight: '1.5',
overflow: 'hidden', overflow: 'hidden',
@@ -2426,9 +2437,9 @@ export default function Outline() {
<div style={{ <div style={{
marginTop: 12, marginTop: 12,
padding: '10px 12px', padding: '10px 12px',
background: 'linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%)', background: token.colorWarningBg,
borderLeft: '3px solid #f97316', borderLeft: `3px solid ${token.colorWarning}`,
borderRadius: 6 borderRadius: token.borderRadius
}}> }}>
<div style={{ <div style={{
display: 'flex', display: 'flex',
@@ -2439,7 +2450,7 @@ export default function Outline() {
<span style={{ <span style={{
fontSize: 13, fontSize: 13,
fontWeight: 600, fontWeight: 600,
color: '#ea580c', color: token.colorWarning,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 4 gap: 4
@@ -2464,11 +2475,11 @@ export default function Outline() {
key={idx} key={idx}
style={{ style={{
padding: '6px 10px', padding: '6px 10px',
background: '#ffffff', background: token.colorBgContainer,
border: '1px solid #fed7aa', border: `1px solid ${token.colorWarningBorder}`,
borderRadius: 4, borderRadius: token.borderRadiusSM,
fontSize: 12, fontSize: 12,
color: '#9a3412', color: token.colorWarningText,
display: 'flex', display: 'flex',
alignItems: 'flex-start', alignItems: 'flex-start',
gap: 8 gap: 8
@@ -2503,9 +2514,9 @@ export default function Outline() {
<div style={{ <div style={{
marginTop: 12, marginTop: 12,
padding: '10px 12px', padding: '10px 12px',
background: 'linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%)', background: token.colorSuccessBg,
borderLeft: '3px solid #22c55e', borderLeft: `3px solid ${token.colorSuccess}`,
borderRadius: 6 borderRadius: token.borderRadius
}}> }}>
<div style={{ <div style={{
display: 'flex', display: 'flex',
@@ -2516,7 +2527,7 @@ export default function Outline() {
<span style={{ <span style={{
fontSize: 13, fontSize: 13,
fontWeight: 600, fontWeight: 600,
color: '#15803d', color: token.colorSuccess,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 4 gap: 4
@@ -2548,11 +2559,11 @@ export default function Outline() {
key={idx} key={idx}
style={{ style={{
padding: isMobile ? '6px 8px' : '8px 10px', padding: isMobile ? '6px 8px' : '8px 10px',
background: '#ffffff', background: token.colorBgContainer,
border: '1px solid #bbf7d0', border: `1px solid ${token.colorSuccessBorder}`,
borderRadius: isMobile ? 4 : 6, borderRadius: token.borderRadius,
fontSize: isMobile ? 11 : 12, fontSize: isMobile ? 11 : 12,
color: '#166534', color: token.colorText,
display: 'flex', display: 'flex',
alignItems: 'flex-start', alignItems: 'flex-start',
gap: isMobile ? 6 : 8, gap: isMobile ? 6 : 8,
@@ -2564,13 +2575,13 @@ export default function Outline() {
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (!isMobile) { if (!isMobile) {
e.currentTarget.style.borderColor = '#22c55e'; e.currentTarget.style.borderColor = token.colorSuccess;
e.currentTarget.style.boxShadow = '0 2px 8px rgba(34, 197, 94, 0.15)'; e.currentTarget.style.boxShadow = `0 2px 8px ${alphaColor(token.colorSuccess, 0.25)}`;
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
if (!isMobile) { if (!isMobile) {
e.currentTarget.style.borderColor = '#bbf7d0'; e.currentTarget.style.borderColor = token.colorSuccessBorder;
e.currentTarget.style.boxShadow = 'none'; e.currentTarget.style.boxShadow = 'none';
} }
}} }}
@@ -2604,9 +2615,9 @@ export default function Outline() {
<div style={{ <div style={{
marginTop: 12, marginTop: 12,
padding: '10px 12px', padding: '10px 12px',
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)', background: token.colorWarningBg,
borderLeft: '3px solid #f59e0b', borderLeft: `3px solid ${token.colorWarning}`,
borderRadius: 6, borderRadius: token.borderRadius,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 8 gap: 8
@@ -2614,7 +2625,7 @@ export default function Outline() {
<span style={{ <span style={{
fontSize: 13, fontSize: 13,
fontWeight: 600, fontWeight: 600,
color: '#b45309' color: token.colorWarning
}}> }}>
💫 💫
</span> </span>
@@ -2625,9 +2636,9 @@ export default function Outline() {
fontSize: 12, fontSize: 12,
padding: '2px 12px', padding: '2px 12px',
borderRadius: 12, borderRadius: 12,
background: '#ffffff', background: token.colorBgContainer,
border: '1px solid #fbbf24', border: `1px solid ${token.colorWarningBorder}`,
color: '#b45309' color: token.colorWarningText
}} }}
> >
{structureData.emotion} {structureData.emotion}
@@ -2640,26 +2651,26 @@ export default function Outline() {
<div style={{ <div style={{
marginTop: 12, marginTop: 12,
padding: '10px 12px', padding: '10px 12px',
background: 'linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)', background: token.colorInfoBg,
borderLeft: '3px solid #3b82f6', borderLeft: `3px solid ${token.colorInfo}`,
borderRadius: 6 borderRadius: token.borderRadius
}}> }}>
<div style={{ <div style={{
fontSize: 13, fontSize: 13,
fontWeight: 600, fontWeight: 600,
color: '#1e40af', color: token.colorInfo,
marginBottom: 6 marginBottom: 6
}}> }}>
🎯 🎯
</div> </div>
<div style={{ <div style={{
fontSize: 12, fontSize: 12,
color: '#1e3a8a', color: token.colorText,
lineHeight: '1.6', lineHeight: '1.6',
padding: '6px 10px', padding: '6px 10px',
background: '#ffffff', background: token.colorBgContainer,
border: '1px solid #93c5fd', border: `1px solid ${token.colorInfoBorder}`,
borderRadius: 4, borderRadius: token.borderRadiusSM,
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap' whiteSpace: 'nowrap'
@@ -2676,7 +2687,7 @@ export default function Outline() {
<div style={{ <div style={{
marginTop: 16, marginTop: 16,
paddingTop: 12, paddingTop: 12,
borderTop: '1px solid #f0f0f0', borderTop: `1px solid ${token.colorBorderSecondary}`,
display: 'flex', display: 'flex',
justifyContent: 'flex-end', justifyContent: 'flex-end',
gap: 8 gap: 8
@@ -2729,8 +2740,8 @@ export default function Outline() {
position: 'sticky', position: 'sticky',
bottom: 0, bottom: 0,
zIndex: 10, zIndex: 10,
backgroundColor: 'var(--color-bg-container)', backgroundColor: token.colorBgContainer,
borderTop: '1px solid #f0f0f0', borderTop: `1px solid ${token.colorBorderSecondary}`,
padding: isMobile ? '8px 0' : '10px 0', padding: isMobile ? '8px 0' : '10px 0',
display: 'flex', display: 'flex',
justifyContent: 'flex-end' 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 { useEffect, useMemo, useState } from 'react';
import { useParams, useNavigate, Outlet, Link, useLocation } from 'react-router-dom'; 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 { import {
ArrowLeftOutlined, ArrowLeftOutlined,
FileTextOutlined, FileTextOutlined,
@@ -18,10 +18,14 @@ import {
TrophyOutlined, TrophyOutlined,
BulbOutlined, BulbOutlined,
CloudOutlined, CloudOutlined,
MoonOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useStore } from '../store'; import { useStore } from '../store';
import { useCharacterSync, useOutlineSync, useChapterSync } from '../store/hooks'; import { useCharacterSync, useOutlineSync, useChapterSync } from '../store/hooks';
import { projectApi } from '../services/api'; 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; const { Header, Sider, Content } = Layout;
@@ -32,9 +36,17 @@ export default function ProjectDetail() {
const { projectId } = useParams<{ projectId: string }>(); const { projectId } = useParams<{ projectId: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState<boolean>(() => getStoredSidebarCollapsed());
const [drawerVisible, setDrawerVisible] = useState(false); const [drawerVisible, setDrawerVisible] = useState(false);
const [mobile, setMobile] = useState(isMobile()); 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(() => { useEffect(() => {
@@ -47,6 +59,10 @@ export default function ProjectDetail() {
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize);
}, []); }, []);
useEffect(() => {
setStoredSidebarCollapsed(collapsed);
}, [collapsed]);
const { const {
currentProject, currentProject,
setCurrentProject, setCurrentProject,
@@ -97,6 +113,81 @@ export default function ProjectDetail() {
// Hook 内部已经更新了 store,不需要再次刷新 // Hook 内部已经更新了 store,不需要再次刷新
const menuItems = [ 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', key: 'sponsor',
icon: <HeartOutlined />, icon: <HeartOutlined />,
@@ -157,11 +248,6 @@ export default function ProjectDetail() {
icon: <CloudOutlined />, icon: <CloudOutlined />,
label: <Link to={`/project/${projectId}/prompt-workshop`}></Link>, 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]} selectedKeys={[selectedKey]}
style={{ style={{
borderRight: 0, borderRight: 0,
paddingTop: '16px' paddingTop: '12px'
}} }}
items={menuItems} items={collapsed ? menuItemsCollapsed : menuItems}
onClick={() => mobile && setDrawerVisible(false)} onClick={() => mobile && setDrawerVisible(false)}
/> />
</div> </div>
@@ -215,54 +301,43 @@ export default function ProjectDetail() {
return ( return (
<Layout style={{ minHeight: '100vh', height: '100vh', overflow: 'hidden' }}> <Layout style={{ minHeight: '100vh', height: '100vh', overflow: 'hidden' }}>
<Header style={{ <Header style={{
background: 'var(--color-primary)', background: token.colorPrimary,
padding: mobile ? '0 12px' : '0 24px', padding: mobile ? '0 12px' : '0 24px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
position: 'fixed', position: 'fixed',
top: 0, top: 0,
left: 0, left: mobile ? 0 : (collapsed ? 60 : 220),
right: 0, right: 0,
zIndex: 1000, zIndex: 1000,
boxShadow: 'var(--shadow-header)', boxShadow: `0 2px 10px ${alphaColor(token.colorText, 0.16)}`,
height: mobile ? 56 : 70 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 }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', zIndex: 1 }}>
<Button {mobile && (
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 && (
<Button <Button
type="text" type="text"
icon={<ArrowLeftOutlined />} icon={<MenuUnfoldOutlined />}
onClick={() => navigate('/')} onClick={() => setDrawerVisible(true)}
style={{ style={{
fontSize: '16px', fontSize: '18px',
color: '#fff', color: token.colorWhite,
height: '40px', width: '36px',
padding: '0 16px' height: '36px'
}} }}
> />
</Button>
)} )}
</div> </div>
<h2 style={{ <h2 style={{
margin: 0, margin: 0,
color: '#fff', color: token.colorWhite,
fontSize: mobile ? '16px' : '24px', fontSize: mobile ? '16px' : '24px',
fontWeight: 600, 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', position: mobile ? 'static' : 'absolute',
left: mobile ? 'auto' : '50%', left: mobile ? 'auto' : '50%',
transform: mobile ? 'none' : 'translateX(-50%)', transform: mobile ? 'none' : 'translateX(-50%)',
@@ -284,7 +359,7 @@ export default function ProjectDetail() {
onClick={() => navigate('/')} onClick={() => navigate('/')}
style={{ style={{
fontSize: '14px', fontSize: '14px',
color: '#fff', color: token.colorWhite,
height: '36px', height: '36px',
padding: '0 8px', padding: '0 8px',
zIndex: 1 zIndex: 1
@@ -315,23 +390,23 @@ export default function ProjectDetail() {
minWidth: '56px', minWidth: '56px',
height: '56px', height: '56px',
padding: '0 12px', 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', cursor: 'default',
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-3px) scale(1.02)'; 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.boxShadow = `inset 0 0 20px ${alphaColor(token.colorWhite, 0.25)}, 0 8px 16px ${alphaColor(token.colorText, 0.15)}`;
e.currentTarget.style.border = '1px solid rgba(255, 255, 255, 0.1)'; e.currentTarget.style.border = `1px solid ${alphaColor(token.colorWhite, 0.1)}`;
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0) scale(1)'; 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={{ <span style={{
fontSize: '11px', fontSize: '11px',
color: 'rgba(255, 255, 255, 0.9)', color: alphaColor(token.colorWhite, 0.9),
marginBottom: '2px', marginBottom: '2px',
lineHeight: 1 lineHeight: 1
}}> }}>
@@ -340,7 +415,7 @@ export default function ProjectDetail() {
<span style={{ <span style={{
fontSize: '15px', fontSize: '15px',
fontWeight: '600', fontWeight: '600',
color: '#fff', color: token.colorWhite,
lineHeight: 1, lineHeight: 1,
fontFamily: 'Monaco, monospace' fontFamily: 'Monaco, monospace'
}}> }}>
@@ -357,7 +432,24 @@ export default function ProjectDetail() {
<Layout style={{ marginTop: mobile ? 56 : 70 }}> <Layout style={{ marginTop: mobile ? 56 : 70 }}>
{mobile ? ( {mobile ? (
<Drawer <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" placement="left"
onClose={() => setDrawerVisible(false)} onClose={() => setDrawerVisible(false)}
open={drawerVisible} open={drawerVisible}
@@ -365,6 +457,13 @@ export default function ProjectDetail() {
styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column' } }} styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column' } }}
> >
{renderMenu()} {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> </Drawer>
) : ( ) : (
<Sider <Sider
@@ -374,15 +473,18 @@ export default function ProjectDetail() {
trigger={null} trigger={null}
width={220} width={220}
collapsedWidth={60} collapsedWidth={60}
className="modern-sider"
style={{ style={{
position: 'fixed', position: 'fixed',
left: 0, left: 0,
top: 70, top: 0,
bottom: 0, bottom: 0,
overflow: 'hidden', overflow: 'hidden',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', 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={{ <div style={{
@@ -390,18 +492,148 @@ export default function ProjectDetail() {
display: 'flex', display: 'flex',
flexDirection: 'column' 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()} {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> </div>
</Sider> </Sider>
)} )}
<Layout style={{ <Layout style={{
marginLeft: mobile ? 0 : (collapsed ? 60 : 220), marginLeft: mobile ? 0 : (collapsed ? 60 : 220),
transition: 'all 0.2s' transition: 'margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
}}> }}>
<Content <Content
style={{ style={{
background: 'var(--color-bg-base)', background: token.colorBgLayout,
padding: mobile ? 12 : 24, padding: mobile ? 12 : 24,
height: mobile ? 'calc(100vh - 56px)' : 'calc(100vh - 70px)', height: mobile ? 'calc(100vh - 56px)' : 'calc(100vh - 70px)',
overflow: 'hidden', overflow: 'hidden',
@@ -410,10 +642,10 @@ export default function ProjectDetail() {
}} }}
> >
<div style={{ <div style={{
background: 'var(--color-bg-container)', background: token.colorBgContainer,
padding: mobile ? 12 : 24, padding: mobile ? 12 : 24,
borderRadius: mobile ? '8px' : '12px', borderRadius: mobile ? '8px' : '12px',
boxShadow: 'var(--shadow-card)', boxShadow: `0 8px 24px ${alphaColor(token.colorText, 0.08)}`,
height: '100%', height: '100%',
overflow: 'hidden', overflow: 'hidden',
display: 'flex', display: 'flex',
+18 -17
View File
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { import {
Form, Input, InputNumber, Select, Button, Card, Form, Input, InputNumber, Select, Button, Card,
Row, Col, Typography, Space, message, Radio Row, Col, Typography, Space, message, Radio, theme
} from 'antd'; } from 'antd';
import { import {
RocketOutlined, ArrowLeftOutlined, CheckCircleOutlined RocketOutlined, ArrowLeftOutlined, CheckCircleOutlined
@@ -18,6 +18,7 @@ export default function ProjectWizardNew() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const { token } = theme.useToken();
// 状态管理 // 状态管理
const [currentStep, setCurrentStep] = useState<'form' | 'generating'>('form'); const [currentStep, setCurrentStep] = useState<'form' | 'generating'>('form');
@@ -196,7 +197,7 @@ export default function ProjectWizardNew() {
<Card <Card
hoverable hoverable
style={{ 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, borderWidth: 2,
height: '100%', height: '100%',
}} }}
@@ -205,13 +206,13 @@ export default function ProjectWizardNew() {
<Radio value="one-to-one" style={{ width: '100%' }}> <Radio value="one-to-one" style={{ width: '100%' }}>
<Space direction="vertical" size={4} style={{ width: '100%' }}> <Space direction="vertical" size={4} style={{ width: '100%' }}>
<div style={{ fontSize: 16, fontWeight: 'bold' }}> <div style={{ fontSize: 16, fontWeight: 'bold' }}>
<CheckCircleOutlined style={{ marginRight: 8, color: 'var(--color-success)' }} /> <CheckCircleOutlined style={{ marginRight: 8, color: token.colorSuccess }} />
(11) (11)
</div> </div>
<div style={{ fontSize: 12, color: '#666' }}> <div style={{ fontSize: 12, color: token.colorTextSecondary }}>
</div> </div>
<div style={{ fontSize: 11, color: '#999' }}> <div style={{ fontSize: 11, color: token.colorTextTertiary }}>
💡 💡
</div> </div>
</Space> </Space>
@@ -223,7 +224,7 @@ export default function ProjectWizardNew() {
<Card <Card
hoverable hoverable
style={{ 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, borderWidth: 2,
height: '100%', height: '100%',
}} }}
@@ -232,13 +233,13 @@ export default function ProjectWizardNew() {
<Radio value="one-to-many" style={{ width: '100%' }}> <Radio value="one-to-many" style={{ width: '100%' }}>
<Space direction="vertical" size={4} style={{ width: '100%' }}> <Space direction="vertical" size={4} style={{ width: '100%' }}>
<div style={{ fontSize: 16, fontWeight: 'bold' }}> <div style={{ fontSize: 16, fontWeight: 'bold' }}>
<CheckCircleOutlined style={{ marginRight: 8, color: 'var(--color-success)' }} /> <CheckCircleOutlined style={{ marginRight: 8, color: token.colorSuccess }} />
(1N) (1N)
</div> </div>
<div style={{ fontSize: 12, color: '#666' }}> <div style={{ fontSize: 12, color: token.colorTextSecondary }}>
</div> </div>
<div style={{ fontSize: 11, color: '#999' }}> <div style={{ fontSize: 11, color: token.colorTextTertiary }}>
💡 💡
</div> </div>
</Space> </Space>
@@ -322,15 +323,15 @@ export default function ProjectWizardNew() {
return ( return (
<div style={{ <div style={{
minHeight: '100dvh', minHeight: '100dvh',
background: 'var(--color-bg-base)', background: token.colorBgBase,
}}> }}>
{/* 顶部标题栏 - 固定不滚动 */} {/* 顶部标题栏 - 固定不滚动 */}
<div style={{ <div style={{
position: 'sticky', position: 'sticky',
top: 0, top: 0,
zIndex: 100, zIndex: 100,
background: 'var(--color-primary)', background: token.colorPrimary,
boxShadow: 'var(--shadow-header)', boxShadow: `0 6px 20px color-mix(in srgb, ${token.colorPrimary} 30%, transparent)`,
}}> }}>
<div style={{ <div style={{
maxWidth: 1200, maxWidth: 1200,
@@ -346,9 +347,9 @@ export default function ProjectWizardNew() {
size={isMobile ? 'middle' : 'large'} size={isMobile ? 'middle' : 'large'}
disabled={currentStep === 'generating'} disabled={currentStep === 'generating'}
style={{ style={{
background: 'rgba(255,255,255,0.2)', background: `color-mix(in srgb, ${token.colorWhite} 20%, transparent)`,
borderColor: 'rgba(255,255,255,0.3)', borderColor: `color-mix(in srgb, ${token.colorWhite} 30%, transparent)`,
color: '#fff', color: token.colorWhite,
}} }}
> >
{isMobile ? '返回' : '返回首页'} {isMobile ? '返回' : '返回首页'}
@@ -356,8 +357,8 @@ export default function ProjectWizardNew() {
<Title level={isMobile ? 4 : 2} style={{ <Title level={isMobile ? 4 : 2} style={{
margin: 0, margin: 0,
color: '#fff', color: token.colorWhite,
textShadow: '0 2px 4px rgba(0,0,0,0.1)', textShadow: '0 2px 4px color-mix(in srgb, var(--ant-color-black) 18%, transparent)',
}}> }}>
<RocketOutlined style={{ marginRight: 8 }} /> <RocketOutlined style={{ marginRight: 8 }} />
+40 -34
View File
@@ -15,7 +15,8 @@ import {
Alert, Alert,
Upload, Upload,
Spin, Spin,
Empty Empty,
theme
} from 'antd'; } from 'antd';
import { import {
EditOutlined, EditOutlined,
@@ -27,7 +28,7 @@ import {
InfoCircleOutlined InfoCircleOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import axios from 'axios'; import axios from 'axios';
import { cardStyles, cardHoverHandlers, gridConfig } from '../components/CardStyles'; import { promptTemplateCardStyles, promptTemplateCardHoverHandlers, promptTemplateGridConfig } from '../components/CardStyles';
const { TextArea } = Input; const { TextArea } = Input;
const { Title, Text, Paragraph } = Typography; const { Title, Text, Paragraph } = Typography;
@@ -54,6 +55,7 @@ interface CategoryGroup {
} }
export default function PromptTemplates() { export default function PromptTemplates() {
const { token } = theme.useToken();
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
const [categories, setCategories] = useState<CategoryGroup[]>([]); const [categories, setCategories] = useState<CategoryGroup[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string>('0'); const [selectedCategory, setSelectedCategory] = useState<string>('0');
@@ -246,13 +248,15 @@ export default function PromptTemplates() {
}; };
const currentTemplates = getCurrentTemplates(); 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 ( return (
<> <>
{contextHolder} {contextHolder}
<div style={{ <div style={{
minHeight: '90vh', minHeight: '90vh',
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)', background: pageBackground,
padding: isMobile ? '20px 16px 70px' : '24px 24px 70px', padding: isMobile ? '20px 16px 70px' : '24px 24px 70px',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -269,9 +273,9 @@ export default function PromptTemplates() {
<Card <Card
variant="borderless" variant="borderless"
style={{ style={{
background: 'linear-gradient(135deg, var(--color-primary) 0%, #5A9BA5 50%, var(--color-primary-hover) 100%)', background: headerBackground,
borderRadius: isMobile ? 16 : 24, 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, marginBottom: isMobile ? 20 : 24,
border: 'none', border: 'none',
position: 'relative', 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', 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: 'rgba(255, 255, 255, 0.05)', 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: 'rgba(255, 255, 255, 0.06)', 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 }}> <Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
<Col xs={24} sm={12} md={14}> <Col xs={24} sm={12} md={14}>
<Space direction="vertical" size={4}> <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}` }}>
<FileSearchOutlined style={{ color: 'rgba(255,255,255,0.9)', marginRight: 8 }} /> <FileSearchOutlined style={{ color: token.colorWhite, opacity: 0.9, marginRight: 8 }} />
</Title> </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生成提示词 AI生成提示词
</Text> </Text>
</Space> </Space>
@@ -303,10 +307,11 @@ export default function PromptTemplates() {
size={isMobile ? 'small' : 'middle'} size={isMobile ? 'small' : 'middle'}
style={{ style={{
borderRadius: 12, borderRadius: 12,
background: 'rgba(255, 255, 255, 0.15)', background: token.colorWhite,
border: '1px solid rgba(255, 255, 255, 0.3)', border: `1px solid ${token.colorWhite}`,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', boxShadow: token.boxShadow,
color: '#fff', color: token.colorPrimary,
fontWeight: 600,
backdropFilter: 'blur(10px)', backdropFilter: 'blur(10px)',
transition: 'all 0.3s ease' transition: 'all 0.3s ease'
}} }}
@@ -323,10 +328,11 @@ export default function PromptTemplates() {
size={isMobile ? 'small' : 'middle'} size={isMobile ? 'small' : 'middle'}
style={{ style={{
borderRadius: 12, borderRadius: 12,
background: 'rgba(255, 255, 255, 0.15)', background: token.colorWhite,
border: '1px solid rgba(255, 255, 255, 0.3)', border: `1px solid ${token.colorWhite}`,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', boxShadow: token.boxShadow,
color: '#fff', color: token.colorPrimary,
fontWeight: 600,
backdropFilter: 'blur(10px)', backdropFilter: 'blur(10px)',
}} }}
> >
@@ -341,7 +347,7 @@ export default function PromptTemplates() {
<Alert <Alert
message={ message={
<Space align="center"> <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> <Text strong style={{ fontSize: isMobile ? 13 : 14 }}>使</Text>
</Space> </Space>
} }
@@ -360,8 +366,8 @@ export default function PromptTemplates() {
style={{ style={{
marginTop: isMobile ? 16 : 24, marginTop: isMobile ? 16 : 24,
borderRadius: 12, borderRadius: 12,
background: 'var(--color-info-bg)', background: token.colorInfoBg,
border: '1px solid var(--color-info-border)' border: `1px solid ${token.colorInfoBorder}`
}} }}
/> />
</Card> </Card>
@@ -374,9 +380,9 @@ export default function PromptTemplates() {
<Card <Card
variant="borderless" variant="borderless"
style={{ style={{
background: 'rgba(255, 255, 255, 0.95)', background: token.colorBgContainer,
borderRadius: isMobile ? 12 : 16, borderRadius: isMobile ? 12 : 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)', boxShadow: token.boxShadowSecondary,
marginBottom: isMobile ? 16 : 24 marginBottom: isMobile ? 16 : 24
}} }}
styles={{ body: { padding: isMobile ? '12px' : '16px' } }} styles={{ body: { padding: isMobile ? '12px' : '16px' } }}
@@ -400,9 +406,9 @@ export default function PromptTemplates() {
<Card <Card
variant="borderless" variant="borderless"
style={{ style={{
background: 'rgba(255, 255, 255, 0.95)', background: token.colorBgContainer,
borderRadius: isMobile ? 12 : 16, borderRadius: isMobile ? 12 : 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)', boxShadow: token.boxShadowSecondary,
}} }}
> >
<Empty <Empty
@@ -413,25 +419,25 @@ export default function PromptTemplates() {
) : ( ) : (
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
{currentTemplates.map(template => ( {currentTemplates.map(template => (
<Col {...gridConfig} key={template.id}> <Col {...promptTemplateGridConfig} key={template.id}>
<Card <Card
hoverable hoverable
variant="borderless" variant="borderless"
style={cardStyles.project} style={promptTemplateCardStyles.templateCard}
styles={{ body: { padding: 0, overflow: 'hidden' } }} styles={{ body: { padding: 0, overflow: 'hidden' } }}
{...cardHoverHandlers} {...promptTemplateCardHoverHandlers}
> >
{/* 头部 */} {/* 头部 */}
<div style={{ <div style={{
background: template.is_system_default background: template.is_system_default
? 'var(--color-bg-layout)' ? token.colorFillTertiary
: 'var(--color-primary)', : token.colorPrimary,
padding: isMobile ? '16px' : '20px', padding: isMobile ? '16px' : '20px',
position: 'relative' position: 'relative'
}}> }}>
<Space direction="vertical" size={8} style={{ width: '100%' }}> <Space direction="vertical" size={8} style={{ width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <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} {template.template_name}
</Title> </Title>
{!template.is_system_default && ( {!template.is_system_default && (
@@ -444,10 +450,10 @@ export default function PromptTemplates() {
)} )}
</div> </div>
<Space wrap> <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} {template.category}
</Tag> </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 ? '系统默认' : '已自定义'} {template.is_system_default ? '系统默认' : '已自定义'}
</Tag> </Tag>
</Space> </Space>
+17 -15
View File
@@ -20,6 +20,7 @@ import {
Pagination, Pagination,
Alert, Alert,
Statistic, Statistic,
theme,
} from 'antd'; } from 'antd';
import { import {
SearchOutlined, SearchOutlined,
@@ -120,6 +121,7 @@ export default function PromptWorkshop() {
const [activeTab, setActiveTab] = useState<string>('browse'); const [activeTab, setActiveTab] = useState<string>('browse');
const isMobile = window.innerWidth <= 768; const isMobile = window.innerWidth <= 768;
const { token } = theme.useToken();
// 判断是否为服务端管理员 // 判断是否为服务端管理员
const isServerAdmin = serviceStatus?.mode === 'server' && currentUser?.is_admin; const isServerAdmin = serviceStatus?.mode === 'server' && currentUser?.is_admin;
@@ -431,7 +433,7 @@ export default function PromptWorkshop() {
borderRadius: 12, borderRadius: 12,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
border: '1px solid #f0f0f0', border: `1px solid ${token.colorBorderSecondary}`,
}} }}
bodyStyle={{ bodyStyle={{
padding: 16, padding: 16,
@@ -446,7 +448,7 @@ export default function PromptWorkshop() {
<Tooltip title={item.is_liked ? '取消点赞' : '点赞'} key="like"> <Tooltip title={item.is_liked ? '取消点赞' : '点赞'} key="like">
<span onClick={() => handleLike(item)}> <span onClick={() => handleLike(item)}>
{item.is_liked ? ( {item.is_liked ? (
<HeartFilled style={{ color: '#ff4d4f' }} /> <HeartFilled style={{ color: token.colorError }} />
) : ( ) : (
<HeartOutlined /> <HeartOutlined />
)} )}
@@ -489,7 +491,7 @@ export default function PromptWorkshop() {
style={{ style={{
fontSize: 12, fontSize: 12,
marginBottom: 0, marginBottom: 0,
backgroundColor: '#fafafa', backgroundColor: token.colorFillQuaternary,
padding: 8, padding: 8,
borderRadius: 4, borderRadius: 4,
flex: 1, flex: 1,
@@ -512,7 +514,7 @@ export default function PromptWorkshop() {
)} )}
</div> </div>
<div style={{ marginTop: 8, color: '#999', fontSize: 12 }}> <div style={{ marginTop: 8, color: token.colorTextTertiary, fontSize: 12 }}>
<Space> <Space>
<span><UserOutlined /> {item.author_name || '匿名'}</span> <span><UserOutlined /> {item.author_name || '匿名'}</span>
</Space> </Space>
@@ -569,7 +571,7 @@ export default function PromptWorkshop() {
}} }}
> >
<Card <Card
style={{ borderRadius: 12, height: '100%', border: '1px solid #f0f0f0' }} style={{ borderRadius: 12, height: '100%', border: `1px solid ${token.colorBorderSecondary}` }}
bodyStyle={{ padding: 16 }} bodyStyle={{ padding: 16 }}
> >
<Space direction="vertical" style={{ width: '100%' }}> <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() : '-'} : {sub.created_at ? new Date(sub.created_at).toLocaleDateString() : '-'}
</div> </div>
@@ -802,7 +804,7 @@ export default function PromptWorkshop() {
</Col> </Col>
<Col span={4}> <Col span={4}>
<Card size="small"> <Card size="small">
<Statistic title="待审核" value={adminStats.total_pending} valueStyle={{ color: '#faad14' }} /> <Statistic title="待审核" value={adminStats.total_pending} valueStyle={{ color: token.colorWarning }} />
</Card> </Card>
</Col> </Col>
<Col span={4}> <Col span={4}>
@@ -853,13 +855,13 @@ export default function PromptWorkshop() {
}} }}
> >
<Card <Card
style={{ borderRadius: 12, border: '1px solid #f0f0f0' }} style={{ borderRadius: 12, border: `1px solid ${token.colorBorderSecondary}` }}
bodyStyle={{ padding: 16 }} bodyStyle={{ padding: 16 }}
actions={[ actions={[
<Button <Button
key="approve" key="approve"
type="link" type="link"
style={{ color: '#52c41a' }} style={{ color: token.colorSuccess }}
onClick={() => { onClick={() => {
setReviewingSubmission(sub); setReviewingSubmission(sub);
reviewForm.setFieldsValue({ reviewForm.setFieldsValue({
@@ -887,7 +889,7 @@ export default function PromptWorkshop() {
{sub.prompt_content} {sub.prompt_content}
</Paragraph> </Paragraph>
<div style={{ fontSize: 11, color: '#999' }}> <div style={{ fontSize: 11, color: token.colorTextTertiary }}>
<div>: {sub.submitter_name || '未知'}</div> <div>: {sub.submitter_name || '未知'}</div>
<div>: {sub.source_instance}</div> <div>: {sub.source_instance}</div>
<div>: {sub.created_at ? new Date(sub.created_at).toLocaleDateString() : '-'}</div> <div>: {sub.created_at ? new Date(sub.created_at).toLocaleDateString() : '-'}</div>
@@ -928,7 +930,7 @@ export default function PromptWorkshop() {
}} }}
> >
<Card <Card
style={{ borderRadius: 12, border: '1px solid #f0f0f0' }} style={{ borderRadius: 12, border: `1px solid ${token.colorBorderSecondary}` }}
bodyStyle={{ padding: 16 }} bodyStyle={{ padding: 16 }}
actions={[ actions={[
<Tooltip title="编辑" key="edit"> <Tooltip title="编辑" key="edit">
@@ -965,7 +967,7 @@ export default function PromptWorkshop() {
{item.prompt_content} {item.prompt_content}
</Paragraph> </Paragraph>
<div style={{ fontSize: 11, color: '#999' }}> <div style={{ fontSize: 11, color: token.colorTextTertiary }}>
<Space> <Space>
<span><HeartOutlined /> {item.like_count || 0}</span> <span><HeartOutlined /> {item.like_count || 0}</span>
<span><DownloadOutlined /> {item.download_count || 0}</span> <span><DownloadOutlined /> {item.download_count || 0}</span>
@@ -992,7 +994,7 @@ export default function PromptWorkshop() {
alignItems: 'center', alignItems: 'center',
padding: isMobile ? '12px 0' : '16px 0', padding: isMobile ? '12px 0' : '16px 0',
marginBottom: isMobile ? 12 : 16, marginBottom: isMobile ? 12 : 16,
borderBottom: '1px solid #f0f0f0', borderBottom: `1px solid ${token.colorBorderSecondary}`,
flexWrap: 'wrap', flexWrap: 'wrap',
gap: 12, gap: 12,
}}> }}>
@@ -1185,7 +1187,7 @@ export default function PromptWorkshop() {
)} )}
<div style={{ <div style={{
backgroundColor: '#f5f5f5', backgroundColor: token.colorFillSecondary,
padding: 16, padding: 16,
borderRadius: 8, borderRadius: 8,
marginBottom: 16, marginBottom: 16,
@@ -1236,7 +1238,7 @@ export default function PromptWorkshop() {
{reviewingSubmission && ( {reviewingSubmission && (
<div> <div>
<div style={{ <div style={{
backgroundColor: '#f5f5f5', backgroundColor: token.colorFillSecondary,
padding: 16, padding: 16,
borderRadius: 8, borderRadius: 8,
marginBottom: 16, marginBottom: 16,
+252 -115
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useMemo, type CSSProperties } from 'react'; import { useState, useEffect, useCallback, useMemo, type CSSProperties } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; 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 { import {
ArrowLeftOutlined, ArrowLeftOutlined,
ApartmentOutlined, ApartmentOutlined,
@@ -100,16 +100,18 @@ interface CharacterListResponse {
const GROUP_MAIN_CAREER_NODE_ID = '__career_group_main__'; const GROUP_MAIN_CAREER_NODE_ID = '__career_group_main__';
const GROUP_SUB_CAREER_NODE_ID = '__career_group_sub__'; const GROUP_SUB_CAREER_NODE_ID = '__career_group_sub__';
const EDGE_CATEGORY_META: Record<string, { label: string; color: string; order: number }> = { type EdgeColorPreset = 'primary' | 'warning' | 'info' | 'textTertiary' | 'error' | 'success';
organization: { label: '组织成员', color: '#722ed1', order: 1 },
career_main: { label: '主职业关联', color: '#faad14', order: 2 }, const EDGE_CATEGORY_META: Record<string, { label: string; colorPreset: EdgeColorPreset; order: number }> = {
career_sub: { label: '副职业关联', color: '#13c2c2', order: 3 }, organization: { label: '组织成员', colorPreset: 'primary', order: 1 },
career_group: { label: '职业分类', color: '#8c8c8c', order: 4 }, career_main: { label: '职业关联', colorPreset: 'warning', order: 2 },
family: { label: '亲属关系', color: '#f39c12', order: 5 }, career_sub: { label: '副职业关联', colorPreset: 'info', order: 3 },
hostile: { label: '敌对关系', color: '#e74c3c', order: 6 }, career_group: { label: '职业分类', colorPreset: 'textTertiary', order: 4 },
professional: { label: '职业关系', color: '#3498db', order: 7 }, family: { label: '亲属关系', colorPreset: 'warning', order: 5 },
social: { label: '社交关系', color: '#27ae60', order: 8 }, hostile: { label: '敌对关系', colorPreset: 'error', order: 6 },
default: { label: '其他关系', color: '#95a5a6', order: 99 }, professional: { label: '职业关系', colorPreset: 'info', order: 7 },
social: { label: '社交关系', colorPreset: 'success', order: 8 },
default: { label: '其他关系', colorPreset: 'textTertiary', order: 99 },
}; };
const getEdgeCategory = (edge: Edge) => const getEdgeCategory = (edge: Edge) =>
@@ -118,13 +120,36 @@ const getEdgeCategory = (edge: Edge) =>
const getEdgeCategoryMeta = (category: string) => const getEdgeCategoryMeta = (category: string) =>
EDGE_CATEGORY_META[category] || { EDGE_CATEGORY_META[category] || {
label: `${category}关系`, label: `${category}关系`,
color: '#95a5a6', colorPreset: 'textTertiary' as const,
order: 999, 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 => ({ const clampTextStyle = (rows: number): CSSProperties => ({
margin: '4px 0 0', margin: '4px 0 0',
color: '#555', color: 'var(--ant-color-text-secondary)',
fontSize: 14, fontSize: 14,
lineHeight: '22px', lineHeight: '22px',
display: '-webkit-box', display: '-webkit-box',
@@ -386,54 +411,65 @@ const getCategoryColor = (
relationshipName: string, relationshipName: string,
isActive: boolean, isActive: boolean,
relationshipTypes: RelationshipType[], relationshipTypes: RelationshipType[],
token: {
colorPrimary: string;
colorWarning: string;
colorInfo: string;
colorTextTertiary: string;
colorError: string;
colorSuccess: string;
colorBorder: string;
},
) => { ) => {
const inactiveColor = token.colorBorder;
if (relationshipName.startsWith('组织成员·')) { if (relationshipName.startsWith('组织成员·')) {
return isActive ? '#722ed1' : '#cdb7f6'; return isActive ? token.colorPrimary : inactiveColor;
} }
if (relationshipName.startsWith('主职业·')) { if (relationshipName.startsWith('主职业·')) {
return isActive ? '#faad14' : '#ffe7ba'; return isActive ? token.colorWarning : inactiveColor;
} }
if (relationshipName.startsWith('副职业·')) { if (relationshipName.startsWith('副职业·')) {
return isActive ? '#13c2c2' : '#b5f5ec'; return isActive ? token.colorInfo : inactiveColor;
} }
if (relationshipName.startsWith('职业分类·')) { if (relationshipName.startsWith('职业分类·')) {
return isActive ? '#8c8c8c' : '#d9d9d9'; return isActive ? token.colorTextTertiary : inactiveColor;
} }
const relType = relationshipTypes.find((rt) => rt.name === relationshipName); const relType = relationshipTypes.find((rt) => rt.name === relationshipName);
const category = relType?.category || 'default'; const category = relType?.category || 'default';
const categoryColors: Record<string, { active: string; inactive: string }> = { const categoryColors: Record<string, string> = {
family: { active: '#f39c12', inactive: '#fcd59e' }, family: token.colorWarning,
hostile: { active: '#e74c3c', inactive: '#f5a49a' }, hostile: token.colorError,
professional: { active: '#3498db', inactive: '#a9d4ed' }, professional: token.colorInfo,
social: { active: '#27ae60', inactive: '#a3d9b5' }, social: token.colorSuccess,
default: { active: '#95a5a6', inactive: '#c8d0d2' }, default: token.colorTextTertiary,
}; };
const colors = categoryColors[category] || categoryColors.default; const activeColor = categoryColors[category] || categoryColors.default;
return isActive ? colors.active : colors.inactive; return isActive ? activeColor : inactiveColor;
}; };
const getCharacterNodeStyle = (roleType: string): CSSProperties => { const getCharacterNodeStyle = (roleType: string): CSSProperties => {
const roleColorMap: Record<string, string> = { const roleColorMap: Record<string, string> = {
protagonist: '#e74c3c', protagonist: 'var(--ant-color-error)',
antagonist: '#9b59b6', antagonist: 'var(--ant-color-primary)',
supporting: '#3498db', supporting: 'var(--ant-color-info)',
}; };
const baseColor = roleColorMap[roleType] || '#3498db'; const baseColor = roleColorMap[roleType] || 'var(--ant-color-info)';
return { return {
width: 130, width: 130,
height: 130, height: 130,
border: `2px solid ${baseColor}`, border: `2px solid ${baseColor}`,
borderRadius: '50%', borderRadius: '50%',
background: `linear-gradient(135deg, #ffffff, ${baseColor}15)`, background: `linear-gradient(135deg, var(--ant-color-bg-container), color-mix(in srgb, ${baseColor} 12%, var(--ant-color-bg-container)))`,
boxShadow: `0 4px 16px ${baseColor}25`, boxShadow: `0 4px 16px color-mix(in srgb, ${baseColor} 25%, transparent)`,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -445,10 +481,10 @@ const getCharacterNodeStyle = (roleType: string): CSSProperties => {
const getOrganizationNodeStyle = (): CSSProperties => ({ const getOrganizationNodeStyle = (): CSSProperties => ({
width: 160, width: 160,
height: 90, height: 90,
border: '2px solid #27ae60', border: '2px solid var(--ant-color-success)',
borderRadius: 12, borderRadius: 12,
background: 'linear-gradient(135deg, #ffffff, #27ae6015)', 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 rgba(39, 174, 96, 0.15)', boxShadow: '0 4px 16px color-mix(in srgb, var(--ant-color-success) 15%, transparent)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -457,15 +493,15 @@ const getOrganizationNodeStyle = (): CSSProperties => ({
}); });
const getCareerNodeStyle = (type: 'main' | 'sub'): 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 { return {
width: 150, width: 150,
height: 72, height: 72,
border: `2px solid ${color}`, border: `2px solid ${color}`,
borderRadius: 12, borderRadius: 12,
background: `linear-gradient(135deg, #ffffff, ${color}15)`, 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}20`, boxShadow: `0 4px 12px color-mix(in srgb, ${color} 20%, transparent)`,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -475,15 +511,15 @@ const getCareerNodeStyle = (type: 'main' | 'sub'): CSSProperties => {
}; };
const getCareerGroupStyle = (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 { return {
width: 130, width: 130,
height: 52, height: 52,
border: `2px dashed ${color}`, border: `2px dashed ${color}`,
borderRadius: 26, borderRadius: 26,
backgroundColor: '#ffffff', backgroundColor: 'var(--ant-color-bg-container)',
boxShadow: `0 2px 8px ${color}15`, boxShadow: `0 2px 8px color-mix(in srgb, ${color} 15%, transparent)`,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -511,12 +547,12 @@ const InfoField = ({
marginBottom: 12, marginBottom: 12,
padding: '12px 14px', padding: '12px 14px',
borderRadius: 12, borderRadius: 12,
background: '#f8f9fa', background: 'var(--ant-color-fill-quaternary)',
border: '1px solid #eef0f2', border: '1px solid var(--ant-color-border-secondary)',
boxShadow: '0 2px 4px rgba(0,0,0,0.02)', 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} {label}
</Text> </Text>
<div style={clampTextStyle(rows)}>{value}</div> <div style={clampTextStyle(rows)}>{value}</div>
@@ -527,6 +563,9 @@ const InfoField = ({
export default function RelationshipGraph() { export default function RelationshipGraph() {
const { projectId } = useParams<{ projectId: string }>(); const { projectId } = useParams<{ projectId: string }>();
const navigate = useNavigate(); 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 [graphData, setGraphData] = useState<GraphData | null>(null);
const [, setLoading] = useState(false); const [, setLoading] = useState(false);
@@ -564,11 +603,13 @@ export default function RelationshipGraph() {
return { return {
category, category,
count, 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')); .sort((a, b) => a.order - b.order || a.label.localeCompare(b.label, 'zh-CN'));
}, [edges]); }, [edges, token]);
useEffect(() => { useEffect(() => {
if (edgeCategoryOptions.length === 0) { if (edgeCategoryOptions.length === 0) {
@@ -625,7 +666,15 @@ export default function RelationshipGraph() {
layoutWeight?: number; layoutWeight?: number;
}, },
): Edge => { ): 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 isOrgMemberLink = relationship.startsWith('组织成员·');
const isCareerMainLink = relationship.startsWith('主职业·'); const isCareerMainLink = relationship.startsWith('主职业·');
const isCareerSubLink = relationship.startsWith('副职业·'); const isCareerSubLink = relationship.startsWith('副职业·');
@@ -645,12 +694,12 @@ export default function RelationshipGraph() {
opacity: isCareerClassLink ? 0.5 : (isCareerMainLink || isCareerSubLink ? 0.6 : 1), opacity: isCareerClassLink ? 0.5 : (isCareerMainLink || isCareerSubLink ? 0.6 : 1),
}, },
labelStyle: { labelStyle: {
fill: '#666', fill: token.colorTextSecondary,
fontSize: 10, fontSize: 10,
fontWeight: isCareerMainLink || isCareerSubLink ? 600 : 500, fontWeight: isCareerMainLink || isCareerSubLink ? 600 : 500,
}, },
labelBgStyle: { labelBgStyle: {
fill: '#fff', fill: token.colorBgContainer,
fillOpacity: 0.9, fillOpacity: 0.9,
}, },
markerEnd: { 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 () => { const loadGraphData = useCallback(async () => {
@@ -705,28 +765,28 @@ export default function RelationshipGraph() {
const detail = detailMap[node.id]; const detail = detailMap[node.id];
const roleColorMap: Record<string, string> = { const roleColorMap: Record<string, string> = {
protagonist: '#e74c3c', protagonist: token.colorError,
antagonist: '#9b59b6', antagonist: token.colorPrimary,
supporting: '#3498db', supporting: token.colorInfo,
}; };
const baseColor = roleColorMap[node.role_type] || '#3498db'; const baseColor = roleColorMap[node.role_type] || token.colorInfo;
const labelContent = node.type === 'organization' ? ( const labelContent = node.type === 'organization' ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
<ApartmentOutlined style={{ fontSize: 24, color: '#27ae60', marginBottom: 4 }} /> <ApartmentOutlined style={{ fontSize: 24, color: token.colorSuccess, marginBottom: 4 }} />
<div style={{ fontWeight: 600, fontSize: 14, color: '#333', maxWidth: '90%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{node.name}</div> <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: '#666', marginTop: 2 }}>{detail?.organization_type || '组织'}</div> <div style={{ fontSize: 11, color: token.colorTextSecondary, marginTop: 2 }}>{detail?.organization_type || '组织'}</div>
</div> </div>
) : ( ) : (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
{detail?.avatar_url ? ( {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 }} /> <UserOutlined style={{ fontSize: 28, color: baseColor }} />
</div> </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)' }}> <div style={{ fontSize: 11, color: baseColor, marginTop: 2, transform: 'scale(0.9)' }}>
{node.role_type === 'protagonist' ? '主角' : node.role_type === 'antagonist' ? '反派' : '配角'} {node.role_type === 'protagonist' ? '主角' : node.role_type === 'antagonist' ? '反派' : '配角'}
</div> </div>
@@ -753,8 +813,8 @@ export default function RelationshipGraph() {
data: { data: {
label: ( label: (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ fontSize: 11, color: '#d48806', marginBottom: 2 }}></div> <div style={{ fontSize: 11, color: token.colorWarning, marginBottom: 2 }}></div>
<div style={{ fontWeight: 600, fontSize: 13, color: '#333' }}>{career.name}</div> <div style={{ fontWeight: 600, fontSize: 13, color: token.colorText }}>{career.name}</div>
</div> </div>
), ),
type: 'career_main', type: 'career_main',
@@ -769,8 +829,8 @@ export default function RelationshipGraph() {
data: { data: {
label: ( label: (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ fontSize: 11, color: '#08979c', marginBottom: 2 }}></div> <div style={{ fontSize: 11, color: token.colorInfo, marginBottom: 2 }}></div>
<div style={{ fontWeight: 600, fontSize: 13, color: '#333' }}>{career.name}</div> <div style={{ fontWeight: 600, fontSize: 13, color: token.colorText }}>{career.name}</div>
</div> </div>
), ),
type: 'career_sub', type: 'career_sub',
@@ -922,7 +982,23 @@ export default function RelationshipGraph() {
} finally { } finally {
setLoading(false); 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 加载完成后再加载图数据 // 当 relationshipTypes 加载完成后再加载图数据
useEffect(() => { useEffect(() => {
@@ -1000,27 +1076,27 @@ export default function RelationshipGraph() {
marginBottom: 12, marginBottom: 12,
padding: '12px 14px', padding: '12px 14px',
borderRadius: 12, borderRadius: 12,
background: '#f8f9fa', background: token.colorFillQuaternary,
border: '1px solid #eef0f2', border: `1px solid ${token.colorBorderSecondary}`,
boxShadow: '0 2px 4px rgba(0,0,0,0.02)', 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> </Text>
<div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 8 }}> <div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 8 }}>
{nodeDetail.main_career_id ? ( {nodeDetail.main_career_id ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Tag color="gold" style={{ margin: 0, borderRadius: 12, padding: '0 10px', fontWeight: 500 }}></Tag> <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} {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> </span>
</div> </div>
) : ( ) : (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Tag style={{ margin: 0, borderRadius: 12, padding: '0 10px' }}></Tag> <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>
)} )}
@@ -1029,9 +1105,9 @@ export default function RelationshipGraph() {
<Tag color="cyan" style={{ margin: 0, borderRadius: 12, padding: '0 10px', fontWeight: 500 }}></Tag> <Tag color="cyan" style={{ margin: 0, borderRadius: 12, padding: '0 10px', fontWeight: 500 }}></Tag>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, flex: 1 }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, flex: 1 }}>
{subCareerData.map((sub, index) => ( {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} {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> </span>
))} ))}
</div> </div>
@@ -1039,7 +1115,7 @@ export default function RelationshipGraph() {
) : ( ) : (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Tag style={{ margin: 0, borderRadius: 12, padding: '0 10px' }}></Tag> <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>
)} )}
</div> </div>
@@ -1057,7 +1133,7 @@ export default function RelationshipGraph() {
minHeight: 0, minHeight: 0,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
backgroundColor: '#f5f5f5', backgroundColor: token.colorBgLayout,
overflow: 'hidden', 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', justifyContent: 'flex-end', alignItems: 'center', gap: 14, fontSize: 12, flexWrap: 'wrap' }}>
{/* 节点图例 */} {/* 节点图例 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <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> <span></span>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <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> <span></span>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <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> <span></span>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <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> <span></span>
</div> </div>
<span style={{ color: '#d9d9d9' }}>|</span> <span style={{ color: token.colorBorder }}>|</span>
{/* 连线图例 */} {/* 连线图例 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <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> <span></span>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <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> <span></span>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <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> <span></span>
</div> </div>
</div> </div>
@@ -1141,8 +1217,8 @@ export default function RelationshipGraph() {
onClick={() => toggleEdgeCategoryVisibility(option.category)} onClick={() => toggleEdgeCategoryVisibility(option.category)}
style={ style={
isVisible isVisible
? { backgroundColor: option.color, borderColor: option.color, color: '#fff' } ? { backgroundColor: option.color, borderColor: option.color, color: token.colorWhite }
: { color: '#666' } : { color: token.colorTextSecondary }
} }
> >
{option.label}{option.count} {option.label}{option.count}
@@ -1154,7 +1230,68 @@ export default function RelationshipGraph() {
</Space> </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 <ReactFlow
nodes={nodes} nodes={nodes}
edges={visibleEdges} edges={visibleEdges}
@@ -1192,7 +1329,7 @@ export default function RelationshipGraph() {
width: '100%', width: '100%',
flex: 1, flex: 1,
borderRadius: 16, borderRadius: 16,
boxShadow: '0 12px 32px rgba(0,0,0,0.12)', boxShadow: `0 12px 32px ${alphaColor(token.colorTextBase, 0.22)}`,
overflow: 'hidden', overflow: 'hidden',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -1238,8 +1375,8 @@ export default function RelationshipGraph() {
height: '100%', height: '100%',
borderRadius: '50%', borderRadius: '50%',
objectFit: 'cover', objectFit: 'cover',
border: '3px solid #fff', border: `3px solid ${token.colorBgContainer}`,
boxShadow: '0 4px 12px rgba(0,0,0,0.1)', boxShadow: `0 4px 12px ${alphaColor(token.colorTextBase, 0.18)}`,
}} }}
/> />
) : ( ) : (
@@ -1248,14 +1385,14 @@ export default function RelationshipGraph() {
width: '100%', width: '100%',
height: '100%', height: '100%',
borderRadius: '50%', borderRadius: '50%',
backgroundColor: nodeDetail.color || (nodeDetail.is_organization ? '#27ae60' : '#1890ff'), backgroundColor: nodeDetail.color || (nodeDetail.is_organization ? token.colorSuccess : token.colorPrimary),
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
fontSize: 32, fontSize: 32,
color: '#fff', color: token.colorWhite,
border: '3px solid #fff', border: `3px solid ${token.colorBgContainer}`,
boxShadow: '0 4px 12px rgba(0,0,0,0.1)', boxShadow: `0 4px 12px ${alphaColor(token.colorTextBase, 0.18)}`,
}} }}
> >
{nodeDetail.is_organization ? <TeamOutlined /> : <UserOutlined />} {nodeDetail.is_organization ? <TeamOutlined /> : <UserOutlined />}
@@ -1266,23 +1403,23 @@ export default function RelationshipGraph() {
position: 'absolute', position: 'absolute',
bottom: -4, bottom: -4,
right: -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%', borderRadius: '50%',
width: 28, width: 28,
height: 28, height: 28,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
border: '2px solid #fff', border: `2px solid ${token.colorBgContainer}`,
color: '#fff', color: token.colorWhite,
boxShadow: '0 2px 6px rgba(0,0,0,0.15)', boxShadow: `0 2px 6px ${alphaColor(token.colorTextBase, 0.22)}`,
}} }}
> >
{nodeDetail.is_organization ? <ApartmentOutlined style={{ fontSize: 14 }} /> : <UserOutlined style={{ fontSize: 14 }} />} {nodeDetail.is_organization ? <ApartmentOutlined style={{ fontSize: 14 }} /> : <UserOutlined style={{ fontSize: 14 }} />}
</div> </div>
</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' }}> <Space size={6} wrap style={{ justifyContent: 'center' }}>
{!nodeDetail.is_organization && ( {!nodeDetail.is_organization && (
<Tag <Tag
@@ -1321,12 +1458,12 @@ export default function RelationshipGraph() {
marginBottom: 12, marginBottom: 12,
padding: '12px 14px', padding: '12px 14px',
borderRadius: 12, borderRadius: 12,
background: '#f8f9fa', background: token.colorFillQuaternary,
border: '1px solid #eef0f2', border: `1px solid ${token.colorBorderSecondary}`,
boxShadow: '0 2px 4px rgba(0,0,0,0.02)', 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> </Text>
<Space size={[6, 8]} wrap style={{ marginTop: 10 }}> <Space size={[6, 8]} wrap style={{ marginTop: 10 }}>
@@ -1352,16 +1489,16 @@ export default function RelationshipGraph() {
marginBottom: 12, marginBottom: 12,
padding: '12px 14px', padding: '12px 14px',
borderRadius: 12, borderRadius: 12,
background: '#f8f9fa', background: token.colorFillQuaternary,
border: '1px solid #eef0f2', border: `1px solid ${token.colorBorderSecondary}`,
boxShadow: '0 2px 4px rgba(0,0,0,0.02)', 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> </Text>
<div style={{ ...clampTextStyle(1), fontSize: 18, color: '#f39c12', fontWeight: 'bold' }}> <div style={{ ...clampTextStyle(1), fontSize: 18, color: token.colorWarning, fontWeight: 'bold' }}>
{nodeDetail.power_level}<span style={{ fontSize: 14, color: '#888', fontWeight: 'normal' }}>/100</span> {nodeDetail.power_level}<span style={{ fontSize: 14, color: token.colorTextTertiary, fontWeight: 'normal' }}>/100</span>
</div> </div>
</div> </div>
)} )}
@@ -1372,12 +1509,12 @@ export default function RelationshipGraph() {
marginBottom: 12, marginBottom: 12,
padding: '12px 14px', padding: '12px 14px',
borderRadius: 12, borderRadius: 12,
background: '#f8f9fa', background: token.colorFillQuaternary,
border: '1px solid #eef0f2', border: `1px solid ${token.colorBorderSecondary}`,
boxShadow: '0 2px 4px rgba(0,0,0,0.02)', 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> </Text>
<Space size={[6, 8]} wrap style={{ marginTop: 10 }}> <Space size={[6, 8]} wrap style={{ marginTop: 10 }}>
@@ -1407,9 +1544,9 @@ export default function RelationshipGraph() {
zIndex: 1000, 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"> <Space align="start">
<TrophyOutlined style={{ color: '#faad14', marginTop: 4 }} /> <TrophyOutlined style={{ color: token.colorWarning, marginTop: 4 }} />
<div> <div>
<Text strong></Text> <Text strong></Text>
<p style={{ ...clampTextStyle(2), marginTop: 2 }}> <p style={{ ...clampTextStyle(2), marginTop: 2 }}>
+3 -2
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; 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 { PlusOutlined, ApartmentOutlined, UserOutlined, EditOutlined } from '@ant-design/icons';
import { useStore } from '../store'; import { useStore } from '../store';
import axios from 'axios'; import axios from 'axios';
@@ -45,6 +45,7 @@ export default function Relationships() {
const [editingRelationship, setEditingRelationship] = useState<Relationship | null>(null); const [editingRelationship, setEditingRelationship] = useState<Relationship | null>(null);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
const { token } = theme.useToken();
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@@ -393,7 +394,7 @@ export default function Relationships() {
key={category} key={category}
size="small" size="small"
title={categoryLabels[category] || category} title={categoryLabels[category] || category}
headStyle={{ backgroundColor: '#f5f5f5' }} headStyle={{ backgroundColor: token.colorFillAlter }}
> >
<Space direction="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }}>
{types.map(type => ( {types.map(type => (
+69 -65
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; 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 { SaveOutlined, DeleteOutlined, ReloadOutlined, InfoCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined, PlusOutlined, EditOutlined, CopyOutlined, WarningOutlined } from '@ant-design/icons';
import { settingsApi, mcpPluginApi } from '../services/api'; import { settingsApi, mcpPluginApi } from '../services/api';
import type { SettingsUpdate, APIKeyPreset, PresetCreateRequest, APIKeyPresetConfig } from '../types'; import type { SettingsUpdate, APIKeyPreset, PresetCreateRequest, APIKeyPresetConfig } from '../types';
@@ -11,6 +11,7 @@ const { useBreakpoint } = Grid;
const { TextArea } = Input; const { TextArea } = Input;
export default function SettingsPage() { export default function SettingsPage() {
const { token } = theme.useToken();
const screens = useBreakpoint(); const screens = useBreakpoint();
const isMobile = !screens.md; // md断点是768px const isMobile = !screens.md; // md断点是768px
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -51,6 +52,9 @@ export default function SettingsPage() {
const [presetModelsFetched, setPresetModelsFetched] = useState(false); const [presetModelsFetched, setPresetModelsFetched] = useState(false);
const [presetModelSearchText, setPresetModelSearchText] = useState(''); 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(() => { useEffect(() => {
loadSettings(); loadSettings();
if (activeTab === 'presets') { if (activeTab === 'presets') {
@@ -181,7 +185,7 @@ export default function SettingsPage() {
modal.warning({ modal.warning({
title: ( title: (
<Space> <Space>
<WarningOutlined style={{ color: '#faad14' }} /> <WarningOutlined style={{ color: token.colorWarning }} />
<span>API </span> <span>API </span>
</Space> </Space>
), ),
@@ -196,8 +200,8 @@ export default function SettingsPage() {
/> />
<div style={{ <div style={{
padding: 12, padding: 12,
background: 'var(--color-info-bg)', background: token.colorInfoBg,
border: '1px solid var(--color-info-border)', border: `1px solid ${token.colorInfoBorder}`,
borderRadius: 8 borderRadius: 8
}}> }}>
<Text strong style={{ display: 'block', marginBottom: 8 }}></Text> <Text strong style={{ display: 'block', marginBottom: 8 }}></Text>
@@ -600,7 +604,7 @@ export default function SettingsPage() {
modal.warning({ modal.warning({
title: ( title: (
<Space> <Space>
<WarningOutlined style={{ color: '#faad14' }} /> <WarningOutlined style={{ color: token.colorWarning }} />
<span>API </span> <span>API </span>
</Space> </Space>
), ),
@@ -615,8 +619,8 @@ export default function SettingsPage() {
/> />
<div style={{ <div style={{
padding: 12, padding: 12,
background: 'var(--color-info-bg)', background: token.colorInfoBg,
border: '1px solid var(--color-info-border)', border: `1px solid ${token.colorInfoBorder}`,
borderRadius: 8 borderRadius: 8
}}> }}>
<Text strong style={{ display: 'block', marginBottom: 8 }}></Text> <Text strong style={{ display: 'block', marginBottom: 8 }}></Text>
@@ -657,15 +661,15 @@ export default function SettingsPage() {
width: isMobile ? '90%' : 600, width: isMobile ? '90%' : 600,
content: ( content: (
<div style={{ padding: '8px 0' }}> <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 }}> <div style={{ marginBottom: 24, padding: 16, background: token.colorSuccessBg, border: `1px solid ${token.colorSuccessBorder}`, borderRadius: 8 }}>
<Typography.Text strong style={{ color: 'var(--color-success)' }}> <Typography.Text strong style={{ color: token.colorSuccess }}>
API API
</Typography.Text> </Typography.Text>
</div> </div>
<div style={{ <div style={{
padding: 16, padding: 16,
background: 'var(--color-bg-layout)', background: token.colorBgLayout,
borderRadius: 8, borderRadius: 8,
marginBottom: 16 marginBottom: 16
}}> }}>
@@ -711,13 +715,13 @@ export default function SettingsPage() {
{result.error && ( {result.error && (
<div style={{ <div style={{
padding: 16, padding: 16,
background: 'var(--color-error-bg)', background: token.colorErrorBg,
border: '1px solid var(--color-error-border)', border: `1px solid ${token.colorErrorBorder}`,
borderRadius: 8, borderRadius: 8,
marginBottom: 16 marginBottom: 16
}}> }}>
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>:</Text> <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} {result.error}
</Text> </Text>
</div> </div>
@@ -726,8 +730,8 @@ export default function SettingsPage() {
{result.suggestions && result.suggestions.length > 0 && ( {result.suggestions && result.suggestions.length > 0 && (
<div style={{ <div style={{
padding: 16, padding: 16,
background: 'var(--color-warning-bg)', background: token.colorWarningBg,
border: '1px solid var(--color-warning-border)', border: `1px solid ${token.colorWarningBorder}`,
borderRadius: 8, borderRadius: 8,
marginBottom: 16 marginBottom: 16
}}> }}>
@@ -817,10 +821,10 @@ export default function SettingsPage() {
<List.Item <List.Item
key={preset.id} key={preset.id}
style={{ style={{
background: isActive ? '#f0f5ff' : 'transparent', background: isActive ? token.colorInfoBg : 'transparent',
padding: '16px', padding: '16px',
marginBottom: '8px', marginBottom: '8px',
border: isActive ? '2px solid #1890ff' : '1px solid #f0f0f0', border: isActive ? `2px solid ${token.colorPrimary}` : `1px solid ${token.colorBorderSecondary}`,
borderRadius: '8px', borderRadius: '8px',
}} }}
actions={[ actions={[
@@ -870,7 +874,7 @@ export default function SettingsPage() {
avatar={ avatar={
isActive && ( isActive && (
<CheckCircleOutlined <CheckCircleOutlined
style={{ fontSize: '24px', color: '#52c41a' }} style={{ fontSize: '24px', color: token.colorSuccess }}
/> />
) )
} }
@@ -883,7 +887,7 @@ export default function SettingsPage() {
description={ description={
<Space direction="vertical" size="small" style={{ width: '100%' }}> <Space direction="vertical" size="small" style={{ width: '100%' }}>
{preset.description && ( {preset.description && (
<div style={{ color: '#666' }}>{preset.description}</div> <div style={{ color: token.colorTextSecondary }}>{preset.description}</div>
)} )}
<Space wrap> <Space wrap>
<Tag color={getProviderColor(preset.config.api_provider)}> <Tag color={getProviderColor(preset.config.api_provider)}>
@@ -893,7 +897,7 @@ export default function SettingsPage() {
<Tag>: {preset.config.temperature}</Tag> <Tag>: {preset.config.temperature}</Tag>
<Tag>Tokens: {preset.config.max_tokens}</Tag> <Tag>Tokens: {preset.config.max_tokens}</Tag>
</Space> </Space>
<div style={{ fontSize: '12px', color: '#999' }}> <div style={{ fontSize: '12px', color: token.colorTextTertiary }}>
: {new Date(preset.created_at).toLocaleString()} : {new Date(preset.created_at).toLocaleString()}
</div> </div>
</Space> </Space>
@@ -913,7 +917,7 @@ export default function SettingsPage() {
{contextHolder} {contextHolder}
<div style={{ <div style={{
minHeight: '90vh', minHeight: '90vh',
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)', background: pageBackground,
padding: isMobile ? '20px 16px 70px' : '24px 24px 70px', padding: isMobile ? '20px 16px 70px' : '24px 24px 70px',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -930,9 +934,9 @@ export default function SettingsPage() {
<Card <Card
variant="borderless" variant="borderless"
style={{ style={{
background: 'linear-gradient(135deg, var(--color-primary) 0%, #5A9BA5 50%, var(--color-primary-hover) 100%)', background: headerBackground,
borderRadius: isMobile ? 16 : 24, 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, marginBottom: isMobile ? 20 : 24,
border: 'none', border: 'none',
position: 'relative', 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', 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: 'rgba(255, 255, 255, 0.05)', 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: 'rgba(255, 255, 255, 0.06)', 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 }}> <Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
<Col xs={24} sm={12}> <Col xs={24} sm={12}>
<Space direction="vertical" size={4}> <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 AI API
</Title> </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配置预设 AI接口参数API配置预设
</Text> </Text>
</Space> </Space>
@@ -965,9 +969,9 @@ export default function SettingsPage() {
<Card <Card
variant="borderless" variant="borderless"
style={{ style={{
background: 'rgba(255, 255, 255, 0.95)', background: token.colorBgContainer,
borderRadius: isMobile ? 12 : 16, borderRadius: isMobile ? 12 : 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)', boxShadow: token.boxShadowSecondary,
flex: 1, flex: 1,
}} }}
styles={{ styles={{
@@ -1030,7 +1034,7 @@ export default function SettingsPage() {
<span>API </span> <span>API </span>
<InfoCircleOutlined <InfoCircleOutlined
title="选择你的AI服务提供商" title="选择你的AI服务提供商"
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
/> />
</Space> </Space>
} }
@@ -1052,7 +1056,7 @@ export default function SettingsPage() {
<span>API </span> <span>API </span>
<InfoCircleOutlined <InfoCircleOutlined
title="你的API密钥,将加密存储" title="你的API密钥,将加密存储"
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
/> />
</Space> </Space>
} }
@@ -1072,7 +1076,7 @@ export default function SettingsPage() {
<span>API </span> <span>API </span>
<InfoCircleOutlined <InfoCircleOutlined
title="API的基础URL地址" title="API的基础URL地址"
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
/> />
</Space> </Space>
} }
@@ -1094,7 +1098,7 @@ export default function SettingsPage() {
<span></span> <span></span>
<InfoCircleOutlined <InfoCircleOutlined
title="AI模型的名称,如 gpt-4, gpt-3.5-turbo" 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> </Space>
} }
@@ -1121,17 +1125,17 @@ export default function SettingsPage() {
<> <>
{menu} {menu}
{fetchingModels && ( {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" /> ... <Spin size="small" /> ...
</div> </div>
)} )}
{!fetchingModels && modelOptions.length === 0 && modelsFetched && !modelSearchText && ( {!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> </div>
)} )}
{!fetchingModels && modelOptions.length === 0 && !modelsFetched && !modelSearchText && ( {!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> </div>
)} )}
@@ -1200,13 +1204,13 @@ export default function SettingsPage() {
<div style={{ fontWeight: 500, fontSize: isMobile ? '13px' : '14px' }}> <div style={{ fontWeight: 500, fontSize: isMobile ? '13px' : '14px' }}>
{option.data.description === '手动输入的模型名称' ? ( {option.data.description === '手动输入的模型名称' ? (
<Space size={4}> <Space size={4}>
<EditOutlined style={{ color: 'var(--color-primary)' }} /> <EditOutlined style={{ color: token.colorPrimary }} />
<span>使 "{option.data.label}"</span> <span>使 "{option.data.label}"</span>
</Space> </Space>
) : option.data.label} ) : option.data.label}
</div> </div>
{option.data.description && option.data.description !== '手动输入的模型名称' && ( {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} {option.data.description}
</div> </div>
)} )}
@@ -1221,7 +1225,7 @@ export default function SettingsPage() {
<span></span> <span></span>
<InfoCircleOutlined <InfoCircleOutlined
title="控制输出的随机性,值越高越随机(0.0-2.0)" title="控制输出的随机性,值越高越随机(0.0-2.0)"
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
/> />
</Space> </Space>
} }
@@ -1246,7 +1250,7 @@ export default function SettingsPage() {
<span> Token </span> <span> Token </span>
<InfoCircleOutlined <InfoCircleOutlined
title="单次请求的最大token数量" title="单次请求的最大token数量"
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
/> />
</Space> </Space>
} }
@@ -1270,7 +1274,7 @@ export default function SettingsPage() {
<span></span> <span></span>
<InfoCircleOutlined <InfoCircleOutlined
title="设置全局系统提示词,每次AI调用时都会自动使用。可用于设定AI的角色、语言风格等" title="设置全局系统提示词,每次AI调用时都会自动使用。可用于设定AI的角色、语言风格等"
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
/> />
</Space> </Space>
} }
@@ -1291,9 +1295,9 @@ export default function SettingsPage() {
message={ message={
<Space> <Space>
{testResult.success ? ( {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 }}> <span style={{ fontSize: isMobile ? '14px' : '16px', fontWeight: 500 }}>
{testResult.message} {testResult.message}
@@ -1313,16 +1317,16 @@ export default function SettingsPage() {
<div style={{ <div style={{
fontSize: isMobile ? '12px' : '13px', fontSize: isMobile ? '12px' : '13px',
padding: '8px 12px', padding: '8px 12px',
background: '#f6ffed', background: token.colorSuccessBg,
borderRadius: '4px', borderRadius: '4px',
border: '1px solid #b7eb8f', border: `1px solid ${token.colorSuccessBorder}`,
marginTop: '8px' marginTop: '8px'
}}> }}>
<div style={{ marginBottom: '4px', fontWeight: 500 }}>AI :</div> <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>
)} )}
<div style={{ color: 'var(--color-success)', fontSize: isMobile ? '12px' : '13px', marginTop: '4px' }}> <div style={{ color: token.colorSuccess, fontSize: isMobile ? '12px' : '13px', marginTop: '4px' }}>
API 使 API 使
</div> </div>
</Space> </Space>
@@ -1332,16 +1336,16 @@ export default function SettingsPage() {
<div style={{ <div style={{
fontSize: isMobile ? '12px' : '13px', fontSize: isMobile ? '12px' : '13px',
padding: '8px 12px', padding: '8px 12px',
background: '#fff2e8', background: token.colorErrorBg,
borderRadius: '4px', borderRadius: '4px',
border: '1px solid #ffbb96', border: `1px solid ${token.colorErrorBorder}`,
color: '#d4380d' color: token.colorError
}}> }}>
<strong>:</strong> {testResult.error} <strong>:</strong> {testResult.error}
</div> </div>
)} )}
{testResult.error_type && ( {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} : {testResult.error_type}
</div> </div>
)} )}
@@ -1354,7 +1358,7 @@ export default function SettingsPage() {
margin: 0, margin: 0,
paddingLeft: isMobile ? '16px' : '20px', paddingLeft: isMobile ? '16px' : '20px',
fontSize: isMobile ? '12px' : '13px', fontSize: isMobile ? '12px' : '13px',
color: '#595959' color: token.colorTextSecondary
}}> }}>
{testResult.suggestions.map((suggestion, index) => ( {testResult.suggestions.map((suggestion, index) => (
<li key={index} style={{ marginBottom: '4px' }}>{suggestion}</li> <li key={index} style={{ marginBottom: '4px' }}>{suggestion}</li>
@@ -1386,7 +1390,7 @@ export default function SettingsPage() {
loading={loading} loading={loading}
block block
style={{ style={{
background: 'var(--color-primary)', background: token.colorPrimary,
border: 'none', border: 'none',
height: '44px' height: '44px'
}} }}
@@ -1400,8 +1404,8 @@ export default function SettingsPage() {
loading={testingApi} loading={testingApi}
block block
style={{ style={{
borderColor: 'var(--color-success)', borderColor: token.colorSuccess,
color: 'var(--color-success)', color: token.colorSuccess,
fontWeight: 500, fontWeight: 500,
height: '44px' height: '44px'
}} }}
@@ -1466,8 +1470,8 @@ export default function SettingsPage() {
onClick={handleTestConnection} onClick={handleTestConnection}
loading={testingApi} loading={testingApi}
style={{ style={{
borderColor: 'var(--color-success)', borderColor: token.colorSuccess,
color: 'var(--color-success)', color: token.colorSuccess,
fontWeight: 500, fontWeight: 500,
minWidth: '100px' minWidth: '100px'
}} }}
@@ -1491,7 +1495,7 @@ export default function SettingsPage() {
htmlType="submit" htmlType="submit"
loading={loading} loading={loading}
style={{ style={{
background: 'var(--color-primary)', background: token.colorPrimary,
border: 'none', border: 'none',
minWidth: '120px', minWidth: '120px',
fontWeight: 500 fontWeight: 500
@@ -1611,7 +1615,7 @@ export default function SettingsPage() {
<span></span> <span></span>
<InfoCircleOutlined <InfoCircleOutlined
title="AI模型的名称,点击下拉框自动获取可用模型" title="AI模型的名称,点击下拉框自动获取可用模型"
style={{ color: 'var(--color-text-secondary)', fontSize: '12px' }} style={{ color: token.colorTextSecondary, fontSize: '12px' }}
/> />
</Space> </Space>
} }
@@ -1637,17 +1641,17 @@ export default function SettingsPage() {
<> <>
{menu} {menu}
{fetchingPresetModels && ( {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" /> ... <Spin size="small" /> ...
</div> </div>
)} )}
{!fetchingPresetModels && presetModelOptions.length === 0 && presetModelsFetched && !presetModelSearchText && ( {!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> </div>
)} )}
{!fetchingPresetModels && presetModelOptions.length === 0 && !presetModelsFetched && !presetModelSearchText && ( {!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> </div>
)} )}
@@ -1714,13 +1718,13 @@ export default function SettingsPage() {
<div style={{ fontWeight: 500, fontSize: '13px' }}> <div style={{ fontWeight: 500, fontSize: '13px' }}>
{option.data.description === '手动输入的模型名称' ? ( {option.data.description === '手动输入的模型名称' ? (
<Space size={4}> <Space size={4}>
<EditOutlined style={{ color: 'var(--color-primary)' }} /> <EditOutlined style={{ color: token.colorPrimary }} />
<span>使 "{option.data.label}"</span> <span>使 "{option.data.label}"</span>
</Space> </Space>
) : option.data.label} ) : option.data.label}
</div> </div>
{option.data.description && option.data.description !== '手动输入的模型名称' && ( {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} {option.data.description}
</div> </div>
)} )}
+81 -56
View File
@@ -1,12 +1,13 @@
import { useState } from 'react'; import { useState, type ReactNode } from 'react';
import { Card, Row, Col, Typography, Image, Divider, Modal, Button } from 'antd'; import { Card, Row, Col, Typography, Image, Divider, Modal, Button, theme } from 'antd';
import { import {
HeartOutlined, HeartOutlined,
CheckCircleOutlined, CheckCircleOutlined,
FileTextOutlined, FileTextOutlined,
RocketOutlined, RocketOutlined,
MessageOutlined, MessageOutlined,
StarOutlined // StarOutlined,
WechatOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
const { Title, Paragraph, Text } = Typography; const { Title, Paragraph, Text } = Typography;
@@ -18,6 +19,13 @@ interface SponsorOption {
description: string; description: string;
} }
interface SponsorBenefit {
icon: ReactNode;
title: string;
description: string;
price?: string;
}
const sponsorOptions: SponsorOption[] = [ const sponsorOptions: SponsorOption[] = [
{ amount: 5, label: '🌶️ 一包辣条', image: '/5.png', description: '¥5' }, { amount: 5, label: '🌶️ 一包辣条', image: '/5.png', description: '¥5' },
{ amount: 10, label: '🍱 一顿拼好饭', image: '/10.png', description: '¥10' }, { amount: 10, label: '🍱 一顿拼好饭', image: '/10.png', description: '¥10' },
@@ -26,27 +34,39 @@ const sponsorOptions: SponsorOption[] = [
{ amount: 99, label: '🍲 一顿海底捞', image: '/99.png', description: '¥99' }, { 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: '优先需求响应', 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一键启动', 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: '专属技术支持', title: '专属技术支持',
description: '加入赞助者群,获得远程协助和配置指导(☕ 一杯咖啡)' description: '获得远程协助和配置指导',
price: '(☕ 一杯咖啡)'
} }
]; ];
export default function Sponsor() { export default function Sponsor() {
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const [selectedOption, setSelectedOption] = useState<SponsorOption | null>(null); 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) => { const handleCardClick = (option: SponsorOption) => {
setSelectedOption(option); setSelectedOption(option);
@@ -64,10 +84,11 @@ export default function Sponsor() {
flex: 1, flex: 1,
overflowY: 'auto', overflowY: 'auto',
overflowX: 'hidden', overflowX: 'hidden',
padding: 'clamp(16px, 3vh, 24px) clamp(12px, 2vw, 16px)' // padding: 'clamp(16px, 3vh, 24px) clamp(12px, 2vw, 16px)'
}}> }}>
<div style={{ <div style={{
maxWidth: '1200px', // maxWidth: '1200px',
height: '100%',
margin: '0 auto', margin: '0 auto',
width: '100%', width: '100%',
display: 'flex', display: 'flex',
@@ -76,46 +97,45 @@ export default function Sponsor() {
}}> }}>
{/* 头部标题区域 */} {/* 头部标题区域 */}
<div style={{ textAlign: 'center', marginBottom: 'clamp(20px, 4vh, 32px)' }}> <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={{ <div style={{
marginTop: 'clamp(12px, 2vh, 16px)',
padding: 'clamp(12px, 2vh, 16px)', padding: 'clamp(12px, 2vh, 16px)',
background: 'var(--color-primary)', background: token.colorPrimary,
borderRadius: '12px', 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 📚 MuMuAINovel - AI
</Title> </Title>
<Paragraph style={{ color: '#fff', fontSize: '14px', margin: 0 }}>
AI模型
</Paragraph>
</div> </div>
</div> </div>
{/* 赞助专属权益 */} {/* 赞助专属权益 */}
<div style={{ marginBottom: 'clamp(24px, 4vh, 32px)' }}> <div style={{ marginBottom: 'clamp(24px, 4vh, 32px)' }}>
<Title level={3} style={{ textAlign: 'center', marginBottom: 'clamp(16px, 3vh, 20px)', fontSize: 'clamp(18px, 3vw, 24px)' }}> <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> </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) => ( {benefits.map((benefit, index) => (
<Col xs={24} md={8} key={index}> <Col key={index} flex="1" style={{ minWidth: '200px' }}>
<Card <Card
hoverable hoverable
style={{ style={{
height: '100%', height: '100%',
textAlign: 'center', textAlign: 'center',
borderRadius: '10px', borderRadius: '10px',
boxShadow: '0 2px 8px rgba(0,0,0,0.08)' boxShadow: `0 2px 8px ${alphaColor(token.colorTextBase, 0.12)}`
}} }}
styles={{ styles={{
body: { padding: 'clamp(16px, 3vh, 20px) clamp(12px, 2vw, 16px)' } body: { padding: 'clamp(16px, 3vh, 20px) clamp(12px, 2vw, 16px)' }
@@ -125,9 +145,14 @@ export default function Sponsor() {
{benefit.icon} {benefit.icon}
</div> </div>
<Title level={5} style={{ marginBottom: '8px', fontSize: 'clamp(14px, 2.5vw, 16px)' }}>{benefit.title}</Title> <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} {benefit.description}
</Paragraph> </Paragraph>
{benefit.price && (
<Paragraph style={{ color: token.colorWarning, margin: '4px 0 0', fontSize: 'clamp(12px, 2vw, 13px)', fontWeight: 600 }}>
{benefit.price}
</Paragraph>
)}
</Card> </Card>
</Col> </Col>
))} ))}
@@ -135,9 +160,9 @@ export default function Sponsor() {
</div> </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)' }}> <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> </Title>
@@ -150,34 +175,34 @@ export default function Sponsor() {
style={{ style={{
textAlign: 'center', textAlign: 'center',
borderRadius: '10px', borderRadius: '10px',
boxShadow: 'var(--shadow-card)', boxShadow: `0 2px 8px ${alphaColor(token.colorTextBase, 0.12)}`,
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.3s', transition: 'all 0.3s',
border: '2px solid var(--color-border)' border: `2px solid ${token.colorBorder}`
}} }}
styles={{ styles={{
body: { padding: 'clamp(16px, 3vh, 20px) clamp(10px, 2vw, 12px)' } body: { padding: 'clamp(16px, 3vh, 20px) clamp(10px, 2vw, 12px)' }
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-8px)'; e.currentTarget.style.transform = 'translateY(-8px)';
e.currentTarget.style.boxShadow = 'var(--shadow-elevated)'; e.currentTarget.style.boxShadow = `0 8px 24px ${alphaColor(token.colorPrimary, 0.3)}`;
e.currentTarget.style.borderColor = 'var(--color-primary)'; e.currentTarget.style.borderColor = token.colorPrimary;
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'var(--shadow-card)'; e.currentTarget.style.boxShadow = `0 2px 8px ${alphaColor(token.colorTextBase, 0.12)}`;
e.currentTarget.style.borderColor = 'var(--color-border)'; e.currentTarget.style.borderColor = token.colorBorder;
}} }}
> >
<Title level={3} style={{ <Title level={3} style={{
color: 'var(--color-primary)', color: token.colorPrimary,
marginBottom: '4px', marginBottom: '4px',
fontSize: 'clamp(20px, 4vw, 28px)', fontSize: 'clamp(20px, 4vw, 28px)',
fontWeight: 'bold' fontWeight: 'bold'
}}> }}>
{option.description} {option.description}
</Title> </Title>
<Text style={{ fontSize: 'clamp(12px, 2vw, 14px)', color: '#666' }}> <Text style={{ fontSize: 'clamp(12px, 2vw, 14px)', color: token.colorTextSecondary }}>
{option.label} {option.label}
</Text> </Text>
</Card> </Card>
@@ -186,29 +211,29 @@ export default function Sponsor() {
</Row> </Row>
</div> </div>
<Divider style={{ margin: 'clamp(16px, 3vh, 24px) 0' }} /> <Divider style={{ margin: 'clamp(16px, 3vh, 18px) 0' }} />
{/* 感谢文案 */} {/* 感谢文案 */}
<div style={{ <div style={{
textAlign: 'center', textAlign: 'center',
padding: 'clamp(16px, 3vh, 24px) clamp(16px, 3vw, 20px)', padding: 'clamp(16px, 3vw, 20px)',
background: '#f9f9f9', background: token.colorFillQuaternary,
borderRadius: '10px', borderRadius: '10px',
marginTop: 'auto' marginTop: 'auto'
}}> }}>
<Title level={4} style={{ marginBottom: '12px', fontSize: 'clamp(16px, 3vw, 20px)' }}> <Title level={4} style={{ marginBottom: '12px', fontSize: 'clamp(16px, 3vw, 20px)' }}>
💖 MuMuAINovel 💖 MuMuAINovel
</Title> </Title>
<Paragraph style={{ fontSize: 'clamp(12px, 2vw, 14px)', color: '#666', marginBottom: '12px' }}> <Paragraph style={{ fontSize: 'clamp(12px, 2vw, 14px)', color: token.colorTextSecondary, marginBottom: '12px' }}>
AI小说创作体验 AI小说创作体验!
</Paragraph> </Paragraph>
<div style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}> {/* <div style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>
<StarOutlined style={{ color: '#faad14', margin: '0 4px' }} /> <StarOutlined style={{ color: token.colorWarning, margin: '0 4px' }} />
<StarOutlined style={{ color: '#faad14', margin: '0 4px' }} /> <StarOutlined style={{ color: token.colorWarning, margin: '0 4px' }} />
<StarOutlined style={{ color: '#faad14', margin: '0 4px' }} /> <StarOutlined style={{ color: token.colorWarning, margin: '0 4px' }} />
<StarOutlined style={{ color: '#faad14', margin: '0 4px' }} /> <StarOutlined style={{ color: token.colorWarning, margin: '0 4px' }} />
<StarOutlined style={{ color: '#faad14', margin: '0 4px' }} /> <StarOutlined style={{ color: token.colorWarning, margin: '0 4px' }} />
</div> </div> */}
</div> </div>
</div> </div>
</div> </div>
@@ -240,14 +265,14 @@ export default function Sponsor() {
style={{ style={{
maxWidth: '280px', maxWidth: '280px',
borderRadius: '8px', borderRadius: '8px',
border: '1px solid #f0f0f0' border: `1px solid ${token.colorBorderSecondary}`
}} }}
preview={false} preview={false}
/> />
<Paragraph style={{ marginTop: '20px', color: '#666' }}> <Paragraph style={{ marginTop: '20px', color: token.colorTextSecondary }}>
</Paragraph> </Paragraph>
<Paragraph style={{ color: '#999', fontSize: '12px' }}> <Paragraph style={{ color: token.colorTextTertiary, fontSize: '12px' }}>
/QQ联系我们获取权益 /QQ联系我们获取权益
</Paragraph> </Paragraph>
</div> </div>
+31 -28
View File
@@ -19,6 +19,7 @@ import {
Col, Col,
Pagination, Pagination,
Dropdown, Dropdown,
theme,
} from 'antd'; } from 'antd';
import { import {
PlusOutlined, PlusOutlined,
@@ -72,6 +73,8 @@ export default function UserManagement() {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [editForm] = Form.useForm(); const [editForm] = Form.useForm();
const [modal, contextHolder] = Modal.useModal(); 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 => { const filteredUsers = users.filter(user => {
@@ -180,7 +183,7 @@ export default function UserManagement() {
<div> <div>
<p><Text strong>{values.username}</Text></p> <p><Text strong>{values.username}</Text></p>
<p><Text strong copyable>{res.default_password}</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> </p>
</div> </div>
@@ -270,7 +273,7 @@ export default function UserManagement() {
<div> <div>
<p><Text strong>{currentUser.username}</Text></p> <p><Text strong>{currentUser.username}</Text></p>
<p><Text strong copyable>{res.new_password}</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> </p>
</div> </div>
@@ -312,7 +315,7 @@ export default function UserManagement() {
sortOrder: sortField === 'username' ? sortOrder : null, sortOrder: sortField === 'username' ? sortOrder : null,
render: (text: string) => ( render: (text: string) => (
<Space> <Space>
<UserOutlined style={{ color: 'var(--color-primary)' }} /> <UserOutlined style={{ color: token.colorPrimary }} />
<Text strong>{text}</Text> <Text strong>{text}</Text>
</Space> </Space>
), ),
@@ -508,7 +511,7 @@ export default function UserManagement() {
return ( return (
<div style={{ <div style={{
height: '100vh', 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', padding: isMobile ? '20px 16px' : '40px 24px',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -528,9 +531,9 @@ export default function UserManagement() {
<Card <Card
variant="borderless" variant="borderless"
style={{ 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, 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, marginBottom: isMobile ? 20 : 24,
border: 'none', border: 'none',
position: 'relative', 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', 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: 'rgba(255, 255, 255, 0.05)', 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: 'rgba(255, 255, 255, 0.06)', 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 }}> <Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
<Col xs={24} sm={12}> <Col xs={24} sm={12}>
<Space direction="vertical" size={4}> <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 ${alphaColor(token.colorText, 0.2)}` }}>
<TeamOutlined style={{ color: 'rgba(255,255,255,0.9)', marginRight: 12 }} /> <TeamOutlined style={{ color: alphaColor(token.colorWhite, 0.9), marginRight: 12 }} />
</Title> </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> </Text>
</Space> </Space>
@@ -561,19 +564,19 @@ export default function UserManagement() {
onClick={() => navigate('/')} onClick={() => navigate('/')}
style={{ style={{
borderRadius: 12, borderRadius: 12,
background: 'rgba(255, 255, 255, 0.15)', background: alphaColor(token.colorWhite, 0.15),
border: '1px solid rgba(255, 255, 255, 0.3)', border: `1px solid ${alphaColor(token.colorWhite, 0.3)}`,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', boxShadow: `0 2px 8px ${alphaColor(token.colorText, 0.15)}`,
color: '#fff', color: token.colorWhite,
backdropFilter: 'blur(10px)', backdropFilter: 'blur(10px)',
transition: 'all 0.3s ease' transition: 'all 0.3s ease'
}} }}
onMouseEnter={(e) => { 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)'; e.currentTarget.style.transform = 'translateY(-1px)';
}} }}
onMouseLeave={(e) => { 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'; e.currentTarget.style.transform = 'none';
}} }}
> >
@@ -585,10 +588,10 @@ export default function UserManagement() {
onClick={() => setModalVisible(true)} onClick={() => setModalVisible(true)}
style={{ style={{
borderRadius: 12, borderRadius: 12,
background: 'rgba(255, 193, 7, 0.95)', background: alphaColor(token.colorWarning, 0.95),
border: '1px solid rgba(255, 255, 255, 0.3)', border: `1px solid ${alphaColor(token.colorWhite, 0.3)}`,
boxShadow: '0 4px 16px rgba(255, 193, 7, 0.4)', boxShadow: `0 4px 16px ${alphaColor(token.colorWarning, 0.4)}`,
color: '#fff', color: token.colorWhite,
fontWeight: 600 fontWeight: 600
}} }}
> >
@@ -604,11 +607,11 @@ export default function UserManagement() {
<Card <Card
variant="borderless" variant="borderless"
style={{ style={{
background: 'rgba(255, 255, 255, 0.7)', background: alphaColor(token.colorBgContainer, 0.72),
borderRadius: isMobile ? 16 : 24, borderRadius: isMobile ? 16 : 24,
border: '1px solid rgba(255, 255, 255, 0.4)', border: `1px solid ${alphaColor(token.colorWhite, 0.45)}`,
backdropFilter: 'blur(20px)', backdropFilter: 'blur(20px)',
boxShadow: '0 4px 24px rgba(0, 0, 0, 0.04)', boxShadow: `0 4px 24px ${alphaColor(token.colorText, 0.06)}`,
flex: 1, flex: 1,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -625,11 +628,11 @@ export default function UserManagement() {
{/* 搜索栏 */} {/* 搜索栏 */}
<div style={{ <div style={{
padding: '16px 24px 0 24px', padding: '16px 24px 0 24px',
borderBottom: '1px solid rgba(0, 0, 0, 0.03)', borderBottom: `1px solid ${alphaColor(token.colorText, 0.06)}`,
}}> }}>
<Input <Input
placeholder="搜索用户名、显示名称或用户ID" placeholder="搜索用户名、显示名称或用户ID"
prefix={<SearchOutlined style={{ color: '#999' }} />} prefix={<SearchOutlined style={{ color: token.colorTextTertiary }} />}
value={searchText} value={searchText}
onChange={(e) => { onChange={(e) => {
setSearchText(e.target.value); setSearchText(e.target.value);
@@ -676,7 +679,7 @@ export default function UserManagement() {
{/* 固定分页控件 */} {/* 固定分页控件 */}
<div style={{ <div style={{
padding: '16px 24px 24px 24px', padding: '16px 24px 24px 24px',
borderTop: '1px solid rgba(0, 0, 0, 0.03)', borderTop: `1px solid ${alphaColor(token.colorText, 0.06)}`,
background: 'transparent', background: 'transparent',
display: 'flex', display: 'flex',
justifyContent: 'center', 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 { GlobalOutlined, EditOutlined, SyncOutlined, FormOutlined } from '@ant-design/icons';
import { useState } from 'react'; import { useState } from 'react';
import { useStore } from '../store'; import { useStore } from '../store';
import { cardStyles } from '../components/CardStyles'; import { worldSettingCardStyles } from '../components/CardStyles';
import { projectApi, wizardStreamApi } from '../services/api'; import { projectApi, wizardStreamApi } from '../services/api';
import { SSELoadingOverlay } from '../components/SSELoadingOverlay'; import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
@@ -29,6 +29,7 @@ export default function WorldSetting() {
} | null>(null); } | null>(null);
const [isSavingPreview, setIsSavingPreview] = useState(false); const [isSavingPreview, setIsSavingPreview] = useState(false);
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
const { token } = theme.useToken();
// AI重新生成世界观 // AI重新生成世界观
const handleRegenerate = async () => { const handleRegenerate = async () => {
@@ -140,14 +141,14 @@ export default function WorldSetting() {
position: 'sticky', position: 'sticky',
top: 0, top: 0,
zIndex: 10, zIndex: 10,
backgroundColor: '#fff', backgroundColor: token.colorBgContainer,
padding: '16px 0', padding: '16px 0',
marginBottom: 16, marginBottom: 16,
borderBottom: '1px solid var(--color-border-secondary)', borderBottom: `1px solid ${token.colorBorderSecondary}`,
display: 'flex', display: 'flex',
alignItems: 'center' 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> <h2 style={{ margin: 0 }}></h2>
</div> </div>
@@ -174,10 +175,10 @@ export default function WorldSetting() {
position: 'sticky', position: 'sticky',
top: 0, top: 0,
zIndex: 10, zIndex: 10,
backgroundColor: '#fff', backgroundColor: token.colorBgContainer,
padding: '16px 0', padding: '16px 0',
marginBottom: 24, marginBottom: 24,
borderBottom: '1px solid #f0f0f0' borderBottom: `1px solid ${token.colorBorderSecondary}`
}}> }}>
<Flex <Flex
justify="space-between" justify="space-between"
@@ -186,7 +187,7 @@ export default function WorldSetting() {
wrap="wrap" wrap="wrap"
> >
<div style={{ display: 'flex', alignItems: 'center', minWidth: 'fit-content' }}> <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> <h2 style={{ margin: 0, whiteSpace: 'nowrap' }}></h2>
</div> </div>
<Flex gap={8} wrap="wrap" style={{ flex: '0 1 auto' }}> <Flex gap={8} wrap="wrap" style={{ flex: '0 1 auto' }}>
@@ -249,7 +250,7 @@ export default function WorldSetting() {
<div style={{ flex: 1, overflowY: 'auto' }}> <div style={{ flex: 1, overflowY: 'auto' }}>
<Card <Card
style={{ style={{
...cardStyles.base, ...worldSettingCardStyles.sectionCard,
marginBottom: 16 marginBottom: 16
}} }}
title={ title={
@@ -274,7 +275,7 @@ export default function WorldSetting() {
<Card <Card
style={{ style={{
...cardStyles.base, ...worldSettingCardStyles.sectionCard,
marginBottom: 16 marginBottom: 16
}} }}
title={ title={
@@ -287,16 +288,16 @@ export default function WorldSetting() {
<div style={{ padding: '16px 0' }}> <div style={{ padding: '16px 0' }}>
{currentProject.world_time_period && ( {currentProject.world_time_period && (
<div style={{ marginBottom: 24 }}> <div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: 'var(--color-primary)', marginBottom: 12 }}> <Title level={5} style={{ color: token.colorPrimary, marginBottom: 12 }}>
</Title> </Title>
<Paragraph style={{ <Paragraph style={{
fontSize: 15, fontSize: 15,
lineHeight: 1.8, lineHeight: 1.8,
padding: 16, padding: 16,
background: 'var(--color-bg-layout)', background: token.colorBgLayout,
borderRadius: 8, borderRadius: 8,
borderLeft: '4px solid var(--color-primary)' borderLeft: `4px solid ${token.colorPrimary}`
}}> }}>
{currentProject.world_time_period} {currentProject.world_time_period}
</Paragraph> </Paragraph>
@@ -305,16 +306,16 @@ export default function WorldSetting() {
{currentProject.world_location && ( {currentProject.world_location && (
<div style={{ marginBottom: 24 }}> <div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: 'var(--color-success)', marginBottom: 12 }}> <Title level={5} style={{ color: token.colorSuccess, marginBottom: 12 }}>
</Title> </Title>
<Paragraph style={{ <Paragraph style={{
fontSize: 15, fontSize: 15,
lineHeight: 1.8, lineHeight: 1.8,
padding: 16, padding: 16,
background: 'var(--color-bg-layout)', background: token.colorBgLayout,
borderRadius: 8, borderRadius: 8,
borderLeft: '4px solid var(--color-success)' borderLeft: `4px solid ${token.colorSuccess}`
}}> }}>
{currentProject.world_location} {currentProject.world_location}
</Paragraph> </Paragraph>
@@ -323,16 +324,16 @@ export default function WorldSetting() {
{currentProject.world_atmosphere && ( {currentProject.world_atmosphere && (
<div style={{ marginBottom: 24 }}> <div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: 'var(--color-warning)', marginBottom: 12 }}> <Title level={5} style={{ color: token.colorWarning, marginBottom: 12 }}>
</Title> </Title>
<Paragraph style={{ <Paragraph style={{
fontSize: 15, fontSize: 15,
lineHeight: 1.8, lineHeight: 1.8,
padding: 16, padding: 16,
background: 'var(--color-bg-layout)', background: token.colorBgLayout,
borderRadius: 8, borderRadius: 8,
borderLeft: '4px solid var(--color-warning)' borderLeft: `4px solid ${token.colorWarning}`
}}> }}>
{currentProject.world_atmosphere} {currentProject.world_atmosphere}
</Paragraph> </Paragraph>
@@ -341,16 +342,16 @@ export default function WorldSetting() {
{currentProject.world_rules && ( {currentProject.world_rules && (
<div style={{ marginBottom: 0 }}> <div style={{ marginBottom: 0 }}>
<Title level={5} style={{ color: 'var(--color-error)', marginBottom: 12 }}> <Title level={5} style={{ color: token.colorError, marginBottom: 12 }}>
</Title> </Title>
<Paragraph style={{ <Paragraph style={{
fontSize: 15, fontSize: 15,
lineHeight: 1.8, lineHeight: 1.8,
padding: 16, padding: 16,
background: 'var(--color-bg-layout)', background: token.colorBgLayout,
borderRadius: 8, borderRadius: 8,
borderLeft: '4px solid var(--color-error)' borderLeft: `4px solid ${token.colorError}`
}}> }}>
{currentProject.world_rules} {currentProject.world_rules}
</Paragraph> </Paragraph>
@@ -616,71 +617,71 @@ export default function WorldSetting() {
> >
{newWorldData && ( {newWorldData && (
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}> <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 type="warning" strong>
"确认替换" "确认替换"
</Typography.Text> </Typography.Text>
</div> </div>
<div style={{ marginBottom: 24 }}> <div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: 'var(--color-primary)', marginBottom: 12 }}> <Title level={5} style={{ color: token.colorPrimary, marginBottom: 12 }}>
</Title> </Title>
<Paragraph style={{ <Paragraph style={{
fontSize: 15, fontSize: 15,
lineHeight: 1.8, lineHeight: 1.8,
padding: 16, padding: 16,
background: '#f5f5f5', background: token.colorBgLayout,
borderRadius: 8, borderRadius: 8,
borderLeft: '4px solid #1890ff' borderLeft: `4px solid ${token.colorPrimary}`
}}> }}>
{newWorldData.time_period} {newWorldData.time_period}
</Paragraph> </Paragraph>
</div> </div>
<div style={{ marginBottom: 24 }}> <div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: '#52c41a', marginBottom: 12 }}> <Title level={5} style={{ color: token.colorSuccess, marginBottom: 12 }}>
</Title> </Title>
<Paragraph style={{ <Paragraph style={{
fontSize: 15, fontSize: 15,
lineHeight: 1.8, lineHeight: 1.8,
padding: 16, padding: 16,
background: '#f5f5f5', background: token.colorBgLayout,
borderRadius: 8, borderRadius: 8,
borderLeft: '4px solid #52c41a' borderLeft: `4px solid ${token.colorSuccess}`
}}> }}>
{newWorldData.location} {newWorldData.location}
</Paragraph> </Paragraph>
</div> </div>
<div style={{ marginBottom: 24 }}> <div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: '#faad14', marginBottom: 12 }}> <Title level={5} style={{ color: token.colorWarning, marginBottom: 12 }}>
</Title> </Title>
<Paragraph style={{ <Paragraph style={{
fontSize: 15, fontSize: 15,
lineHeight: 1.8, lineHeight: 1.8,
padding: 16, padding: 16,
background: '#f5f5f5', background: token.colorBgLayout,
borderRadius: 8, borderRadius: 8,
borderLeft: '4px solid #faad14' borderLeft: `4px solid ${token.colorWarning}`
}}> }}>
{newWorldData.atmosphere} {newWorldData.atmosphere}
</Paragraph> </Paragraph>
</div> </div>
<div style={{ marginBottom: 0 }}> <div style={{ marginBottom: 0 }}>
<Title level={5} style={{ color: '#f5222d', marginBottom: 12 }}> <Title level={5} style={{ color: token.colorError, marginBottom: 12 }}>
</Title> </Title>
<Paragraph style={{ <Paragraph style={{
fontSize: 15, fontSize: 15,
lineHeight: 1.8, lineHeight: 1.8,
padding: 16, padding: 16,
background: '#f5f5f5', background: token.colorBgLayout,
borderRadius: 8, borderRadius: 8,
borderLeft: '4px solid #f5222d' borderLeft: `4px solid ${token.colorError}`
}}> }}>
{newWorldData.rules} {newWorldData.rules}
</Paragraph> </Paragraph>
+10 -7
View File
@@ -13,6 +13,7 @@ import {
Typography, Typography,
Row, Row,
Col, Col,
theme,
} from 'antd'; } from 'antd';
import { import {
PlusOutlined, PlusOutlined,
@@ -38,6 +39,8 @@ export default function WritingStyles() {
const [createForm] = Form.useForm(); const [createForm] = Form.useForm();
const [editForm] = Form.useForm(); const [editForm] = Form.useForm();
const { token } = theme.useToken();
const isMobile = window.innerWidth <= 768; const isMobile = window.innerWidth <= 768;
// 卡片网格配置 // 卡片网格配置
@@ -170,10 +173,10 @@ export default function WritingStyles() {
position: 'sticky', position: 'sticky',
top: 0, top: 0,
zIndex: 10, zIndex: 10,
backgroundColor: '#fff', backgroundColor: token.colorBgContainer,
padding: isMobile ? '12px 0' : '16px 0', padding: isMobile ? '12px 0' : '16px 0',
marginBottom: isMobile ? 12 : 16, marginBottom: isMobile ? 12 : 16,
borderBottom: '1px solid #f0f0f0', borderBottom: `1px solid ${token.colorBorderSecondary}`,
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
@@ -220,7 +223,7 @@ export default function WritingStyles() {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
borderRadius: 12, 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={{ bodyStyle={{
flex: 1, flex: 1,
@@ -235,7 +238,7 @@ export default function WritingStyles() {
style={{ cursor: style.is_default ? 'default' : 'pointer' }} style={{ cursor: style.is_default ? 'default' : 'pointer' }}
> >
{style.is_default ? ( {style.is_default ? (
<StarFilled style={{ color: '#faad14', fontSize: 18 }} /> <StarFilled style={{ color: token.colorWarning, fontSize: 18 }} />
) : ( ) : (
<StarOutlined style={{ fontSize: 18 }} /> <StarOutlined style={{ fontSize: 18 }} />
)} )}
@@ -246,7 +249,7 @@ export default function WritingStyles() {
style={{ style={{
fontSize: 18, fontSize: 18,
cursor: style.user_id === null ? 'not-allowed' : 'pointer', cursor: style.user_id === null ? 'not-allowed' : 'pointer',
color: style.user_id === null ? '#ccc' : undefined color: style.user_id === null ? token.colorTextQuaternary : undefined
}} }}
/>, />,
<Popconfirm <Popconfirm
@@ -261,7 +264,7 @@ export default function WritingStyles() {
<DeleteOutlined <DeleteOutlined
style={{ style={{
fontSize: 18, 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' cursor: style.user_id === null ? 'not-allowed' : 'pointer'
}} }}
/> />
@@ -292,7 +295,7 @@ export default function WritingStyles() {
style={{ style={{
fontSize: 12, fontSize: 12,
marginBottom: 0, marginBottom: 0,
backgroundColor: '#fafafa', backgroundColor: token.colorFillAlter,
padding: 8, padding: 8,
borderRadius: 4, borderRadius: 4,
flex: 1, flex: 1,