refactor: 大量前端页面/组件样式从硬编码颜色迁移到 antd token 主题变量
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo, useEffect, useRef } from 'react';
|
||||
import { theme } from 'antd';
|
||||
|
||||
// 标注数据类型
|
||||
export interface MemoryAnnotation {
|
||||
@@ -35,14 +36,6 @@ interface AnnotatedTextProps {
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
// 类型颜色映射
|
||||
const TYPE_COLORS = {
|
||||
hook: '#ff6b6b',
|
||||
foreshadow: '#6b7bff',
|
||||
plot_point: '#51cf66',
|
||||
character_event: '#ffd93d',
|
||||
};
|
||||
|
||||
// 类型图标映射
|
||||
const TYPE_ICONS = {
|
||||
hook: '🎣',
|
||||
@@ -65,6 +58,14 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
|
||||
}) => {
|
||||
const annotationRefs = useRef<Record<string, HTMLSpanElement | null>>({});
|
||||
|
||||
const { token } = theme.useToken();
|
||||
const typeColors: Record<MemoryAnnotation['type'], string> = {
|
||||
hook: token.colorError,
|
||||
foreshadow: token.colorInfo,
|
||||
plot_point: token.colorSuccess,
|
||||
character_event: token.colorWarning,
|
||||
};
|
||||
|
||||
// 当需要滚动到特定标注时
|
||||
useEffect(() => {
|
||||
if (scrollToAnnotation && annotationRefs.current[scrollToAnnotation]) {
|
||||
@@ -214,7 +215,7 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
|
||||
const { annotation, annotations } = segment;
|
||||
if (!annotation) return null;
|
||||
|
||||
const color = TYPE_COLORS[annotation.type];
|
||||
const color = typeColors[annotation.type];
|
||||
const icon = TYPE_ICONS[annotation.type];
|
||||
const isActive = activeAnnotationId === annotation.id;
|
||||
|
||||
@@ -238,17 +239,17 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
|
||||
position: 'relative',
|
||||
borderBottom: `2px solid ${color}`,
|
||||
cursor: 'pointer',
|
||||
backgroundColor: isActive ? `${color}22` : 'transparent',
|
||||
backgroundColor: isActive ? `color-mix(in srgb, ${color} 13%, transparent)` : 'transparent',
|
||||
transition: 'all 0.2s',
|
||||
padding: '2px 0',
|
||||
}}
|
||||
onClick={() => onAnnotationClick?.(annotation)}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = `${color}33`;
|
||||
e.currentTarget.style.backgroundColor = `color-mix(in srgb, ${color} 20%, transparent)`;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = isActive
|
||||
? `${color}22`
|
||||
? `color-mix(in srgb, ${color} 13%, transparent)`
|
||||
: 'transparent';
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Modal, Button, Space } from 'antd';
|
||||
import { Modal, Button, Space, theme } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface AnnouncementModalProps {
|
||||
@@ -11,6 +11,8 @@ interface AnnouncementModalProps {
|
||||
export default function AnnouncementModal({ visible, onClose, onDoNotShowToday, onNeverShow }: AnnouncementModalProps) {
|
||||
const [qqImageError, setQqImageError] = useState(false);
|
||||
const [wxImageError, setWxImageError] = useState(false);
|
||||
const { token } = theme.useToken();
|
||||
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
@@ -35,7 +37,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
<div style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--color-primary)',
|
||||
color: token.colorPrimary,
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
🎉 欢迎使用 AI小说创作助手
|
||||
@@ -64,9 +66,9 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
borderRadius: '8px',
|
||||
height: '40px',
|
||||
fontSize: '14px',
|
||||
background: 'var(--color-primary)',
|
||||
borderColor: 'var(--color-primary)',
|
||||
boxShadow: 'var(--shadow-primary)',
|
||||
background: token.colorPrimary,
|
||||
borderColor: token.colorPrimary,
|
||||
boxShadow: `0 8px 20px ${alphaColor(token.colorPrimary, 0.32)}`,
|
||||
}}
|
||||
>
|
||||
永不再展示
|
||||
@@ -78,16 +80,16 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
styles={{
|
||||
body: {
|
||||
padding: '20px',
|
||||
background: 'var(--color-bg-container)',
|
||||
background: token.colorBgContainer,
|
||||
},
|
||||
header: {
|
||||
background: 'linear-gradient(135deg, rgba(77, 128, 136, 0.08) 0%, rgba(248, 246, 241, 0.95) 100%)',
|
||||
borderBottom: '1px solid var(--color-border-secondary)',
|
||||
background: `linear-gradient(135deg, ${alphaColor(token.colorPrimary, 0.1)} 0%, ${alphaColor(token.colorBgContainer, 0.98)} 100%)`,
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
padding: '16px 24px',
|
||||
},
|
||||
footer: {
|
||||
background: 'var(--color-bg-container)',
|
||||
borderTop: '1px solid var(--color-border-secondary)',
|
||||
background: token.colorBgContainer,
|
||||
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||||
padding: '16px 24px',
|
||||
},
|
||||
}}
|
||||
@@ -96,7 +98,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
<div style={{
|
||||
marginBottom: '12px',
|
||||
fontSize: '15px',
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
lineHeight: '1.5',
|
||||
}}>
|
||||
<p style={{ marginBottom: '8px' }}>👋 欢迎加入我们的交流群!在这里你可以:</p>
|
||||
@@ -111,7 +113,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
<li>🐛 反馈问题和建议</li>
|
||||
<li>📚 分享创作经验和灵感</li>
|
||||
</ul>
|
||||
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '12px' }}>
|
||||
<p style={{ fontWeight: 600, color: token.colorText, marginBottom: '12px' }}>
|
||||
扫描下方二维码加入交流群:
|
||||
</p>
|
||||
</div>
|
||||
@@ -122,7 +124,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
alignItems: 'flex-start',
|
||||
gap: '24px',
|
||||
padding: '16px',
|
||||
background: 'var(--color-bg-layout)',
|
||||
background: token.colorBgLayout,
|
||||
borderRadius: '8px',
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
@@ -133,7 +135,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
alignItems: 'center',
|
||||
minWidth: '200px',
|
||||
}}>
|
||||
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '8px', fontSize: '14px' }}>
|
||||
<p style={{ fontWeight: 600, color: token.colorText, marginBottom: '8px', fontSize: '14px' }}>
|
||||
QQ交流群
|
||||
</p>
|
||||
{!qqImageError ? (
|
||||
@@ -141,10 +143,10 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'var(--color-bg-container)',
|
||||
background: token.colorBgContainer,
|
||||
borderRadius: '8px',
|
||||
padding: '6px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
boxShadow: `0 2px 8px ${alphaColor(token.colorText, 0.12)}`,
|
||||
}}>
|
||||
<img
|
||||
src="/qq.jpg"
|
||||
@@ -167,9 +169,9 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'var(--color-bg-container)',
|
||||
background: token.colorBgContainer,
|
||||
borderRadius: '8px',
|
||||
color: '#999',
|
||||
color: token.colorTextTertiary,
|
||||
}}>
|
||||
<p>二维码加载失败</p>
|
||||
</div>
|
||||
@@ -183,7 +185,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
alignItems: 'center',
|
||||
minWidth: '200px',
|
||||
}}>
|
||||
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '8px', fontSize: '14px' }}>
|
||||
<p style={{ fontWeight: 600, color: token.colorText, marginBottom: '8px', fontSize: '14px' }}>
|
||||
微信交流群
|
||||
</p>
|
||||
{!wxImageError ? (
|
||||
@@ -191,10 +193,10 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'var(--color-bg-container)',
|
||||
background: token.colorBgContainer,
|
||||
borderRadius: '8px',
|
||||
padding: '6px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
boxShadow: `0 2px 8px ${alphaColor(token.colorText, 0.12)}`,
|
||||
}}>
|
||||
<img
|
||||
src="/WX.png"
|
||||
@@ -217,9 +219,9 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'var(--color-bg-container)',
|
||||
background: token.colorBgContainer,
|
||||
borderRadius: '8px',
|
||||
color: '#999',
|
||||
color: token.colorTextTertiary,
|
||||
}}>
|
||||
<p>二维码加载失败</p>
|
||||
</div>
|
||||
@@ -230,11 +232,11 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
||||
<div style={{
|
||||
marginTop: '16px',
|
||||
padding: '10px',
|
||||
background: 'var(--color-warning-bg)',
|
||||
background: token.colorWarningBg,
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--color-warning-border)',
|
||||
border: `1px solid ${token.colorWarningBorder}`,
|
||||
fontSize: '13px',
|
||||
color: 'var(--color-warning)',
|
||||
color: token.colorWarning,
|
||||
}}>
|
||||
💡 提示:选择"今日内不再展示"当天不再显示,选择"永不再展示"将永久隐藏此公告
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Typography, Space, Divider, Badge, Button, Grid } from 'antd';
|
||||
import { Typography, Space, Divider, Badge, Button, Grid, theme } from 'antd';
|
||||
import { GithubOutlined, CopyrightOutlined, HeartFilled, ClockCircleOutlined, GiftOutlined } from '@ant-design/icons';
|
||||
import { VERSION_INFO, getVersionString } from '../config/version';
|
||||
import { checkLatestVersion } from '../services/versionService';
|
||||
@@ -17,6 +17,8 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
const [hasUpdate, setHasUpdate] = useState(false);
|
||||
const [latestVersion, setLatestVersion] = useState('');
|
||||
const [releaseUrl, setReleaseUrl] = useState('');
|
||||
const { token } = theme.useToken();
|
||||
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||
|
||||
useEffect(() => {
|
||||
// 检查版本更新(每次都重新检查)
|
||||
@@ -55,11 +57,11 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
right: 0,
|
||||
backdropFilter: 'blur(20px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(20px) saturate(180%)',
|
||||
borderTop: '1px solid var(--color-border)',
|
||||
borderTop: `1px solid ${token.colorBorder}`,
|
||||
padding: isMobile ? '8px 12px' : '10px 16px',
|
||||
zIndex: 100,
|
||||
boxShadow: 'var(--shadow-card)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)', // 半透明背景以支持 backdrop-filter
|
||||
boxShadow: `0 -2px 16px ${alphaColor(token.colorText, 0.08)}`,
|
||||
backgroundColor: alphaColor(token.colorBgContainer, 0.82), // 半透明背景以支持 backdrop-filter
|
||||
transition: 'left 0.3s ease', // 平滑过渡
|
||||
}}
|
||||
>
|
||||
@@ -87,23 +89,23 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
color: 'var(--color-primary)',
|
||||
color: token.colorPrimary,
|
||||
cursor: hasUpdate ? 'pointer' : 'default',
|
||||
}}
|
||||
title={hasUpdate ? `发现新版本 v${latestVersion},点击查看` : '当前版本'}
|
||||
>
|
||||
<strong style={{ color: 'var(--color-text-primary)' }}>{VERSION_INFO.projectName}</strong>
|
||||
<strong style={{ color: token.colorText }}>{VERSION_INFO.projectName}</strong>
|
||||
<span>{getVersionString()}</span>
|
||||
</Text>
|
||||
</Badge>
|
||||
<Divider type="vertical" style={{ margin: '0 4px', borderColor: 'var(--color-border)' }} />
|
||||
<Divider type="vertical" style={{ margin: '0 4px', borderColor: token.colorBorder }} />
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<GiftOutlined />}
|
||||
onClick={() => window.open('https://mumuverse.space:1588/', '_blank')}
|
||||
style={{
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
fontSize: 11,
|
||||
height: 24,
|
||||
padding: '0 4px',
|
||||
@@ -114,7 +116,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
>
|
||||
赞助
|
||||
</Button>
|
||||
<Divider type="vertical" style={{ margin: '0 4px', borderColor: 'var(--color-border)' }} />
|
||||
<Divider type="vertical" style={{ margin: '0 4px', borderColor: token.colorBorder }} />
|
||||
<Link
|
||||
href={VERSION_INFO.githubUrl}
|
||||
target="_blank"
|
||||
@@ -124,7 +126,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
}}
|
||||
>
|
||||
<GithubOutlined style={{ fontSize: 12 }} />
|
||||
@@ -132,7 +134,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: 'var(--color-text-tertiary)',
|
||||
color: token.colorTextTertiary,
|
||||
}}
|
||||
>
|
||||
<ClockCircleOutlined style={{ fontSize: 10, marginRight: 4 }} />
|
||||
@@ -144,7 +146,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
<Space
|
||||
direction="horizontal"
|
||||
size={12}
|
||||
split={<Divider type="vertical" style={{ borderColor: 'var(--color-border)' }} />}
|
||||
split={<Divider type="vertical" style={{ borderColor: token.colorBorder }} />}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
@@ -160,7 +162,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
textShadow: 'none',
|
||||
cursor: hasUpdate ? 'pointer' : 'default',
|
||||
transition: 'all 0.3s',
|
||||
@@ -177,7 +179,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
}}
|
||||
title={hasUpdate ? `发现新版本 v${latestVersion},点击查看` : '当前版本'}
|
||||
>
|
||||
<strong style={{ color: 'var(--color-text-primary)' }}>{VERSION_INFO.projectName}</strong>
|
||||
<strong style={{ color: token.colorText }}>{VERSION_INFO.projectName}</strong>
|
||||
<span>{getVersionString()}</span>
|
||||
</Text>
|
||||
</Badge>
|
||||
@@ -192,7 +194,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
}}
|
||||
>
|
||||
<GithubOutlined style={{ fontSize: 13 }} />
|
||||
@@ -206,7 +208,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
}}
|
||||
>
|
||||
LinuxDO 社区
|
||||
@@ -218,9 +220,9 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
icon={<GiftOutlined style={{ fontSize: 14 }} />}
|
||||
onClick={() => window.open('https://mumuverse.space:1588/', '_blank')}
|
||||
style={{
|
||||
background: 'var(--color-primary)',
|
||||
background: token.colorPrimary,
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 12px rgba(77, 128, 136, 0.3)',
|
||||
boxShadow: `0 4px 12px ${alphaColor(token.colorPrimary, 0.35)}`,
|
||||
fontSize: 13,
|
||||
height: 32,
|
||||
padding: '0 20px',
|
||||
@@ -232,11 +234,11 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
e.currentTarget.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.6)';
|
||||
e.currentTarget.style.boxShadow = `0 6px 16px ${alphaColor(token.colorPrimary, 0.5)}`;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.5)';
|
||||
e.currentTarget.style.boxShadow = `0 4px 12px ${alphaColor(token.colorPrimary, 0.35)}`;
|
||||
}}
|
||||
>
|
||||
赞助支持
|
||||
@@ -252,7 +254,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
}}
|
||||
>
|
||||
<CopyrightOutlined style={{ fontSize: 11 }} />
|
||||
@@ -266,7 +268,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
color: 'var(--color-text-tertiary)',
|
||||
color: token.colorTextTertiary,
|
||||
}}
|
||||
>
|
||||
<ClockCircleOutlined style={{ fontSize: 12 }} />
|
||||
@@ -280,12 +282,12 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
color: 'var(--color-text-secondary)',
|
||||
textShadow: '0 1px 3px rgba(0, 0, 0, 0.05)',
|
||||
color: token.colorTextSecondary,
|
||||
textShadow: `0 1px 3px ${alphaColor(token.colorText, 0.08)}`,
|
||||
}}
|
||||
>
|
||||
<span>Made with</span>
|
||||
<HeartFilled style={{ color: 'var(--color-error)', fontSize: 11 }} />
|
||||
<HeartFilled style={{ color: token.colorError, fontSize: 11 }} />
|
||||
<span>by {VERSION_INFO.author}</span>
|
||||
</Text>
|
||||
</Space>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Modal, Spin, Alert, Tabs, Card, Tag, List, Empty, Statistic, Row, Col, Button } from 'antd';
|
||||
import { Modal, Spin, Alert, Tabs, Card, Tag, List, Empty, Statistic, Row, Col, Button, theme } from 'antd';
|
||||
import {
|
||||
ThunderboltOutlined,
|
||||
BulbOutlined,
|
||||
@@ -27,6 +27,7 @@ interface ChapterAnalysisProps {
|
||||
}
|
||||
|
||||
export default function ChapterAnalysis({ chapterId, visible, onClose }: ChapterAnalysisProps) {
|
||||
const { token } = theme.useToken();
|
||||
const [task, setTask] = useState<AnalysisTask | null>(null);
|
||||
const [analysis, setAnalysis] = useState<ChapterAnalysisResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -258,7 +259,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
transition: 'all 0.3s ease',
|
||||
borderRadius: 6,
|
||||
boxShadow: task.progress > 0 && task.status !== 'failed'
|
||||
? '0 0 10px rgba(24, 144, 255, 0.3)'
|
||||
? `0 0 10px color-mix(in srgb, ${token.colorPrimary} 30%, transparent)`
|
||||
: 'none'
|
||||
}} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, Button, Card, Statistic, Row, Col, message } from 'antd';
|
||||
import { Modal, Button, Card, Statistic, Row, Col, message, theme } from 'antd';
|
||||
import { CheckOutlined, CloseOutlined, SwapOutlined } from '@ant-design/icons';
|
||||
import ReactDiffViewer from 'react-diff-viewer-continued';
|
||||
|
||||
@@ -26,6 +26,7 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
|
||||
onApply,
|
||||
onDiscard
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
const [applying, setApplying] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<'split' | 'unified'>('split');
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
@@ -195,7 +196,7 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
|
||||
styles={{
|
||||
variables: {
|
||||
light: {
|
||||
diffViewerBackground: '#fff', // Keep white for diff viewer readability
|
||||
diffViewerBackground: token.colorBgContainer,
|
||||
addedBackground: 'var(--color-success-bg)',
|
||||
addedColor: 'var(--color-text-primary)',
|
||||
removedBackground: 'var(--color-error-bg)',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 提供沉浸式阅读体验,支持主题切换、字体调节、翻页导航等功能
|
||||
*/
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Modal, Button, Slider, Radio, Space, Typography, Spin, message } from 'antd';
|
||||
import { Modal, Button, Slider, Radio, Space, Typography, Spin, message, theme } from 'antd';
|
||||
import {
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
@@ -37,27 +37,12 @@ interface NavigationInfo {
|
||||
current: { id: string; chapter_number: number; title: string };
|
||||
}
|
||||
|
||||
// 主题样式配置
|
||||
const themeStyles = {
|
||||
light: {
|
||||
bg: '#ffffff',
|
||||
text: '#333333',
|
||||
headerBg: '#fafafa',
|
||||
border: '#e8e8e8'
|
||||
},
|
||||
sepia: {
|
||||
bg: '#f5e6c8',
|
||||
text: '#5b4636',
|
||||
headerBg: '#e8d9b8',
|
||||
border: '#d4c5a5'
|
||||
},
|
||||
dark: {
|
||||
bg: '#1a1a1a',
|
||||
text: '#cccccc',
|
||||
headerBg: '#252525',
|
||||
border: '#333333'
|
||||
}
|
||||
};
|
||||
interface ReaderThemeStyle {
|
||||
bg: string;
|
||||
text: string;
|
||||
headerBg: string;
|
||||
border: string;
|
||||
}
|
||||
|
||||
// 本地存储key
|
||||
const SETTINGS_STORAGE_KEY = 'chapter-reader-settings';
|
||||
@@ -94,6 +79,8 @@ export default function ChapterReader({
|
||||
onClose,
|
||||
onChapterChange
|
||||
}: ChapterReaderProps) {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
// 阅读器设置
|
||||
const [settings, setSettings] = useState<ReaderSettings>(loadSettings);
|
||||
|
||||
@@ -200,6 +187,26 @@ export default function ChapterReader({
|
||||
}, [chapter?.id]);
|
||||
|
||||
// 当前主题样式
|
||||
const themeStyles: Record<ReaderSettings['theme'], ReaderThemeStyle> = {
|
||||
light: {
|
||||
bg: token.colorBgContainer,
|
||||
text: token.colorText,
|
||||
headerBg: token.colorBgElevated,
|
||||
border: token.colorBorderSecondary,
|
||||
},
|
||||
sepia: {
|
||||
bg: `color-mix(in srgb, ${token.colorWarningBg} 72%, ${token.colorBgContainer} 28%)`,
|
||||
text: `color-mix(in srgb, ${token.colorText} 85%, ${token.colorTextSecondary} 15%)`,
|
||||
headerBg: `color-mix(in srgb, ${token.colorWarningBg} 58%, ${token.colorBgElevated} 42%)`,
|
||||
border: `color-mix(in srgb, ${token.colorWarningBorder} 65%, ${token.colorBorder} 35%)`,
|
||||
},
|
||||
dark: {
|
||||
bg: `color-mix(in srgb, ${token.colorTextBase} 92%, ${token.colorBgContainer} 8%)`,
|
||||
text: `color-mix(in srgb, ${token.colorTextLightSolid} 82%, ${token.colorTextSecondary} 18%)`,
|
||||
headerBg: `color-mix(in srgb, ${token.colorTextBase} 84%, ${token.colorBgElevated} 16%)`,
|
||||
border: `color-mix(in srgb, ${token.colorTextBase} 60%, ${token.colorBorder} 40%)`,
|
||||
},
|
||||
};
|
||||
const currentTheme = themeStyles[settings.theme];
|
||||
|
||||
// 更新设置的便捷函数
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Card, Space, Tag, Typography, Popconfirm } from 'antd';
|
||||
import { Card, Space, Tag, Typography, Popconfirm, theme } from 'antd';
|
||||
import { EditOutlined, DeleteOutlined, UserOutlined, BankOutlined, ExportOutlined } from '@ant-design/icons';
|
||||
import { cardStyles } from './CardStyles';
|
||||
import { characterCardStyles } from './CardStyles';
|
||||
import type { Character } from '../types';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
@@ -13,6 +13,8 @@ interface CharacterCardProps {
|
||||
}
|
||||
|
||||
export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit, onDelete, onExport }) => {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const getRoleTypeColor = (roleType?: string) => {
|
||||
const roleColors: Record<string, string> = {
|
||||
'protagonist': 'blue',
|
||||
@@ -37,10 +39,10 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
|
||||
|
||||
const getStatusTag = () => {
|
||||
const statusConfig: Record<string, { color: string; label: string }> = {
|
||||
deceased: { color: '#000000', label: '💀 已死亡' },
|
||||
missing: { color: '#faad14', label: '❓ 已失踪' },
|
||||
retired: { color: '#8c8c8c', label: '📤 已退场' },
|
||||
destroyed: { color: '#000000', label: '💀 已覆灭' },
|
||||
deceased: { color: token.colorTextBase, label: '💀 已死亡' },
|
||||
missing: { color: token.colorWarning, label: '❓ 已失踪' },
|
||||
retired: { color: token.colorTextTertiary, label: '📤 已退场' },
|
||||
destroyed: { color: token.colorTextBase, label: '💀 已覆灭' },
|
||||
};
|
||||
const config = statusConfig[charStatus];
|
||||
if (!config) return null;
|
||||
@@ -51,7 +53,7 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
|
||||
<Card
|
||||
hoverable
|
||||
style={{
|
||||
...(isOrganization ? cardStyles.organization : cardStyles.character),
|
||||
...(isOrganization ? characterCardStyles.organizationCard : characterCardStyles.characterCard),
|
||||
...(isInactive ? { opacity: 0.6, filter: 'grayscale(40%)' } : {}),
|
||||
}}
|
||||
styles={{
|
||||
@@ -82,14 +84,14 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
|
||||
<Card.Meta
|
||||
avatar={
|
||||
isOrganization ? (
|
||||
<BankOutlined style={{ fontSize: 32, color: '#52c41a' }} />
|
||||
<BankOutlined style={{ fontSize: 32, color: token.colorSuccess }} />
|
||||
) : (
|
||||
<UserOutlined style={{ fontSize: 32, color: '#1890ff' }} />
|
||||
<UserOutlined style={{ fontSize: 32, color: token.colorPrimary }} />
|
||||
)
|
||||
}
|
||||
title={
|
||||
<Space>
|
||||
<span style={cardStyles.ellipsis}>{character.name}</span>
|
||||
<span style={characterCardStyles.nameEllipsis}>{character.name}</span>
|
||||
{isOrganization ? (
|
||||
<Tag color="green">组织</Tag>
|
||||
) : (
|
||||
@@ -103,7 +105,7 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<div style={cardStyles.description}>
|
||||
<div style={characterCardStyles.descriptionBlock}>
|
||||
{/* 角色特有字段 */}
|
||||
{!isOrganization && (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, Button, Modal, Form, Select, InputNumber, Input, message, Progress, Tag, Space, Divider, Typography } from 'antd';
|
||||
import { Card, Button, Modal, Form, Select, InputNumber, Input, message, Progress, Tag, Space, Divider, Typography, theme } from 'antd';
|
||||
import { EditOutlined, PlusOutlined, DeleteOutlined, TrophyOutlined } from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
|
||||
@@ -44,6 +44,7 @@ export const CharacterCareerCard: React.FC<Props> = ({
|
||||
editable = false,
|
||||
onUpdate
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
const [mainCareer, setMainCareer] = useState<CareerDetail | null>(null);
|
||||
const [subCareers, setSubCareers] = useState<CareerDetail[]>([]);
|
||||
const [allCareers, setAllCareers] = useState<Career[]>([]);
|
||||
@@ -190,7 +191,7 @@ export const CharacterCareerCard: React.FC<Props> = ({
|
||||
<div key={career.id} style={{ marginBottom: 16 }}>
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<Space>
|
||||
<TrophyOutlined style={{ color: isMain ? '#1890ff' : '#8c8c8c' }} />
|
||||
<TrophyOutlined style={{ color: isMain ? token.colorPrimary : token.colorTextTertiary }} />
|
||||
<Text strong={isMain}>{career.career_name}</Text>
|
||||
{isMain && <Tag color="blue">主</Tag>}
|
||||
</Space>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Drawer, Input, List, Typography, Empty, Tag } from 'antd';
|
||||
import { Drawer, Input, List, Typography, Empty, Tag, theme } from 'antd';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import type { Chapter } from '../types';
|
||||
|
||||
@@ -24,6 +24,7 @@ export default function FloatingIndexPanel({
|
||||
groupedChapters,
|
||||
onChapterSelect,
|
||||
}: FloatingIndexPanelProps) {
|
||||
const { token } = theme.useToken();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const filteredGroups = useMemo(() => {
|
||||
@@ -56,7 +57,7 @@ export default function FloatingIndexPanel({
|
||||
body: { padding: 0 },
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '16px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ padding: '16px', borderBottom: `1px solid ${token.colorBorderSecondary}` }}>
|
||||
<Input
|
||||
placeholder="搜索章节标题"
|
||||
prefix={<SearchOutlined />}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo, useEffect, useRef } from 'react';
|
||||
import { Card, Tag, Badge, Empty, Collapse, Divider } from 'antd';
|
||||
import { Card, Tag, Badge, Empty, Collapse, Divider, theme } from 'antd';
|
||||
import {
|
||||
FireOutlined,
|
||||
StarOutlined,
|
||||
@@ -22,22 +22,18 @@ const TYPE_CONFIG = {
|
||||
hook: {
|
||||
label: '钩子',
|
||||
icon: <FireOutlined />,
|
||||
color: '#ff6b6b',
|
||||
},
|
||||
foreshadow: {
|
||||
label: '伏笔',
|
||||
icon: <StarOutlined />,
|
||||
color: '#6b7bff',
|
||||
},
|
||||
plot_point: {
|
||||
label: '情节点',
|
||||
icon: <ThunderboltOutlined />,
|
||||
color: '#51cf66',
|
||||
},
|
||||
character_event: {
|
||||
label: '角色事件',
|
||||
icon: <UserOutlined />,
|
||||
color: '#ffd93d',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -51,7 +47,14 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
||||
onAnnotationClick,
|
||||
scrollToAnnotation,
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
const cardRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
const typeColors: Record<keyof typeof TYPE_CONFIG, string> = {
|
||||
hook: token.colorError,
|
||||
foreshadow: token.colorInfo,
|
||||
plot_point: token.colorSuccess,
|
||||
character_event: token.colorWarning,
|
||||
};
|
||||
|
||||
// 当需要滚动到特定标注卡片时
|
||||
useEffect(() => {
|
||||
@@ -100,6 +103,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
||||
// 渲染单个记忆卡片
|
||||
const renderMemoryCard = (annotation: MemoryAnnotation) => {
|
||||
const config = TYPE_CONFIG[annotation.type];
|
||||
const color = typeColors[annotation.type];
|
||||
const isActive = activeAnnotationId === annotation.id;
|
||||
|
||||
return (
|
||||
@@ -115,8 +119,8 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
||||
onClick={() => onAnnotationClick?.(annotation)}
|
||||
style={{
|
||||
marginBottom: 12,
|
||||
borderLeft: `4px solid ${config.color}`,
|
||||
backgroundColor: isActive ? `${config.color}11` : 'transparent',
|
||||
borderLeft: `4px solid ${color}`,
|
||||
backgroundColor: isActive ? `color-mix(in srgb, ${color} 8%, transparent)` : 'transparent',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
@@ -126,7 +130,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
||||
<Badge
|
||||
count={`${(annotation.importance * 10).toFixed(1)}`}
|
||||
style={{
|
||||
backgroundColor: config.color,
|
||||
backgroundColor: color,
|
||||
float: 'right',
|
||||
}}
|
||||
/>
|
||||
@@ -138,7 +142,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: '#666',
|
||||
color: token.colorTextSecondary,
|
||||
lineHeight: 1.6,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
@@ -160,7 +164,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
||||
|
||||
{/* 特殊元数据 */}
|
||||
{annotation.metadata.strength && (
|
||||
<div style={{ marginTop: 4, fontSize: 11, color: '#999' }}>
|
||||
<div style={{ marginTop: 4, fontSize: 11, color: token.colorTextTertiary }}>
|
||||
强度: {annotation.metadata.strength}/10
|
||||
</div>
|
||||
)}
|
||||
@@ -192,27 +196,27 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
||||
<div style={{ fontWeight: 600, marginBottom: 12 }}>📊 分析概览</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>钩子</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600, color: TYPE_CONFIG.hook.color }}>
|
||||
<div style={{ fontSize: 12, color: token.colorTextTertiary }}>钩子</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600, color: typeColors.hook }}>
|
||||
{stats.hooks}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>伏笔</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600, color: TYPE_CONFIG.foreshadow.color }}>
|
||||
<div style={{ fontSize: 12, color: token.colorTextTertiary }}>伏笔</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600, color: typeColors.foreshadow }}>
|
||||
{stats.foreshadows}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>情节点</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600, color: TYPE_CONFIG.plot_point.color }}>
|
||||
<div style={{ fontSize: 12, color: token.colorTextTertiary }}>情节点</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600, color: typeColors.plot_point }}>
|
||||
{stats.plotPoints}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>角色事件</div>
|
||||
<div style={{ fontSize: 12, color: token.colorTextTertiary }}>角色事件</div>
|
||||
<div
|
||||
style={{ fontSize: 20, fontWeight: 600, color: TYPE_CONFIG.character_event.color }}
|
||||
style={{ fontSize: 20, fontWeight: 600, color: typeColors.character_event }}
|
||||
>
|
||||
{stats.characterEvents}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Modal, Input, Button, Space, Radio, InputNumber, Card, message, Alert, Spin, Typography, Divider } from 'antd';
|
||||
import { Modal, Input, Button, Space, Radio, InputNumber, Card, message, Alert, Spin, Typography, Divider, theme } from 'antd';
|
||||
import { ThunderboltOutlined, CheckOutlined, ReloadOutlined, EditOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import { chapterApi } from '../services/api';
|
||||
|
||||
@@ -33,6 +33,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
onClose,
|
||||
onApply,
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
const [userInstructions, setUserInstructions] = useState('');
|
||||
const [lengthMode, setLengthMode] = useState<LengthMode>('similar');
|
||||
const [customWordCount, setCustomWordCount] = useState<number>(selectedText.length);
|
||||
@@ -178,7 +179,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<EditOutlined style={{ color: 'var(--color-primary)' }} />
|
||||
<EditOutlined style={{ color: token.colorPrimary }} />
|
||||
<span>AI局部重写</span>
|
||||
</Space>
|
||||
}
|
||||
@@ -202,9 +203,9 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
loading={isGenerating}
|
||||
disabled={!userInstructions.trim()}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
||||
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 12px rgba(77, 128, 136, 0.3)',
|
||||
boxShadow: token.boxShadowSecondary,
|
||||
}}
|
||||
>
|
||||
{isGenerating ? '生成中...' : '开始重写'}
|
||||
@@ -221,7 +222,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
type="primary"
|
||||
icon={<CheckOutlined />}
|
||||
onClick={handleAccept}
|
||||
style={{ background: '#52c41a', borderColor: '#52c41a' }}
|
||||
style={{ background: token.colorSuccess, borderColor: token.colorSuccess }}
|
||||
>
|
||||
接受并应用
|
||||
</Button>
|
||||
@@ -250,7 +251,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
body: {
|
||||
maxHeight: 150,
|
||||
overflowY: 'auto',
|
||||
background: '#fafafa',
|
||||
background: token.colorFillAlter,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -258,7 +259,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
style={{
|
||||
margin: 0,
|
||||
whiteSpace: 'pre-wrap',
|
||||
color: '#595959',
|
||||
color: token.colorText,
|
||||
lineHeight: 1.8,
|
||||
}}
|
||||
>
|
||||
@@ -352,7 +353,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
<div
|
||||
style={{
|
||||
height: 4,
|
||||
background: '#f0f0f0',
|
||||
background: token.colorFillTertiary,
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
@@ -360,7 +361,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
||||
background: `linear-gradient(90deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
|
||||
width: `${progress}%`,
|
||||
transition: 'width 0.3s ease',
|
||||
borderRadius: 2,
|
||||
@@ -374,8 +375,8 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
size="small"
|
||||
ref={generatedTextRef}
|
||||
style={{
|
||||
background: generatedText ? '#f6ffed' : '#fafafa',
|
||||
border: generatedText ? '1px solid #b7eb8f' : '1px solid #d9d9d9',
|
||||
background: generatedText ? token.colorSuccessBg : token.colorFillAlter,
|
||||
border: generatedText ? `1px solid ${token.colorSuccessBorder}` : `1px solid ${token.colorBorder}`,
|
||||
}}
|
||||
styles={{
|
||||
body: {
|
||||
@@ -400,7 +401,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
display: 'inline-block',
|
||||
width: 8,
|
||||
height: 16,
|
||||
background: 'var(--color-primary)',
|
||||
background: token.colorPrimary,
|
||||
marginLeft: 2,
|
||||
animation: 'blink 1s infinite',
|
||||
}}
|
||||
@@ -408,7 +409,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
||||
)}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: 20, color: '#8c8c8c' }}>
|
||||
<div style={{ textAlign: 'center', padding: 20, color: token.colorTextTertiary }}>
|
||||
{isGenerating ? '正在生成内容...' : '等待生成...'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { Button, Tooltip, theme } from 'antd';
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
|
||||
interface PartialRegenerateToolbarProps {
|
||||
@@ -19,6 +19,8 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
|
||||
onRegenerate,
|
||||
selectedText
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
if (!visible || !selectedText) return null;
|
||||
|
||||
// 限制显示的选中文本长度
|
||||
@@ -33,15 +35,15 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
zIndex: 10000,
|
||||
background: '#fff',
|
||||
background: token.colorBgElevated,
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.15)',
|
||||
boxShadow: token.boxShadow,
|
||||
padding: '6px 8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
animation: 'fadeIn 0.2s ease-out',
|
||||
border: '1px solid #e8e8e8',
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
@@ -61,7 +63,7 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
|
||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
||||
border: 'none',
|
||||
fontWeight: 500,
|
||||
boxShadow: '0 4px 12px rgba(77, 128, 136, 0.3)',
|
||||
boxShadow: token.boxShadowSecondary,
|
||||
}}
|
||||
>
|
||||
AI重写
|
||||
@@ -69,7 +71,7 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
|
||||
</Tooltip>
|
||||
<span style={{
|
||||
fontSize: 12,
|
||||
color: '#8c8c8c',
|
||||
color: token.colorTextTertiary,
|
||||
maxWidth: 150,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Spin } from 'antd';
|
||||
import { Spin, theme } from 'antd';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
|
||||
interface SSELoadingOverlayProps {
|
||||
@@ -13,6 +13,8 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
progress,
|
||||
message
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
if (!loading) return null;
|
||||
|
||||
return (
|
||||
@@ -22,19 +24,19 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.45)',
|
||||
background: token.colorBgMask,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 9999
|
||||
}}>
|
||||
<div style={{
|
||||
background: '#fff',
|
||||
background: token.colorBgElevated,
|
||||
borderRadius: 12,
|
||||
padding: '40px 60px',
|
||||
minWidth: 400,
|
||||
maxWidth: 600,
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.2)'
|
||||
boxShadow: token.boxShadowSecondary
|
||||
}}>
|
||||
{/* 标题和图标 */}
|
||||
<div style={{
|
||||
@@ -42,13 +44,13 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
marginBottom: 24
|
||||
}}>
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 48, color: 'var(--color-primary)' }} spin />}
|
||||
indicator={<LoadingOutlined style={{ fontSize: 48, color: token.colorPrimary }} spin />}
|
||||
/>
|
||||
<div style={{
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
marginTop: 16,
|
||||
color: 'var(--color-text-primary)'
|
||||
color: token.colorTextHeading
|
||||
}}>
|
||||
AI生成中...
|
||||
</div>
|
||||
@@ -60,7 +62,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
}}>
|
||||
<div style={{
|
||||
height: 12,
|
||||
background: 'var(--color-bg-layout)',
|
||||
background: token.colorFillTertiary,
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
marginBottom: 12
|
||||
@@ -68,12 +70,12 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
<div style={{
|
||||
height: '100%',
|
||||
background: progress === 100
|
||||
? 'linear-gradient(90deg, var(--color-success) 0%, var(--color-success-active) 100%)'
|
||||
: 'linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-active) 100%)',
|
||||
? `linear-gradient(90deg, ${token.colorSuccess} 0%, ${token.colorSuccessActive} 100%)`
|
||||
: `linear-gradient(90deg, ${token.colorPrimary} 0%, ${token.colorPrimaryActive} 100%)`,
|
||||
width: `${progress}%`,
|
||||
transition: 'all 0.3s ease',
|
||||
borderRadius: 6,
|
||||
boxShadow: progress > 0 ? 'var(--shadow-card)' : 'none'
|
||||
boxShadow: progress > 0 ? token.boxShadow : 'none'
|
||||
}} />
|
||||
</div>
|
||||
|
||||
@@ -82,7 +84,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
textAlign: 'center',
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
color: progress === 100 ? 'var(--color-success)' : 'var(--color-primary)',
|
||||
color: progress === 100 ? token.colorSuccess : token.colorPrimary,
|
||||
marginBottom: 8
|
||||
}}>
|
||||
{progress}%
|
||||
@@ -93,7 +95,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
color: '#595959',
|
||||
color: token.colorText,
|
||||
minHeight: 24,
|
||||
padding: '0 20px'
|
||||
}}>
|
||||
@@ -104,7 +106,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
fontSize: 13,
|
||||
color: '#8c8c8c',
|
||||
color: token.colorTextTertiary,
|
||||
marginTop: 16
|
||||
}}>
|
||||
请勿关闭页面,生成过程需要一定时间
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { theme } from 'antd';
|
||||
|
||||
interface SSEProgressBarProps {
|
||||
loading: boolean;
|
||||
@@ -11,6 +12,8 @@ export const SSEProgressBar: React.FC<SSEProgressBarProps> = ({
|
||||
progress,
|
||||
message
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
if (!loading) return null;
|
||||
|
||||
return (
|
||||
@@ -18,14 +21,14 @@ export const SSEProgressBar: React.FC<SSEProgressBarProps> = ({
|
||||
{/* 进度条 */}
|
||||
<div style={{
|
||||
height: 8,
|
||||
background: '#f0f0f0',
|
||||
background: token.colorFillTertiary,
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
marginBottom: 8
|
||||
}}>
|
||||
<div style={{
|
||||
height: '100%',
|
||||
background: progress === 100 ? '#52c41a' : '#1890ff',
|
||||
background: progress === 100 ? token.colorSuccess : token.colorPrimary,
|
||||
width: `${progress}%`,
|
||||
transition: 'all 0.3s ease',
|
||||
borderRadius: 4
|
||||
@@ -39,12 +42,12 @@ export const SSEProgressBar: React.FC<SSEProgressBarProps> = ({
|
||||
alignItems: 'center',
|
||||
fontSize: 14
|
||||
}}>
|
||||
<span style={{ color: '#666' }}>
|
||||
<span style={{ color: token.colorTextSecondary }}>
|
||||
{message || '准备生成...'}
|
||||
</span>
|
||||
<span style={{
|
||||
fontWeight: 'bold',
|
||||
color: progress === 100 ? '#52c41a' : '#1890ff'
|
||||
color: progress === 100 ? token.colorSuccess : token.colorPrimary
|
||||
}}>
|
||||
{progress}%
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Modal, Spin, Button } from 'antd';
|
||||
import { Modal, Spin, Button, theme } from 'antd';
|
||||
import { LoadingOutlined, StopOutlined } from '@ant-design/icons';
|
||||
|
||||
interface SSEProgressModalProps {
|
||||
@@ -27,6 +27,8 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
onCancel,
|
||||
cancelButtonText = '取消任务',
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
@@ -53,13 +55,13 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
marginBottom: 24
|
||||
}}>
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 48, color: 'var(--color-primary)' }} spin />}
|
||||
indicator={<LoadingOutlined style={{ fontSize: 48, color: token.colorPrimary }} spin />}
|
||||
/>
|
||||
<div style={{
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
marginTop: 16,
|
||||
color: 'var(--color-text-primary)'
|
||||
color: token.colorText
|
||||
}}>
|
||||
{title}
|
||||
</div>
|
||||
@@ -72,7 +74,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
}}>
|
||||
<div style={{
|
||||
height: 12,
|
||||
background: 'var(--color-bg-layout)',
|
||||
background: token.colorBgLayout,
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
marginBottom: showPercentage ? 12 : 0
|
||||
@@ -80,12 +82,12 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
<div style={{
|
||||
height: '100%',
|
||||
background: progress === 100
|
||||
? 'linear-gradient(90deg, var(--color-success) 0%, var(--color-success-active) 100%)'
|
||||
: 'linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-active) 100%)',
|
||||
? `linear-gradient(90deg, ${token.colorSuccess} 0%, ${token.colorSuccess} 100%)`
|
||||
: `linear-gradient(90deg, ${token.colorPrimary} 0%, ${token.colorPrimary} 100%)`,
|
||||
width: `${progress}%`,
|
||||
transition: 'all 0.3s ease',
|
||||
borderRadius: 6,
|
||||
boxShadow: progress > 0 ? 'var(--shadow-card)' : 'none'
|
||||
boxShadow: progress > 0 ? token.boxShadow : 'none'
|
||||
}} />
|
||||
</div>
|
||||
|
||||
@@ -95,7 +97,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
textAlign: 'center',
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
color: progress === 100 ? 'var(--color-success)' : 'var(--color-primary)',
|
||||
color: progress === 100 ? token.colorSuccess : token.colorPrimary,
|
||||
marginBottom: 8
|
||||
}}>
|
||||
{progress}%
|
||||
@@ -107,7 +109,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
minHeight: 24,
|
||||
padding: '0 20px',
|
||||
marginBottom: 16
|
||||
@@ -119,7 +121,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
fontSize: 13,
|
||||
color: 'var(--color-text-tertiary)',
|
||||
color: token.colorTextTertiary,
|
||||
marginBottom: onCancel ? 16 : 0
|
||||
}}>
|
||||
请勿关闭页面,生成过程需要一定时间
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Dropdown, Avatar, Space, Typography, message, Modal, Form, Input, Button } from 'antd';
|
||||
import { Dropdown, Avatar, Space, Typography, message, Modal, Form, Input, Button, theme } from 'antd';
|
||||
import { UserOutlined, LogoutOutlined, TeamOutlined, CrownOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import { authApi } from '../services/api';
|
||||
import type { User } from '../types';
|
||||
@@ -11,14 +11,18 @@ const { Text } = Typography;
|
||||
interface UserMenuProps {
|
||||
/** 是否总是显示完整信息(用于移动端侧边栏) */
|
||||
showFullInfo?: boolean;
|
||||
/** 紧凑模式(用于折叠侧边栏,仅展示头像) */
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export default function UserMenu({ showFullInfo = false }: UserMenuProps) {
|
||||
export default function UserMenu({ showFullInfo = false, compact = false }: UserMenuProps) {
|
||||
const navigate = useNavigate();
|
||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||
const [showChangePassword, setShowChangePassword] = useState(false);
|
||||
const [changePasswordForm] = Form.useForm();
|
||||
const [changingPassword, setChangingPassword] = useState(false);
|
||||
const { token } = theme.useToken();
|
||||
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||
|
||||
useEffect(() => {
|
||||
loadCurrentUser();
|
||||
@@ -126,36 +130,36 @@ export default function UserMenu({ showFullInfo = false }: UserMenuProps) {
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: '8px 16px',
|
||||
background: 'rgba(255, 255, 255, 0.6)', // 保持半透明以配合 Backdrop
|
||||
gap: compact ? 0 : 12,
|
||||
padding: compact ? '4px' : '8px 16px',
|
||||
background: alphaColor(token.colorBgContainer, 0.65), // 保持半透明以配合 Backdrop
|
||||
backdropFilter: 'blur(10px)',
|
||||
WebkitBackdropFilter: 'blur(10px)',
|
||||
borderRadius: 24,
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: compact ? 16 : 24,
|
||||
border: `1px solid ${token.colorBorder}`,
|
||||
transition: 'all 0.3s ease',
|
||||
boxShadow: 'var(--shadow-card)',
|
||||
boxShadow: `0 8px 20px ${alphaColor(token.colorText, 0.08)}`,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'var(--color-bg-container)'; // 悬浮时变实
|
||||
e.currentTarget.style.background = token.colorBgContainer; // 悬浮时变实
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
e.currentTarget.style.boxShadow = 'var(--shadow-elevated)';
|
||||
e.currentTarget.style.boxShadow = `0 12px 28px ${alphaColor(token.colorText, 0.14)}`;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.6)';
|
||||
e.currentTarget.style.background = alphaColor(token.colorBgContainer, 0.65);
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = 'var(--shadow-card)';
|
||||
e.currentTarget.style.boxShadow = `0 8px 20px ${alphaColor(token.colorText, 0.08)}`;
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Avatar
|
||||
src={currentUser.avatar_url}
|
||||
icon={<UserOutlined />}
|
||||
size={40}
|
||||
size={compact ? 32 : 40}
|
||||
style={{
|
||||
backgroundColor: 'var(--color-primary)',
|
||||
border: '3px solid #fff',
|
||||
boxShadow: 'var(--shadow-card)',
|
||||
backgroundColor: token.colorPrimary,
|
||||
border: `3px solid ${token.colorWhite}`,
|
||||
boxShadow: `0 8px 20px ${alphaColor(token.colorText, 0.12)}`,
|
||||
}}
|
||||
/>
|
||||
{currentUser.is_admin && (
|
||||
@@ -165,28 +169,28 @@ export default function UserMenu({ showFullInfo = false }: UserMenuProps) {
|
||||
right: -2,
|
||||
width: 18,
|
||||
height: 18,
|
||||
background: 'linear-gradient(135deg, #ffd700 0%, #ffaa00 100%)',
|
||||
background: `linear-gradient(135deg, ${token.colorWarning} 0%, ${token.colorWarningHover} 100%)`,
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '2px solid white',
|
||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
|
||||
border: `2px solid ${token.colorWhite}`,
|
||||
boxShadow: `0 2px 4px ${alphaColor(token.colorText, 0.2)}`,
|
||||
}}>
|
||||
<CrownOutlined style={{ fontSize: 9, color: '#fff' }} />
|
||||
<CrownOutlined style={{ fontSize: 9, color: token.colorWhite }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Space direction="vertical" size={0} style={{ display: (window.innerWidth <= 768 && !showFullInfo) ? 'none' : 'flex' }}>
|
||||
<Space direction="vertical" size={0} style={{ display: compact ? 'none' : ((window.innerWidth <= 768 && !showFullInfo) ? 'none' : 'flex') }}>
|
||||
<Text strong style={{
|
||||
color: 'var(--color-text-primary)',
|
||||
color: token.colorText,
|
||||
fontSize: 14,
|
||||
lineHeight: '20px',
|
||||
}}>
|
||||
{currentUser.display_name || currentUser.username}
|
||||
</Text>
|
||||
<Text style={{
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: token.colorTextSecondary,
|
||||
fontSize: 12,
|
||||
lineHeight: '18px',
|
||||
}}>
|
||||
|
||||
Reference in New Issue
Block a user