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

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