refactor: 大量前端页面/组件样式从硬编码颜色迁移到 antd token 主题变量
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import React, { useMemo, useEffect, useRef } from 'react';
|
import React, { useMemo, useEffect, useRef } from 'react';
|
||||||
|
import { theme } from 'antd';
|
||||||
|
|
||||||
// 标注数据类型
|
// 标注数据类型
|
||||||
export interface MemoryAnnotation {
|
export interface MemoryAnnotation {
|
||||||
@@ -35,14 +36,6 @@ interface AnnotatedTextProps {
|
|||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 类型颜色映射
|
|
||||||
const TYPE_COLORS = {
|
|
||||||
hook: '#ff6b6b',
|
|
||||||
foreshadow: '#6b7bff',
|
|
||||||
plot_point: '#51cf66',
|
|
||||||
character_event: '#ffd93d',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 类型图标映射
|
// 类型图标映射
|
||||||
const TYPE_ICONS = {
|
const TYPE_ICONS = {
|
||||||
hook: '🎣',
|
hook: '🎣',
|
||||||
@@ -65,6 +58,14 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const annotationRefs = useRef<Record<string, HTMLSpanElement | null>>({});
|
const annotationRefs = useRef<Record<string, HTMLSpanElement | null>>({});
|
||||||
|
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const typeColors: Record<MemoryAnnotation['type'], string> = {
|
||||||
|
hook: token.colorError,
|
||||||
|
foreshadow: token.colorInfo,
|
||||||
|
plot_point: token.colorSuccess,
|
||||||
|
character_event: token.colorWarning,
|
||||||
|
};
|
||||||
|
|
||||||
// 当需要滚动到特定标注时
|
// 当需要滚动到特定标注时
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scrollToAnnotation && annotationRefs.current[scrollToAnnotation]) {
|
if (scrollToAnnotation && annotationRefs.current[scrollToAnnotation]) {
|
||||||
@@ -214,7 +215,7 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
|
|||||||
const { annotation, annotations } = segment;
|
const { annotation, annotations } = segment;
|
||||||
if (!annotation) return null;
|
if (!annotation) return null;
|
||||||
|
|
||||||
const color = TYPE_COLORS[annotation.type];
|
const color = typeColors[annotation.type];
|
||||||
const icon = TYPE_ICONS[annotation.type];
|
const icon = TYPE_ICONS[annotation.type];
|
||||||
const isActive = activeAnnotationId === annotation.id;
|
const isActive = activeAnnotationId === annotation.id;
|
||||||
|
|
||||||
@@ -238,17 +239,17 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
borderBottom: `2px solid ${color}`,
|
borderBottom: `2px solid ${color}`,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
backgroundColor: isActive ? `${color}22` : 'transparent',
|
backgroundColor: isActive ? `color-mix(in srgb, ${color} 13%, transparent)` : 'transparent',
|
||||||
transition: 'all 0.2s',
|
transition: 'all 0.2s',
|
||||||
padding: '2px 0',
|
padding: '2px 0',
|
||||||
}}
|
}}
|
||||||
onClick={() => onAnnotationClick?.(annotation)}
|
onClick={() => onAnnotationClick?.(annotation)}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.backgroundColor = `${color}33`;
|
e.currentTarget.style.backgroundColor = `color-mix(in srgb, ${color} 20%, transparent)`;
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.backgroundColor = isActive
|
e.currentTarget.style.backgroundColor = isActive
|
||||||
? `${color}22`
|
? `color-mix(in srgb, ${color} 13%, transparent)`
|
||||||
: 'transparent';
|
: 'transparent';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Modal, Button, Space } from 'antd';
|
import { Modal, Button, Space, theme } from 'antd';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
interface AnnouncementModalProps {
|
interface AnnouncementModalProps {
|
||||||
@@ -11,6 +11,8 @@ interface AnnouncementModalProps {
|
|||||||
export default function AnnouncementModal({ visible, onClose, onDoNotShowToday, onNeverShow }: AnnouncementModalProps) {
|
export default function AnnouncementModal({ visible, onClose, onDoNotShowToday, onNeverShow }: AnnouncementModalProps) {
|
||||||
const [qqImageError, setQqImageError] = useState(false);
|
const [qqImageError, setQqImageError] = useState(false);
|
||||||
const [wxImageError, setWxImageError] = useState(false);
|
const [wxImageError, setWxImageError] = useState(false);
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
@@ -35,7 +37,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
|||||||
<div style={{
|
<div style={{
|
||||||
fontSize: '20px',
|
fontSize: '20px',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: 'var(--color-primary)',
|
color: token.colorPrimary,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
}}>
|
}}>
|
||||||
🎉 欢迎使用 AI小说创作助手
|
🎉 欢迎使用 AI小说创作助手
|
||||||
@@ -64,9 +66,9 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
|||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
height: '40px',
|
height: '40px',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
background: 'var(--color-primary)',
|
background: token.colorPrimary,
|
||||||
borderColor: 'var(--color-primary)',
|
borderColor: token.colorPrimary,
|
||||||
boxShadow: 'var(--shadow-primary)',
|
boxShadow: `0 8px 20px ${alphaColor(token.colorPrimary, 0.32)}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
永不再展示
|
永不再展示
|
||||||
@@ -78,16 +80,16 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
|||||||
styles={{
|
styles={{
|
||||||
body: {
|
body: {
|
||||||
padding: '20px',
|
padding: '20px',
|
||||||
background: 'var(--color-bg-container)',
|
background: token.colorBgContainer,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
background: 'linear-gradient(135deg, rgba(77, 128, 136, 0.08) 0%, rgba(248, 246, 241, 0.95) 100%)',
|
background: `linear-gradient(135deg, ${alphaColor(token.colorPrimary, 0.1)} 0%, ${alphaColor(token.colorBgContainer, 0.98)} 100%)`,
|
||||||
borderBottom: '1px solid var(--color-border-secondary)',
|
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||||
padding: '16px 24px',
|
padding: '16px 24px',
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
background: 'var(--color-bg-container)',
|
background: token.colorBgContainer,
|
||||||
borderTop: '1px solid var(--color-border-secondary)',
|
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||||||
padding: '16px 24px',
|
padding: '16px 24px',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -96,7 +98,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginBottom: '12px',
|
marginBottom: '12px',
|
||||||
fontSize: '15px',
|
fontSize: '15px',
|
||||||
color: 'var(--color-text-secondary)',
|
color: token.colorTextSecondary,
|
||||||
lineHeight: '1.5',
|
lineHeight: '1.5',
|
||||||
}}>
|
}}>
|
||||||
<p style={{ marginBottom: '8px' }}>👋 欢迎加入我们的交流群!在这里你可以:</p>
|
<p style={{ marginBottom: '8px' }}>👋 欢迎加入我们的交流群!在这里你可以:</p>
|
||||||
@@ -111,7 +113,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
|||||||
<li>🐛 反馈问题和建议</li>
|
<li>🐛 反馈问题和建议</li>
|
||||||
<li>📚 分享创作经验和灵感</li>
|
<li>📚 分享创作经验和灵感</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '12px' }}>
|
<p style={{ fontWeight: 600, color: token.colorText, marginBottom: '12px' }}>
|
||||||
扫描下方二维码加入交流群:
|
扫描下方二维码加入交流群:
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,7 +124,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
|||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
gap: '24px',
|
gap: '24px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
background: 'var(--color-bg-layout)',
|
background: token.colorBgLayout,
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
}}>
|
}}>
|
||||||
@@ -133,7 +135,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
minWidth: '200px',
|
minWidth: '200px',
|
||||||
}}>
|
}}>
|
||||||
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '8px', fontSize: '14px' }}>
|
<p style={{ fontWeight: 600, color: token.colorText, marginBottom: '8px', fontSize: '14px' }}>
|
||||||
QQ交流群
|
QQ交流群
|
||||||
</p>
|
</p>
|
||||||
{!qqImageError ? (
|
{!qqImageError ? (
|
||||||
@@ -141,10 +143,10 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
background: 'var(--color-bg-container)',
|
background: token.colorBgContainer,
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
padding: '6px',
|
padding: '6px',
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
boxShadow: `0 2px 8px ${alphaColor(token.colorText, 0.12)}`,
|
||||||
}}>
|
}}>
|
||||||
<img
|
<img
|
||||||
src="/qq.jpg"
|
src="/qq.jpg"
|
||||||
@@ -167,9 +169,9 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
background: 'var(--color-bg-container)',
|
background: token.colorBgContainer,
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
color: '#999',
|
color: token.colorTextTertiary,
|
||||||
}}>
|
}}>
|
||||||
<p>二维码加载失败</p>
|
<p>二维码加载失败</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -183,7 +185,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
minWidth: '200px',
|
minWidth: '200px',
|
||||||
}}>
|
}}>
|
||||||
<p style={{ fontWeight: 600, color: 'var(--color-text-primary)', marginBottom: '8px', fontSize: '14px' }}>
|
<p style={{ fontWeight: 600, color: token.colorText, marginBottom: '8px', fontSize: '14px' }}>
|
||||||
微信交流群
|
微信交流群
|
||||||
</p>
|
</p>
|
||||||
{!wxImageError ? (
|
{!wxImageError ? (
|
||||||
@@ -191,10 +193,10 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
background: 'var(--color-bg-container)',
|
background: token.colorBgContainer,
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
padding: '6px',
|
padding: '6px',
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
boxShadow: `0 2px 8px ${alphaColor(token.colorText, 0.12)}`,
|
||||||
}}>
|
}}>
|
||||||
<img
|
<img
|
||||||
src="/WX.png"
|
src="/WX.png"
|
||||||
@@ -217,9 +219,9 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
background: 'var(--color-bg-container)',
|
background: token.colorBgContainer,
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
color: '#999',
|
color: token.colorTextTertiary,
|
||||||
}}>
|
}}>
|
||||||
<p>二维码加载失败</p>
|
<p>二维码加载失败</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,11 +232,11 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday,
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginTop: '16px',
|
marginTop: '16px',
|
||||||
padding: '10px',
|
padding: '10px',
|
||||||
background: 'var(--color-warning-bg)',
|
background: token.colorWarningBg,
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
border: '1px solid var(--color-warning-border)',
|
border: `1px solid ${token.colorWarningBorder}`,
|
||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
color: 'var(--color-warning)',
|
color: token.colorWarning,
|
||||||
}}>
|
}}>
|
||||||
💡 提示:选择"今日内不再展示"当天不再显示,选择"永不再展示"将永久隐藏此公告
|
💡 提示:选择"今日内不再展示"当天不再显示,选择"永不再展示"将永久隐藏此公告
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Typography, Space, Divider, Badge, Button, Grid } from 'antd';
|
import { Typography, Space, Divider, Badge, Button, Grid, theme } from 'antd';
|
||||||
import { GithubOutlined, CopyrightOutlined, HeartFilled, ClockCircleOutlined, GiftOutlined } from '@ant-design/icons';
|
import { GithubOutlined, CopyrightOutlined, HeartFilled, ClockCircleOutlined, GiftOutlined } from '@ant-design/icons';
|
||||||
import { VERSION_INFO, getVersionString } from '../config/version';
|
import { VERSION_INFO, getVersionString } from '../config/version';
|
||||||
import { checkLatestVersion } from '../services/versionService';
|
import { checkLatestVersion } from '../services/versionService';
|
||||||
@@ -17,6 +17,8 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
const [hasUpdate, setHasUpdate] = useState(false);
|
const [hasUpdate, setHasUpdate] = useState(false);
|
||||||
const [latestVersion, setLatestVersion] = useState('');
|
const [latestVersion, setLatestVersion] = useState('');
|
||||||
const [releaseUrl, setReleaseUrl] = useState('');
|
const [releaseUrl, setReleaseUrl] = useState('');
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 检查版本更新(每次都重新检查)
|
// 检查版本更新(每次都重新检查)
|
||||||
@@ -55,11 +57,11 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
right: 0,
|
right: 0,
|
||||||
backdropFilter: 'blur(20px) saturate(180%)',
|
backdropFilter: 'blur(20px) saturate(180%)',
|
||||||
WebkitBackdropFilter: 'blur(20px) saturate(180%)',
|
WebkitBackdropFilter: 'blur(20px) saturate(180%)',
|
||||||
borderTop: '1px solid var(--color-border)',
|
borderTop: `1px solid ${token.colorBorder}`,
|
||||||
padding: isMobile ? '8px 12px' : '10px 16px',
|
padding: isMobile ? '8px 12px' : '10px 16px',
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
boxShadow: 'var(--shadow-card)',
|
boxShadow: `0 -2px 16px ${alphaColor(token.colorText, 0.08)}`,
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.8)', // 半透明背景以支持 backdrop-filter
|
backgroundColor: alphaColor(token.colorBgContainer, 0.82), // 半透明背景以支持 backdrop-filter
|
||||||
transition: 'left 0.3s ease', // 平滑过渡
|
transition: 'left 0.3s ease', // 平滑过渡
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -87,23 +89,23 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
color: 'var(--color-primary)',
|
color: token.colorPrimary,
|
||||||
cursor: hasUpdate ? 'pointer' : 'default',
|
cursor: hasUpdate ? 'pointer' : 'default',
|
||||||
}}
|
}}
|
||||||
title={hasUpdate ? `发现新版本 v${latestVersion},点击查看` : '当前版本'}
|
title={hasUpdate ? `发现新版本 v${latestVersion},点击查看` : '当前版本'}
|
||||||
>
|
>
|
||||||
<strong style={{ color: 'var(--color-text-primary)' }}>{VERSION_INFO.projectName}</strong>
|
<strong style={{ color: token.colorText }}>{VERSION_INFO.projectName}</strong>
|
||||||
<span>{getVersionString()}</span>
|
<span>{getVersionString()}</span>
|
||||||
</Text>
|
</Text>
|
||||||
</Badge>
|
</Badge>
|
||||||
<Divider type="vertical" style={{ margin: '0 4px', borderColor: 'var(--color-border)' }} />
|
<Divider type="vertical" style={{ margin: '0 4px', borderColor: token.colorBorder }} />
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<GiftOutlined />}
|
icon={<GiftOutlined />}
|
||||||
onClick={() => window.open('https://mumuverse.space:1588/', '_blank')}
|
onClick={() => window.open('https://mumuverse.space:1588/', '_blank')}
|
||||||
style={{
|
style={{
|
||||||
color: 'var(--color-text-secondary)',
|
color: token.colorTextSecondary,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
height: 24,
|
height: 24,
|
||||||
padding: '0 4px',
|
padding: '0 4px',
|
||||||
@@ -114,7 +116,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
>
|
>
|
||||||
赞助
|
赞助
|
||||||
</Button>
|
</Button>
|
||||||
<Divider type="vertical" style={{ margin: '0 4px', borderColor: 'var(--color-border)' }} />
|
<Divider type="vertical" style={{ margin: '0 4px', borderColor: token.colorBorder }} />
|
||||||
<Link
|
<Link
|
||||||
href={VERSION_INFO.githubUrl}
|
href={VERSION_INFO.githubUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -124,7 +126,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
color: 'var(--color-text-secondary)',
|
color: token.colorTextSecondary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GithubOutlined style={{ fontSize: 12 }} />
|
<GithubOutlined style={{ fontSize: 12 }} />
|
||||||
@@ -132,7 +134,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: 'var(--color-text-tertiary)',
|
color: token.colorTextTertiary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ClockCircleOutlined style={{ fontSize: 10, marginRight: 4 }} />
|
<ClockCircleOutlined style={{ fontSize: 10, marginRight: 4 }} />
|
||||||
@@ -144,7 +146,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
<Space
|
<Space
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
size={12}
|
size={12}
|
||||||
split={<Divider type="vertical" style={{ borderColor: 'var(--color-border)' }} />}
|
split={<Divider type="vertical" style={{ borderColor: token.colorBorder }} />}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -160,7 +162,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 6,
|
gap: 6,
|
||||||
color: 'var(--color-text-secondary)',
|
color: token.colorTextSecondary,
|
||||||
textShadow: 'none',
|
textShadow: 'none',
|
||||||
cursor: hasUpdate ? 'pointer' : 'default',
|
cursor: hasUpdate ? 'pointer' : 'default',
|
||||||
transition: 'all 0.3s',
|
transition: 'all 0.3s',
|
||||||
@@ -177,7 +179,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
}}
|
}}
|
||||||
title={hasUpdate ? `发现新版本 v${latestVersion},点击查看` : '当前版本'}
|
title={hasUpdate ? `发现新版本 v${latestVersion},点击查看` : '当前版本'}
|
||||||
>
|
>
|
||||||
<strong style={{ color: 'var(--color-text-primary)' }}>{VERSION_INFO.projectName}</strong>
|
<strong style={{ color: token.colorText }}>{VERSION_INFO.projectName}</strong>
|
||||||
<span>{getVersionString()}</span>
|
<span>{getVersionString()}</span>
|
||||||
</Text>
|
</Text>
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -192,7 +194,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 6,
|
gap: 6,
|
||||||
color: 'var(--color-text-secondary)',
|
color: token.colorTextSecondary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GithubOutlined style={{ fontSize: 13 }} />
|
<GithubOutlined style={{ fontSize: 13 }} />
|
||||||
@@ -206,7 +208,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: 'var(--color-text-secondary)',
|
color: token.colorTextSecondary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
LinuxDO 社区
|
LinuxDO 社区
|
||||||
@@ -218,9 +220,9 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
icon={<GiftOutlined style={{ fontSize: 14 }} />}
|
icon={<GiftOutlined style={{ fontSize: 14 }} />}
|
||||||
onClick={() => window.open('https://mumuverse.space:1588/', '_blank')}
|
onClick={() => window.open('https://mumuverse.space:1588/', '_blank')}
|
||||||
style={{
|
style={{
|
||||||
background: 'var(--color-primary)',
|
background: token.colorPrimary,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
boxShadow: '0 4px 12px rgba(77, 128, 136, 0.3)',
|
boxShadow: `0 4px 12px ${alphaColor(token.colorPrimary, 0.35)}`,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
height: 32,
|
height: 32,
|
||||||
padding: '0 20px',
|
padding: '0 20px',
|
||||||
@@ -232,11 +234,11 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||||
e.currentTarget.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.6)';
|
e.currentTarget.style.boxShadow = `0 6px 16px ${alphaColor(token.colorPrimary, 0.5)}`;
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.transform = 'translateY(0)';
|
e.currentTarget.style.transform = 'translateY(0)';
|
||||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.5)';
|
e.currentTarget.style.boxShadow = `0 4px 12px ${alphaColor(token.colorPrimary, 0.35)}`;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
赞助支持
|
赞助支持
|
||||||
@@ -252,7 +254,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 6,
|
gap: 6,
|
||||||
color: 'var(--color-text-secondary)',
|
color: token.colorTextSecondary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyrightOutlined style={{ fontSize: 11 }} />
|
<CopyrightOutlined style={{ fontSize: 11 }} />
|
||||||
@@ -266,7 +268,7 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
color: 'var(--color-text-tertiary)',
|
color: token.colorTextTertiary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ClockCircleOutlined style={{ fontSize: 12 }} />
|
<ClockCircleOutlined style={{ fontSize: 12 }} />
|
||||||
@@ -280,12 +282,12 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
color: 'var(--color-text-secondary)',
|
color: token.colorTextSecondary,
|
||||||
textShadow: '0 1px 3px rgba(0, 0, 0, 0.05)',
|
textShadow: `0 1px 3px ${alphaColor(token.colorText, 0.08)}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Made with</span>
|
<span>Made with</span>
|
||||||
<HeartFilled style={{ color: 'var(--color-error)', fontSize: 11 }} />
|
<HeartFilled style={{ color: token.colorError, fontSize: 11 }} />
|
||||||
<span>by {VERSION_INFO.author}</span>
|
<span>by {VERSION_INFO.author}</span>
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Modal, Spin, Alert, Tabs, Card, Tag, List, Empty, Statistic, Row, Col, Button } from 'antd';
|
import { Modal, Spin, Alert, Tabs, Card, Tag, List, Empty, Statistic, Row, Col, Button, theme } from 'antd';
|
||||||
import {
|
import {
|
||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
BulbOutlined,
|
BulbOutlined,
|
||||||
@@ -27,6 +27,7 @@ interface ChapterAnalysisProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ChapterAnalysis({ chapterId, visible, onClose }: ChapterAnalysisProps) {
|
export default function ChapterAnalysis({ chapterId, visible, onClose }: ChapterAnalysisProps) {
|
||||||
|
const { token } = theme.useToken();
|
||||||
const [task, setTask] = useState<AnalysisTask | null>(null);
|
const [task, setTask] = useState<AnalysisTask | null>(null);
|
||||||
const [analysis, setAnalysis] = useState<ChapterAnalysisResponse | null>(null);
|
const [analysis, setAnalysis] = useState<ChapterAnalysisResponse | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -258,7 +259,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
|||||||
transition: 'all 0.3s ease',
|
transition: 'all 0.3s ease',
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
boxShadow: task.progress > 0 && task.status !== 'failed'
|
boxShadow: task.progress > 0 && task.status !== 'failed'
|
||||||
? '0 0 10px rgba(24, 144, 255, 0.3)'
|
? `0 0 10px color-mix(in srgb, ${token.colorPrimary} 30%, transparent)`
|
||||||
: 'none'
|
: 'none'
|
||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Modal, Button, Card, Statistic, Row, Col, message } from 'antd';
|
import { Modal, Button, Card, Statistic, Row, Col, message, theme } from 'antd';
|
||||||
import { CheckOutlined, CloseOutlined, SwapOutlined } from '@ant-design/icons';
|
import { CheckOutlined, CloseOutlined, SwapOutlined } from '@ant-design/icons';
|
||||||
import ReactDiffViewer from 'react-diff-viewer-continued';
|
import ReactDiffViewer from 'react-diff-viewer-continued';
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
|
|||||||
onApply,
|
onApply,
|
||||||
onDiscard
|
onDiscard
|
||||||
}) => {
|
}) => {
|
||||||
|
const { token } = theme.useToken();
|
||||||
const [applying, setApplying] = useState(false);
|
const [applying, setApplying] = useState(false);
|
||||||
const [viewMode, setViewMode] = useState<'split' | 'unified'>('split');
|
const [viewMode, setViewMode] = useState<'split' | 'unified'>('split');
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
@@ -195,7 +196,7 @@ const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
|
|||||||
styles={{
|
styles={{
|
||||||
variables: {
|
variables: {
|
||||||
light: {
|
light: {
|
||||||
diffViewerBackground: '#fff', // Keep white for diff viewer readability
|
diffViewerBackground: token.colorBgContainer,
|
||||||
addedBackground: 'var(--color-success-bg)',
|
addedBackground: 'var(--color-success-bg)',
|
||||||
addedColor: 'var(--color-text-primary)',
|
addedColor: 'var(--color-text-primary)',
|
||||||
removedBackground: 'var(--color-error-bg)',
|
removedBackground: 'var(--color-error-bg)',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 提供沉浸式阅读体验,支持主题切换、字体调节、翻页导航等功能
|
* 提供沉浸式阅读体验,支持主题切换、字体调节、翻页导航等功能
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Modal, Button, Slider, Radio, Space, Typography, Spin, message } from 'antd';
|
import { Modal, Button, Slider, Radio, Space, Typography, Spin, message, theme } from 'antd';
|
||||||
import {
|
import {
|
||||||
LeftOutlined,
|
LeftOutlined,
|
||||||
RightOutlined,
|
RightOutlined,
|
||||||
@@ -37,27 +37,12 @@ interface NavigationInfo {
|
|||||||
current: { id: string; chapter_number: number; title: string };
|
current: { id: string; chapter_number: number; title: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主题样式配置
|
interface ReaderThemeStyle {
|
||||||
const themeStyles = {
|
bg: string;
|
||||||
light: {
|
text: string;
|
||||||
bg: '#ffffff',
|
headerBg: string;
|
||||||
text: '#333333',
|
border: string;
|
||||||
headerBg: '#fafafa',
|
}
|
||||||
border: '#e8e8e8'
|
|
||||||
},
|
|
||||||
sepia: {
|
|
||||||
bg: '#f5e6c8',
|
|
||||||
text: '#5b4636',
|
|
||||||
headerBg: '#e8d9b8',
|
|
||||||
border: '#d4c5a5'
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
bg: '#1a1a1a',
|
|
||||||
text: '#cccccc',
|
|
||||||
headerBg: '#252525',
|
|
||||||
border: '#333333'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 本地存储key
|
// 本地存储key
|
||||||
const SETTINGS_STORAGE_KEY = 'chapter-reader-settings';
|
const SETTINGS_STORAGE_KEY = 'chapter-reader-settings';
|
||||||
@@ -94,6 +79,8 @@ export default function ChapterReader({
|
|||||||
onClose,
|
onClose,
|
||||||
onChapterChange
|
onChapterChange
|
||||||
}: ChapterReaderProps) {
|
}: ChapterReaderProps) {
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
// 阅读器设置
|
// 阅读器设置
|
||||||
const [settings, setSettings] = useState<ReaderSettings>(loadSettings);
|
const [settings, setSettings] = useState<ReaderSettings>(loadSettings);
|
||||||
|
|
||||||
@@ -200,6 +187,26 @@ export default function ChapterReader({
|
|||||||
}, [chapter?.id]);
|
}, [chapter?.id]);
|
||||||
|
|
||||||
// 当前主题样式
|
// 当前主题样式
|
||||||
|
const themeStyles: Record<ReaderSettings['theme'], ReaderThemeStyle> = {
|
||||||
|
light: {
|
||||||
|
bg: token.colorBgContainer,
|
||||||
|
text: token.colorText,
|
||||||
|
headerBg: token.colorBgElevated,
|
||||||
|
border: token.colorBorderSecondary,
|
||||||
|
},
|
||||||
|
sepia: {
|
||||||
|
bg: `color-mix(in srgb, ${token.colorWarningBg} 72%, ${token.colorBgContainer} 28%)`,
|
||||||
|
text: `color-mix(in srgb, ${token.colorText} 85%, ${token.colorTextSecondary} 15%)`,
|
||||||
|
headerBg: `color-mix(in srgb, ${token.colorWarningBg} 58%, ${token.colorBgElevated} 42%)`,
|
||||||
|
border: `color-mix(in srgb, ${token.colorWarningBorder} 65%, ${token.colorBorder} 35%)`,
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
bg: `color-mix(in srgb, ${token.colorTextBase} 92%, ${token.colorBgContainer} 8%)`,
|
||||||
|
text: `color-mix(in srgb, ${token.colorTextLightSolid} 82%, ${token.colorTextSecondary} 18%)`,
|
||||||
|
headerBg: `color-mix(in srgb, ${token.colorTextBase} 84%, ${token.colorBgElevated} 16%)`,
|
||||||
|
border: `color-mix(in srgb, ${token.colorTextBase} 60%, ${token.colorBorder} 40%)`,
|
||||||
|
},
|
||||||
|
};
|
||||||
const currentTheme = themeStyles[settings.theme];
|
const currentTheme = themeStyles[settings.theme];
|
||||||
|
|
||||||
// 更新设置的便捷函数
|
// 更新设置的便捷函数
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Card, Space, Tag, Typography, Popconfirm } from 'antd';
|
import { Card, Space, Tag, Typography, Popconfirm, theme } from 'antd';
|
||||||
import { EditOutlined, DeleteOutlined, UserOutlined, BankOutlined, ExportOutlined } from '@ant-design/icons';
|
import { EditOutlined, DeleteOutlined, UserOutlined, BankOutlined, ExportOutlined } from '@ant-design/icons';
|
||||||
import { cardStyles } from './CardStyles';
|
import { characterCardStyles } from './CardStyles';
|
||||||
import type { Character } from '../types';
|
import type { Character } from '../types';
|
||||||
|
|
||||||
const { Text, Paragraph } = Typography;
|
const { Text, Paragraph } = Typography;
|
||||||
@@ -13,6 +13,8 @@ interface CharacterCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit, onDelete, onExport }) => {
|
export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit, onDelete, onExport }) => {
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
const getRoleTypeColor = (roleType?: string) => {
|
const getRoleTypeColor = (roleType?: string) => {
|
||||||
const roleColors: Record<string, string> = {
|
const roleColors: Record<string, string> = {
|
||||||
'protagonist': 'blue',
|
'protagonist': 'blue',
|
||||||
@@ -37,10 +39,10 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
|
|||||||
|
|
||||||
const getStatusTag = () => {
|
const getStatusTag = () => {
|
||||||
const statusConfig: Record<string, { color: string; label: string }> = {
|
const statusConfig: Record<string, { color: string; label: string }> = {
|
||||||
deceased: { color: '#000000', label: '💀 已死亡' },
|
deceased: { color: token.colorTextBase, label: '💀 已死亡' },
|
||||||
missing: { color: '#faad14', label: '❓ 已失踪' },
|
missing: { color: token.colorWarning, label: '❓ 已失踪' },
|
||||||
retired: { color: '#8c8c8c', label: '📤 已退场' },
|
retired: { color: token.colorTextTertiary, label: '📤 已退场' },
|
||||||
destroyed: { color: '#000000', label: '💀 已覆灭' },
|
destroyed: { color: token.colorTextBase, label: '💀 已覆灭' },
|
||||||
};
|
};
|
||||||
const config = statusConfig[charStatus];
|
const config = statusConfig[charStatus];
|
||||||
if (!config) return null;
|
if (!config) return null;
|
||||||
@@ -51,7 +53,7 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
|
|||||||
<Card
|
<Card
|
||||||
hoverable
|
hoverable
|
||||||
style={{
|
style={{
|
||||||
...(isOrganization ? cardStyles.organization : cardStyles.character),
|
...(isOrganization ? characterCardStyles.organizationCard : characterCardStyles.characterCard),
|
||||||
...(isInactive ? { opacity: 0.6, filter: 'grayscale(40%)' } : {}),
|
...(isInactive ? { opacity: 0.6, filter: 'grayscale(40%)' } : {}),
|
||||||
}}
|
}}
|
||||||
styles={{
|
styles={{
|
||||||
@@ -82,14 +84,14 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
|
|||||||
<Card.Meta
|
<Card.Meta
|
||||||
avatar={
|
avatar={
|
||||||
isOrganization ? (
|
isOrganization ? (
|
||||||
<BankOutlined style={{ fontSize: 32, color: '#52c41a' }} />
|
<BankOutlined style={{ fontSize: 32, color: token.colorSuccess }} />
|
||||||
) : (
|
) : (
|
||||||
<UserOutlined style={{ fontSize: 32, color: '#1890ff' }} />
|
<UserOutlined style={{ fontSize: 32, color: token.colorPrimary }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<span style={cardStyles.ellipsis}>{character.name}</span>
|
<span style={characterCardStyles.nameEllipsis}>{character.name}</span>
|
||||||
{isOrganization ? (
|
{isOrganization ? (
|
||||||
<Tag color="green">组织</Tag>
|
<Tag color="green">组织</Tag>
|
||||||
) : (
|
) : (
|
||||||
@@ -103,7 +105,7 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
|
|||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
description={
|
description={
|
||||||
<div style={cardStyles.description}>
|
<div style={characterCardStyles.descriptionBlock}>
|
||||||
{/* 角色特有字段 */}
|
{/* 角色特有字段 */}
|
||||||
{!isOrganization && (
|
{!isOrganization && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Card, Button, Modal, Form, Select, InputNumber, Input, message, Progress, Tag, Space, Divider, Typography } from 'antd';
|
import { Card, Button, Modal, Form, Select, InputNumber, Input, message, Progress, Tag, Space, Divider, Typography, theme } from 'antd';
|
||||||
import { EditOutlined, PlusOutlined, DeleteOutlined, TrophyOutlined } from '@ant-design/icons';
|
import { EditOutlined, PlusOutlined, DeleteOutlined, TrophyOutlined } from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ export const CharacterCareerCard: React.FC<Props> = ({
|
|||||||
editable = false,
|
editable = false,
|
||||||
onUpdate
|
onUpdate
|
||||||
}) => {
|
}) => {
|
||||||
|
const { token } = theme.useToken();
|
||||||
const [mainCareer, setMainCareer] = useState<CareerDetail | null>(null);
|
const [mainCareer, setMainCareer] = useState<CareerDetail | null>(null);
|
||||||
const [subCareers, setSubCareers] = useState<CareerDetail[]>([]);
|
const [subCareers, setSubCareers] = useState<CareerDetail[]>([]);
|
||||||
const [allCareers, setAllCareers] = useState<Career[]>([]);
|
const [allCareers, setAllCareers] = useState<Career[]>([]);
|
||||||
@@ -190,7 +191,7 @@ export const CharacterCareerCard: React.FC<Props> = ({
|
|||||||
<div key={career.id} style={{ marginBottom: 16 }}>
|
<div key={career.id} style={{ marginBottom: 16 }}>
|
||||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||||
<Space>
|
<Space>
|
||||||
<TrophyOutlined style={{ color: isMain ? '#1890ff' : '#8c8c8c' }} />
|
<TrophyOutlined style={{ color: isMain ? token.colorPrimary : token.colorTextTertiary }} />
|
||||||
<Text strong={isMain}>{career.career_name}</Text>
|
<Text strong={isMain}>{career.career_name}</Text>
|
||||||
{isMain && <Tag color="blue">主</Tag>}
|
{isMain && <Tag color="blue">主</Tag>}
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { Drawer, Input, List, Typography, Empty, Tag } from 'antd';
|
import { Drawer, Input, List, Typography, Empty, Tag, theme } from 'antd';
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
import type { Chapter } from '../types';
|
import type { Chapter } from '../types';
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ export default function FloatingIndexPanel({
|
|||||||
groupedChapters,
|
groupedChapters,
|
||||||
onChapterSelect,
|
onChapterSelect,
|
||||||
}: FloatingIndexPanelProps) {
|
}: FloatingIndexPanelProps) {
|
||||||
|
const { token } = theme.useToken();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
const filteredGroups = useMemo(() => {
|
const filteredGroups = useMemo(() => {
|
||||||
@@ -56,7 +57,7 @@ export default function FloatingIndexPanel({
|
|||||||
body: { padding: 0 },
|
body: { padding: 0 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ padding: '16px', borderBottom: '1px solid #f0f0f0' }}>
|
<div style={{ padding: '16px', borderBottom: `1px solid ${token.colorBorderSecondary}` }}>
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索章节标题"
|
placeholder="搜索章节标题"
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined />}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useMemo, useEffect, useRef } from 'react';
|
import React, { useMemo, useEffect, useRef } from 'react';
|
||||||
import { Card, Tag, Badge, Empty, Collapse, Divider } from 'antd';
|
import { Card, Tag, Badge, Empty, Collapse, Divider, theme } from 'antd';
|
||||||
import {
|
import {
|
||||||
FireOutlined,
|
FireOutlined,
|
||||||
StarOutlined,
|
StarOutlined,
|
||||||
@@ -22,22 +22,18 @@ const TYPE_CONFIG = {
|
|||||||
hook: {
|
hook: {
|
||||||
label: '钩子',
|
label: '钩子',
|
||||||
icon: <FireOutlined />,
|
icon: <FireOutlined />,
|
||||||
color: '#ff6b6b',
|
|
||||||
},
|
},
|
||||||
foreshadow: {
|
foreshadow: {
|
||||||
label: '伏笔',
|
label: '伏笔',
|
||||||
icon: <StarOutlined />,
|
icon: <StarOutlined />,
|
||||||
color: '#6b7bff',
|
|
||||||
},
|
},
|
||||||
plot_point: {
|
plot_point: {
|
||||||
label: '情节点',
|
label: '情节点',
|
||||||
icon: <ThunderboltOutlined />,
|
icon: <ThunderboltOutlined />,
|
||||||
color: '#51cf66',
|
|
||||||
},
|
},
|
||||||
character_event: {
|
character_event: {
|
||||||
label: '角色事件',
|
label: '角色事件',
|
||||||
icon: <UserOutlined />,
|
icon: <UserOutlined />,
|
||||||
color: '#ffd93d',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,7 +47,14 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
|||||||
onAnnotationClick,
|
onAnnotationClick,
|
||||||
scrollToAnnotation,
|
scrollToAnnotation,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { token } = theme.useToken();
|
||||||
const cardRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
const cardRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||||
|
const typeColors: Record<keyof typeof TYPE_CONFIG, string> = {
|
||||||
|
hook: token.colorError,
|
||||||
|
foreshadow: token.colorInfo,
|
||||||
|
plot_point: token.colorSuccess,
|
||||||
|
character_event: token.colorWarning,
|
||||||
|
};
|
||||||
|
|
||||||
// 当需要滚动到特定标注卡片时
|
// 当需要滚动到特定标注卡片时
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -100,6 +103,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
|||||||
// 渲染单个记忆卡片
|
// 渲染单个记忆卡片
|
||||||
const renderMemoryCard = (annotation: MemoryAnnotation) => {
|
const renderMemoryCard = (annotation: MemoryAnnotation) => {
|
||||||
const config = TYPE_CONFIG[annotation.type];
|
const config = TYPE_CONFIG[annotation.type];
|
||||||
|
const color = typeColors[annotation.type];
|
||||||
const isActive = activeAnnotationId === annotation.id;
|
const isActive = activeAnnotationId === annotation.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -115,8 +119,8 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
|||||||
onClick={() => onAnnotationClick?.(annotation)}
|
onClick={() => onAnnotationClick?.(annotation)}
|
||||||
style={{
|
style={{
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
borderLeft: `4px solid ${config.color}`,
|
borderLeft: `4px solid ${color}`,
|
||||||
backgroundColor: isActive ? `${config.color}11` : 'transparent',
|
backgroundColor: isActive ? `color-mix(in srgb, ${color} 8%, transparent)` : 'transparent',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.2s',
|
transition: 'all 0.2s',
|
||||||
}}
|
}}
|
||||||
@@ -126,7 +130,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
|||||||
<Badge
|
<Badge
|
||||||
count={`${(annotation.importance * 10).toFixed(1)}`}
|
count={`${(annotation.importance * 10).toFixed(1)}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: config.color,
|
backgroundColor: color,
|
||||||
float: 'right',
|
float: 'right',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -138,7 +142,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: '#666',
|
color: token.colorTextSecondary,
|
||||||
lineHeight: 1.6,
|
lineHeight: 1.6,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
}}
|
}}
|
||||||
@@ -160,7 +164,7 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
|||||||
|
|
||||||
{/* 特殊元数据 */}
|
{/* 特殊元数据 */}
|
||||||
{annotation.metadata.strength && (
|
{annotation.metadata.strength && (
|
||||||
<div style={{ marginTop: 4, fontSize: 11, color: '#999' }}>
|
<div style={{ marginTop: 4, fontSize: 11, color: token.colorTextTertiary }}>
|
||||||
强度: {annotation.metadata.strength}/10
|
强度: {annotation.metadata.strength}/10
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -192,27 +196,27 @@ const MemorySidebar: React.FC<MemorySidebarProps> = ({
|
|||||||
<div style={{ fontWeight: 600, marginBottom: 12 }}>📊 分析概览</div>
|
<div style={{ fontWeight: 600, marginBottom: 12 }}>📊 分析概览</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 12, color: '#999' }}>钩子</div>
|
<div style={{ fontSize: 12, color: token.colorTextTertiary }}>钩子</div>
|
||||||
<div style={{ fontSize: 20, fontWeight: 600, color: TYPE_CONFIG.hook.color }}>
|
<div style={{ fontSize: 20, fontWeight: 600, color: typeColors.hook }}>
|
||||||
{stats.hooks}
|
{stats.hooks}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 12, color: '#999' }}>伏笔</div>
|
<div style={{ fontSize: 12, color: token.colorTextTertiary }}>伏笔</div>
|
||||||
<div style={{ fontSize: 20, fontWeight: 600, color: TYPE_CONFIG.foreshadow.color }}>
|
<div style={{ fontSize: 20, fontWeight: 600, color: typeColors.foreshadow }}>
|
||||||
{stats.foreshadows}
|
{stats.foreshadows}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 12, color: '#999' }}>情节点</div>
|
<div style={{ fontSize: 12, color: token.colorTextTertiary }}>情节点</div>
|
||||||
<div style={{ fontSize: 20, fontWeight: 600, color: TYPE_CONFIG.plot_point.color }}>
|
<div style={{ fontSize: 20, fontWeight: 600, color: typeColors.plot_point }}>
|
||||||
{stats.plotPoints}
|
{stats.plotPoints}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 12, color: '#999' }}>角色事件</div>
|
<div style={{ fontSize: 12, color: token.colorTextTertiary }}>角色事件</div>
|
||||||
<div
|
<div
|
||||||
style={{ fontSize: 20, fontWeight: 600, color: TYPE_CONFIG.character_event.color }}
|
style={{ fontSize: 20, fontWeight: 600, color: typeColors.character_event }}
|
||||||
>
|
>
|
||||||
{stats.characterEvents}
|
{stats.characterEvents}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { Modal, Input, Button, Space, Radio, InputNumber, Card, message, Alert, Spin, Typography, Divider } from 'antd';
|
import { Modal, Input, Button, Space, Radio, InputNumber, Card, message, Alert, Spin, Typography, Divider, theme } from 'antd';
|
||||||
import { ThunderboltOutlined, CheckOutlined, ReloadOutlined, EditOutlined, LoadingOutlined } from '@ant-design/icons';
|
import { ThunderboltOutlined, CheckOutlined, ReloadOutlined, EditOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||||
import { chapterApi } from '../services/api';
|
import { chapterApi } from '../services/api';
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onApply,
|
onApply,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { token } = theme.useToken();
|
||||||
const [userInstructions, setUserInstructions] = useState('');
|
const [userInstructions, setUserInstructions] = useState('');
|
||||||
const [lengthMode, setLengthMode] = useState<LengthMode>('similar');
|
const [lengthMode, setLengthMode] = useState<LengthMode>('similar');
|
||||||
const [customWordCount, setCustomWordCount] = useState<number>(selectedText.length);
|
const [customWordCount, setCustomWordCount] = useState<number>(selectedText.length);
|
||||||
@@ -178,7 +179,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
|||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<EditOutlined style={{ color: 'var(--color-primary)' }} />
|
<EditOutlined style={{ color: token.colorPrimary }} />
|
||||||
<span>AI局部重写</span>
|
<span>AI局部重写</span>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -202,9 +203,9 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
|||||||
loading={isGenerating}
|
loading={isGenerating}
|
||||||
disabled={!userInstructions.trim()}
|
disabled={!userInstructions.trim()}
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
boxShadow: '0 4px 12px rgba(77, 128, 136, 0.3)',
|
boxShadow: token.boxShadowSecondary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isGenerating ? '生成中...' : '开始重写'}
|
{isGenerating ? '生成中...' : '开始重写'}
|
||||||
@@ -221,7 +222,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
|||||||
type="primary"
|
type="primary"
|
||||||
icon={<CheckOutlined />}
|
icon={<CheckOutlined />}
|
||||||
onClick={handleAccept}
|
onClick={handleAccept}
|
||||||
style={{ background: '#52c41a', borderColor: '#52c41a' }}
|
style={{ background: token.colorSuccess, borderColor: token.colorSuccess }}
|
||||||
>
|
>
|
||||||
接受并应用
|
接受并应用
|
||||||
</Button>
|
</Button>
|
||||||
@@ -250,7 +251,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
|||||||
body: {
|
body: {
|
||||||
maxHeight: 150,
|
maxHeight: 150,
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
background: '#fafafa',
|
background: token.colorFillAlter,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -258,7 +259,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
|||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
color: '#595959',
|
color: token.colorText,
|
||||||
lineHeight: 1.8,
|
lineHeight: 1.8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -352,7 +353,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: 4,
|
height: 4,
|
||||||
background: '#f0f0f0',
|
background: token.colorFillTertiary,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
@@ -360,7 +361,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
background: 'linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
background: `linear-gradient(90deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
|
||||||
width: `${progress}%`,
|
width: `${progress}%`,
|
||||||
transition: 'width 0.3s ease',
|
transition: 'width 0.3s ease',
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
@@ -374,8 +375,8 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
|||||||
size="small"
|
size="small"
|
||||||
ref={generatedTextRef}
|
ref={generatedTextRef}
|
||||||
style={{
|
style={{
|
||||||
background: generatedText ? '#f6ffed' : '#fafafa',
|
background: generatedText ? token.colorSuccessBg : token.colorFillAlter,
|
||||||
border: generatedText ? '1px solid #b7eb8f' : '1px solid #d9d9d9',
|
border: generatedText ? `1px solid ${token.colorSuccessBorder}` : `1px solid ${token.colorBorder}`,
|
||||||
}}
|
}}
|
||||||
styles={{
|
styles={{
|
||||||
body: {
|
body: {
|
||||||
@@ -400,7 +401,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
|||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
width: 8,
|
width: 8,
|
||||||
height: 16,
|
height: 16,
|
||||||
background: 'var(--color-primary)',
|
background: token.colorPrimary,
|
||||||
marginLeft: 2,
|
marginLeft: 2,
|
||||||
animation: 'blink 1s infinite',
|
animation: 'blink 1s infinite',
|
||||||
}}
|
}}
|
||||||
@@ -408,7 +409,7 @@ export const PartialRegenerateModal: React.FC<PartialRegenerateModalProps> = ({
|
|||||||
)}
|
)}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ textAlign: 'center', padding: 20, color: '#8c8c8c' }}>
|
<div style={{ textAlign: 'center', padding: 20, color: token.colorTextTertiary }}>
|
||||||
{isGenerating ? '正在生成内容...' : '等待生成...'}
|
{isGenerating ? '正在生成内容...' : '等待生成...'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Tooltip } from 'antd';
|
import { Button, Tooltip, theme } from 'antd';
|
||||||
import { EditOutlined } from '@ant-design/icons';
|
import { EditOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
interface PartialRegenerateToolbarProps {
|
interface PartialRegenerateToolbarProps {
|
||||||
@@ -19,6 +19,8 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
|
|||||||
onRegenerate,
|
onRegenerate,
|
||||||
selectedText
|
selectedText
|
||||||
}) => {
|
}) => {
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
if (!visible || !selectedText) return null;
|
if (!visible || !selectedText) return null;
|
||||||
|
|
||||||
// 限制显示的选中文本长度
|
// 限制显示的选中文本长度
|
||||||
@@ -33,15 +35,15 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
|
|||||||
top: position.top,
|
top: position.top,
|
||||||
left: position.left,
|
left: position.left,
|
||||||
zIndex: 10000,
|
zIndex: 10000,
|
||||||
background: '#fff',
|
background: token.colorBgElevated,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.15)',
|
boxShadow: token.boxShadow,
|
||||||
padding: '6px 8px',
|
padding: '6px 8px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
animation: 'fadeIn 0.2s ease-out',
|
animation: 'fadeIn 0.2s ease-out',
|
||||||
border: '1px solid #e8e8e8',
|
border: `1px solid ${token.colorBorderSecondary}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -61,7 +63,7 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
|
|||||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
boxShadow: '0 4px 12px rgba(77, 128, 136, 0.3)',
|
boxShadow: token.boxShadowSecondary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
AI重写
|
AI重写
|
||||||
@@ -69,7 +71,7 @@ export const PartialRegenerateToolbar: React.FC<PartialRegenerateToolbarProps> =
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span style={{
|
<span style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#8c8c8c',
|
color: token.colorTextTertiary,
|
||||||
maxWidth: 150,
|
maxWidth: 150,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Spin } from 'antd';
|
import { Spin, theme } from 'antd';
|
||||||
import { LoadingOutlined } from '@ant-design/icons';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
interface SSELoadingOverlayProps {
|
interface SSELoadingOverlayProps {
|
||||||
@@ -13,6 +13,8 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
|||||||
progress,
|
progress,
|
||||||
message
|
message
|
||||||
}) => {
|
}) => {
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
if (!loading) return null;
|
if (!loading) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -22,19 +24,19 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
background: 'rgba(0, 0, 0, 0.45)',
|
background: token.colorBgMask,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
zIndex: 9999
|
zIndex: 9999
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
background: '#fff',
|
background: token.colorBgElevated,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: '40px 60px',
|
padding: '40px 60px',
|
||||||
minWidth: 400,
|
minWidth: 400,
|
||||||
maxWidth: 600,
|
maxWidth: 600,
|
||||||
boxShadow: '0 8px 32px rgba(0,0,0,0.2)'
|
boxShadow: token.boxShadowSecondary
|
||||||
}}>
|
}}>
|
||||||
{/* 标题和图标 */}
|
{/* 标题和图标 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -42,13 +44,13 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
|||||||
marginBottom: 24
|
marginBottom: 24
|
||||||
}}>
|
}}>
|
||||||
<Spin
|
<Spin
|
||||||
indicator={<LoadingOutlined style={{ fontSize: 48, color: 'var(--color-primary)' }} spin />}
|
indicator={<LoadingOutlined style={{ fontSize: 48, color: token.colorPrimary }} spin />}
|
||||||
/>
|
/>
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
color: 'var(--color-text-primary)'
|
color: token.colorTextHeading
|
||||||
}}>
|
}}>
|
||||||
AI生成中...
|
AI生成中...
|
||||||
</div>
|
</div>
|
||||||
@@ -60,7 +62,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
|||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
height: 12,
|
height: 12,
|
||||||
background: 'var(--color-bg-layout)',
|
background: token.colorFillTertiary,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
marginBottom: 12
|
marginBottom: 12
|
||||||
@@ -68,12 +70,12 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
|||||||
<div style={{
|
<div style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
background: progress === 100
|
background: progress === 100
|
||||||
? 'linear-gradient(90deg, var(--color-success) 0%, var(--color-success-active) 100%)'
|
? `linear-gradient(90deg, ${token.colorSuccess} 0%, ${token.colorSuccessActive} 100%)`
|
||||||
: 'linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-active) 100%)',
|
: `linear-gradient(90deg, ${token.colorPrimary} 0%, ${token.colorPrimaryActive} 100%)`,
|
||||||
width: `${progress}%`,
|
width: `${progress}%`,
|
||||||
transition: 'all 0.3s ease',
|
transition: 'all 0.3s ease',
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
boxShadow: progress > 0 ? 'var(--shadow-card)' : 'none'
|
boxShadow: progress > 0 ? token.boxShadow : 'none'
|
||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -82,7 +84,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: progress === 100 ? 'var(--color-success)' : 'var(--color-primary)',
|
color: progress === 100 ? token.colorSuccess : token.colorPrimary,
|
||||||
marginBottom: 8
|
marginBottom: 8
|
||||||
}}>
|
}}>
|
||||||
{progress}%
|
{progress}%
|
||||||
@@ -93,7 +95,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
|||||||
<div style={{
|
<div style={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#595959',
|
color: token.colorText,
|
||||||
minHeight: 24,
|
minHeight: 24,
|
||||||
padding: '0 20px'
|
padding: '0 20px'
|
||||||
}}>
|
}}>
|
||||||
@@ -104,7 +106,7 @@ export const SSELoadingOverlay: React.FC<SSELoadingOverlayProps> = ({
|
|||||||
<div style={{
|
<div style={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: '#8c8c8c',
|
color: token.colorTextTertiary,
|
||||||
marginTop: 16
|
marginTop: 16
|
||||||
}}>
|
}}>
|
||||||
请勿关闭页面,生成过程需要一定时间
|
请勿关闭页面,生成过程需要一定时间
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { theme } from 'antd';
|
||||||
|
|
||||||
interface SSEProgressBarProps {
|
interface SSEProgressBarProps {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -11,6 +12,8 @@ export const SSEProgressBar: React.FC<SSEProgressBarProps> = ({
|
|||||||
progress,
|
progress,
|
||||||
message
|
message
|
||||||
}) => {
|
}) => {
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
if (!loading) return null;
|
if (!loading) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -18,14 +21,14 @@ export const SSEProgressBar: React.FC<SSEProgressBarProps> = ({
|
|||||||
{/* 进度条 */}
|
{/* 进度条 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
height: 8,
|
height: 8,
|
||||||
background: '#f0f0f0',
|
background: token.colorFillTertiary,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
marginBottom: 8
|
marginBottom: 8
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
background: progress === 100 ? '#52c41a' : '#1890ff',
|
background: progress === 100 ? token.colorSuccess : token.colorPrimary,
|
||||||
width: `${progress}%`,
|
width: `${progress}%`,
|
||||||
transition: 'all 0.3s ease',
|
transition: 'all 0.3s ease',
|
||||||
borderRadius: 4
|
borderRadius: 4
|
||||||
@@ -39,12 +42,12 @@ export const SSEProgressBar: React.FC<SSEProgressBarProps> = ({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
fontSize: 14
|
fontSize: 14
|
||||||
}}>
|
}}>
|
||||||
<span style={{ color: '#666' }}>
|
<span style={{ color: token.colorTextSecondary }}>
|
||||||
{message || '准备生成...'}
|
{message || '准备生成...'}
|
||||||
</span>
|
</span>
|
||||||
<span style={{
|
<span style={{
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: progress === 100 ? '#52c41a' : '#1890ff'
|
color: progress === 100 ? token.colorSuccess : token.colorPrimary
|
||||||
}}>
|
}}>
|
||||||
{progress}%
|
{progress}%
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Modal, Spin, Button } from 'antd';
|
import { Modal, Spin, Button, theme } from 'antd';
|
||||||
import { LoadingOutlined, StopOutlined } from '@ant-design/icons';
|
import { LoadingOutlined, StopOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
interface SSEProgressModalProps {
|
interface SSEProgressModalProps {
|
||||||
@@ -27,6 +27,8 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
|||||||
onCancel,
|
onCancel,
|
||||||
cancelButtonText = '取消任务',
|
cancelButtonText = '取消任务',
|
||||||
}) => {
|
}) => {
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
if (!visible) return null;
|
if (!visible) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -53,13 +55,13 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
|||||||
marginBottom: 24
|
marginBottom: 24
|
||||||
}}>
|
}}>
|
||||||
<Spin
|
<Spin
|
||||||
indicator={<LoadingOutlined style={{ fontSize: 48, color: 'var(--color-primary)' }} spin />}
|
indicator={<LoadingOutlined style={{ fontSize: 48, color: token.colorPrimary }} spin />}
|
||||||
/>
|
/>
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
color: 'var(--color-text-primary)'
|
color: token.colorText
|
||||||
}}>
|
}}>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
@@ -72,7 +74,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
|||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
height: 12,
|
height: 12,
|
||||||
background: 'var(--color-bg-layout)',
|
background: token.colorBgLayout,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
marginBottom: showPercentage ? 12 : 0
|
marginBottom: showPercentage ? 12 : 0
|
||||||
@@ -80,12 +82,12 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
|||||||
<div style={{
|
<div style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
background: progress === 100
|
background: progress === 100
|
||||||
? 'linear-gradient(90deg, var(--color-success) 0%, var(--color-success-active) 100%)'
|
? `linear-gradient(90deg, ${token.colorSuccess} 0%, ${token.colorSuccess} 100%)`
|
||||||
: 'linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-active) 100%)',
|
: `linear-gradient(90deg, ${token.colorPrimary} 0%, ${token.colorPrimary} 100%)`,
|
||||||
width: `${progress}%`,
|
width: `${progress}%`,
|
||||||
transition: 'all 0.3s ease',
|
transition: 'all 0.3s ease',
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
boxShadow: progress > 0 ? 'var(--shadow-card)' : 'none'
|
boxShadow: progress > 0 ? token.boxShadow : 'none'
|
||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -95,7 +97,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: progress === 100 ? 'var(--color-success)' : 'var(--color-primary)',
|
color: progress === 100 ? token.colorSuccess : token.colorPrimary,
|
||||||
marginBottom: 8
|
marginBottom: 8
|
||||||
}}>
|
}}>
|
||||||
{progress}%
|
{progress}%
|
||||||
@@ -107,7 +109,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
|||||||
<div style={{
|
<div style={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: 'var(--color-text-secondary)',
|
color: token.colorTextSecondary,
|
||||||
minHeight: 24,
|
minHeight: 24,
|
||||||
padding: '0 20px',
|
padding: '0 20px',
|
||||||
marginBottom: 16
|
marginBottom: 16
|
||||||
@@ -119,7 +121,7 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
|||||||
<div style={{
|
<div style={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: 'var(--color-text-tertiary)',
|
color: token.colorTextTertiary,
|
||||||
marginBottom: onCancel ? 16 : 0
|
marginBottom: onCancel ? 16 : 0
|
||||||
}}>
|
}}>
|
||||||
请勿关闭页面,生成过程需要一定时间
|
请勿关闭页面,生成过程需要一定时间
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Dropdown, Avatar, Space, Typography, message, Modal, Form, Input, Button } from 'antd';
|
import { Dropdown, Avatar, Space, Typography, message, Modal, Form, Input, Button, theme } from 'antd';
|
||||||
import { UserOutlined, LogoutOutlined, TeamOutlined, CrownOutlined, LockOutlined } from '@ant-design/icons';
|
import { UserOutlined, LogoutOutlined, TeamOutlined, CrownOutlined, LockOutlined } from '@ant-design/icons';
|
||||||
import { authApi } from '../services/api';
|
import { authApi } from '../services/api';
|
||||||
import type { User } from '../types';
|
import type { User } from '../types';
|
||||||
@@ -11,14 +11,18 @@ const { Text } = Typography;
|
|||||||
interface UserMenuProps {
|
interface UserMenuProps {
|
||||||
/** 是否总是显示完整信息(用于移动端侧边栏) */
|
/** 是否总是显示完整信息(用于移动端侧边栏) */
|
||||||
showFullInfo?: boolean;
|
showFullInfo?: boolean;
|
||||||
|
/** 紧凑模式(用于折叠侧边栏,仅展示头像) */
|
||||||
|
compact?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UserMenu({ showFullInfo = false }: UserMenuProps) {
|
export default function UserMenu({ showFullInfo = false, compact = false }: UserMenuProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||||
const [showChangePassword, setShowChangePassword] = useState(false);
|
const [showChangePassword, setShowChangePassword] = useState(false);
|
||||||
const [changePasswordForm] = Form.useForm();
|
const [changePasswordForm] = Form.useForm();
|
||||||
const [changingPassword, setChangingPassword] = useState(false);
|
const [changingPassword, setChangingPassword] = useState(false);
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadCurrentUser();
|
loadCurrentUser();
|
||||||
@@ -126,36 +130,36 @@ export default function UserMenu({ showFullInfo = false }: UserMenuProps) {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 12,
|
gap: compact ? 0 : 12,
|
||||||
padding: '8px 16px',
|
padding: compact ? '4px' : '8px 16px',
|
||||||
background: 'rgba(255, 255, 255, 0.6)', // 保持半透明以配合 Backdrop
|
background: alphaColor(token.colorBgContainer, 0.65), // 保持半透明以配合 Backdrop
|
||||||
backdropFilter: 'blur(10px)',
|
backdropFilter: 'blur(10px)',
|
||||||
WebkitBackdropFilter: 'blur(10px)',
|
WebkitBackdropFilter: 'blur(10px)',
|
||||||
borderRadius: 24,
|
borderRadius: compact ? 16 : 24,
|
||||||
border: '1px solid var(--color-border)',
|
border: `1px solid ${token.colorBorder}`,
|
||||||
transition: 'all 0.3s ease',
|
transition: 'all 0.3s ease',
|
||||||
boxShadow: 'var(--shadow-card)',
|
boxShadow: `0 8px 20px ${alphaColor(token.colorText, 0.08)}`,
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.background = 'var(--color-bg-container)'; // 悬浮时变实
|
e.currentTarget.style.background = token.colorBgContainer; // 悬浮时变实
|
||||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||||
e.currentTarget.style.boxShadow = 'var(--shadow-elevated)';
|
e.currentTarget.style.boxShadow = `0 12px 28px ${alphaColor(token.colorText, 0.14)}`;
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.6)';
|
e.currentTarget.style.background = alphaColor(token.colorBgContainer, 0.65);
|
||||||
e.currentTarget.style.transform = 'translateY(0)';
|
e.currentTarget.style.transform = 'translateY(0)';
|
||||||
e.currentTarget.style.boxShadow = 'var(--shadow-card)';
|
e.currentTarget.style.boxShadow = `0 8px 20px ${alphaColor(token.colorText, 0.08)}`;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={currentUser.avatar_url}
|
src={currentUser.avatar_url}
|
||||||
icon={<UserOutlined />}
|
icon={<UserOutlined />}
|
||||||
size={40}
|
size={compact ? 32 : 40}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--color-primary)',
|
backgroundColor: token.colorPrimary,
|
||||||
border: '3px solid #fff',
|
border: `3px solid ${token.colorWhite}`,
|
||||||
boxShadow: 'var(--shadow-card)',
|
boxShadow: `0 8px 20px ${alphaColor(token.colorText, 0.12)}`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{currentUser.is_admin && (
|
{currentUser.is_admin && (
|
||||||
@@ -165,28 +169,28 @@ export default function UserMenu({ showFullInfo = false }: UserMenuProps) {
|
|||||||
right: -2,
|
right: -2,
|
||||||
width: 18,
|
width: 18,
|
||||||
height: 18,
|
height: 18,
|
||||||
background: 'linear-gradient(135deg, #ffd700 0%, #ffaa00 100%)',
|
background: `linear-gradient(135deg, ${token.colorWarning} 0%, ${token.colorWarningHover} 100%)`,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
border: '2px solid white',
|
border: `2px solid ${token.colorWhite}`,
|
||||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
|
boxShadow: `0 2px 4px ${alphaColor(token.colorText, 0.2)}`,
|
||||||
}}>
|
}}>
|
||||||
<CrownOutlined style={{ fontSize: 9, color: '#fff' }} />
|
<CrownOutlined style={{ fontSize: 9, color: token.colorWhite }} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Space direction="vertical" size={0} style={{ display: (window.innerWidth <= 768 && !showFullInfo) ? 'none' : 'flex' }}>
|
<Space direction="vertical" size={0} style={{ display: compact ? 'none' : ((window.innerWidth <= 768 && !showFullInfo) ? 'none' : 'flex') }}>
|
||||||
<Text strong style={{
|
<Text strong style={{
|
||||||
color: 'var(--color-text-primary)',
|
color: token.colorText,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
lineHeight: '20px',
|
lineHeight: '20px',
|
||||||
}}>
|
}}>
|
||||||
{currentUser.display_name || currentUser.username}
|
{currentUser.display_name || currentUser.username}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{
|
<Text style={{
|
||||||
color: 'var(--color-text-secondary)',
|
color: token.colorTextSecondary,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
lineHeight: '18px',
|
lineHeight: '18px',
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
+141
-330
@@ -1,70 +1,17 @@
|
|||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* --- 中国风配色方案 (Chinese Style Palette) --- */
|
|
||||||
|
|
||||||
/* 主色调:天青 (Cerulean / Azure) - 类似汝窑 */
|
|
||||||
--color-primary: #4D8088;
|
|
||||||
--color-primary-hover: #5F9EA8;
|
|
||||||
--color-primary-active: #3A666C;
|
|
||||||
|
|
||||||
/* 辅助色 */
|
|
||||||
--color-success: #52C41A;
|
|
||||||
--color-warning: #FAAD14;
|
|
||||||
--color-error: #FF4D4F;
|
|
||||||
--color-info: #1890FF;
|
|
||||||
|
|
||||||
/* 功能色背景/边框 */
|
|
||||||
--color-success-bg: #F6FFED;
|
|
||||||
--color-success-border: #B7EB8F;
|
|
||||||
--color-warning-bg: #FFFBE6;
|
|
||||||
--color-warning-border: #FFE58F;
|
|
||||||
--color-error-bg: #FFF2F0;
|
|
||||||
--color-error-border: #FFCCC7;
|
|
||||||
--color-info-bg: #E6F7FF;
|
|
||||||
--color-info-border: #91D5FF;
|
|
||||||
|
|
||||||
/* 背景色 */
|
|
||||||
--color-bg-base: #F8F6F1;
|
|
||||||
/* 米汤色 (Rice Soup / Cream) - 用于页面背景 */
|
|
||||||
--color-bg-container: #FFFFFF;
|
|
||||||
/* 纯白 - 用于卡片/容器 */
|
|
||||||
--color-bg-layout: #F0F2F5;
|
|
||||||
--color-bg-spotlight: #3A666C;
|
|
||||||
--color-bg-mask: rgba(0, 0, 0, 0.45);
|
|
||||||
|
|
||||||
/* 文本色 */
|
|
||||||
--color-text-base: #2B2B2B;
|
|
||||||
/* 墨色 (Ink) */
|
|
||||||
--color-text-primary: #2B2B2B;
|
|
||||||
--color-text-secondary: #595959;
|
|
||||||
/* 此时 (Secondary Text) */
|
|
||||||
--color-text-tertiary: #8C8C8C;
|
|
||||||
--color-text-quaternary: #BFBFBF;
|
|
||||||
|
|
||||||
/* 边框色 */
|
|
||||||
--color-border: #D9D9D9;
|
|
||||||
--color-border-secondary: #F0F0F0;
|
|
||||||
|
|
||||||
/* 阴影 */
|
|
||||||
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
||||||
--shadow-elevated: 0 8px 24px rgba(77, 128, 136, 0.15);
|
|
||||||
/* 带一点主色调的阴影 */
|
|
||||||
--shadow-primary: 0 4px 16px rgba(77, 128, 136, 0.25);
|
|
||||||
--shadow-header: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
||||||
|
|
||||||
font-family: "PingFang SC", "Microsoft YaHei", "Heiti SC", Inter, system-ui, sans-serif;
|
font-family: "PingFang SC", "Microsoft YaHei", "Heiti SC", Inter, system-ui, sans-serif;
|
||||||
line-height: 1.5715;
|
line-height: 1.5715;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
color-scheme: light;
|
|
||||||
color: var(--color-text-base);
|
|
||||||
background-color: var(--color-bg-base);
|
|
||||||
|
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
||||||
/* 移动端视口适配 */
|
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
-ms-text-size-adjust: 100%;
|
-ms-text-size-adjust: 100%;
|
||||||
}
|
}
|
||||||
@@ -72,43 +19,123 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
/* 禁止移动端双击缩放 */
|
|
||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 自定义滚动条样式 */
|
/* 自定义滚动条样式(与主题弱耦合) */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #f1f1f1;
|
background: rgba(0, 0, 0, 0.06);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #c1c1c1;
|
background: rgba(0, 0, 0, 0.24);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background 0.3s ease;
|
transition: background 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #a8a8a8;
|
background: rgba(0, 0, 0, 0.34);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Firefox 滚动条样式 */
|
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #c1c1c1 #f1f1f1;
|
scrollbar-color: rgba(0, 0, 0, 0.24) rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme-resolved='dark'] ::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme-resolved='dark'] ::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme-resolved='dark'] ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme-resolved='dark'] * {
|
||||||
|
scrollbar-color: rgba(255, 255, 255, 0.28) rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主题切换转场(暗色扩散、亮色收缩) */
|
||||||
|
:root {
|
||||||
|
--theme-transition-duration: 460ms;
|
||||||
|
--theme-transition-easing: cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root::view-transition-group(root) {
|
||||||
|
animation-duration: var(--theme-transition-duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme-transition='to-dark']::view-transition-new(root) {
|
||||||
|
z-index: 2;
|
||||||
|
animation: theme-dark-reveal var(--theme-transition-duration) var(--theme-transition-easing) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme-transition='to-dark']::view-transition-old(root) {
|
||||||
|
z-index: 1;
|
||||||
|
animation: theme-old-fade var(--theme-transition-duration) ease both;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme-transition='to-light']::view-transition-old(root) {
|
||||||
|
z-index: 2;
|
||||||
|
animation: theme-light-collapse var(--theme-transition-duration) var(--theme-transition-easing) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme-transition='to-light']::view-transition-new(root) {
|
||||||
|
z-index: 1;
|
||||||
|
animation: theme-new-fade var(--theme-transition-duration) ease both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes theme-dark-reveal {
|
||||||
|
from {
|
||||||
|
clip-path: circle(0% at 50% 50%);
|
||||||
|
opacity: 0.78;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
clip-path: circle(150% at 50% 50%);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes theme-light-collapse {
|
||||||
|
from {
|
||||||
|
clip-path: circle(150% at 50% 50%);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
clip-path: circle(0% at 50% 50%);
|
||||||
|
opacity: 0.82;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes theme-old-fade {
|
||||||
|
from { opacity: 1; }
|
||||||
|
to { opacity: 0.9; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes theme-new-fade {
|
||||||
|
from { opacity: 0.92; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
:root::view-transition-group(root),
|
||||||
|
:root::view-transition-old(root),
|
||||||
|
:root::view-transition-new(root) {
|
||||||
|
animation-duration: 1ms !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端响应式样式 */
|
/* 移动端响应式样式 */
|
||||||
@@ -117,38 +144,17 @@ body {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端隐藏滚动条 */
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 4px;
|
width: 4px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端优化触摸区域 */
|
|
||||||
button,
|
button,
|
||||||
a,
|
a,
|
||||||
[role="button"] {
|
[role='button'] {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
min-width: 44px;
|
min-width: 44px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
:root {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移动端安全区域适配 (iPhone X+) */
|
|
||||||
@supports (padding: max(0px)) {
|
|
||||||
body {
|
|
||||||
padding-left: env(safe-area-inset-left);
|
|
||||||
padding-right: env(safe-area-inset-right);
|
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移动端禁止长按选择 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
|
|
||||||
img,
|
img,
|
||||||
button {
|
button {
|
||||||
@@ -184,250 +190,55 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
:root {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端安全区域适配 (iPhone X+) */
|
||||||
|
@supports (padding: max(0px)) {
|
||||||
|
body {
|
||||||
|
padding-left: env(safe-area-inset-left);
|
||||||
|
padding-right: env(safe-area-inset-right);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ant-tabs-dropdown {
|
.ant-tabs-dropdown {
|
||||||
z-index: 2000 !important;
|
z-index: 2000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 现代化侧边栏样式 (Modern Sidebar Styles) ===== */
|
.ant-tooltip {
|
||||||
|
max-width: min(420px, calc(100vw - 24px));
|
||||||
/* 侧边栏容器 - 毛玻璃效果 */
|
|
||||||
.modern-sider {
|
|
||||||
background: linear-gradient(180deg,
|
|
||||||
rgba(255, 255, 255, 0.95) 0%,
|
|
||||||
rgba(248, 246, 241, 0.98) 100%) !important;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
border-right: 1px solid rgba(77, 128, 136, 0.12);
|
|
||||||
box-shadow:
|
|
||||||
4px 0 24px rgba(77, 128, 136, 0.08),
|
|
||||||
1px 0 0 rgba(255, 255, 255, 0.8) inset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 侧边栏菜单整体样式 */
|
.ant-tooltip .ant-tooltip-content,
|
||||||
.modern-sider .ant-menu {
|
|
||||||
background: transparent !important;
|
|
||||||
border-right: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 菜单项基础样式 */
|
|
||||||
.modern-sider .ant-menu-item {
|
|
||||||
margin: 6px 12px !important;
|
|
||||||
padding: 0 16px !important;
|
|
||||||
border-radius: 12px !important;
|
|
||||||
height: 48px !important;
|
|
||||||
line-height: 48px !important;
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 折叠状态下的菜单项 */
|
|
||||||
.modern-sider.ant-layout-sider-collapsed .ant-menu-item {
|
|
||||||
margin: 6px 8px !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 菜单项悬停效果 - 仅未选中项 */
|
|
||||||
.modern-sider .ant-menu-item:not(.ant-menu-item-selected):hover {
|
|
||||||
background: linear-gradient(135deg,
|
|
||||||
rgba(77, 128, 136, 0.15) 0%,
|
|
||||||
rgba(95, 158, 168, 0.22) 100%) !important;
|
|
||||||
transform: translateX(6px);
|
|
||||||
box-shadow:
|
|
||||||
0 2px 8px rgba(77, 128, 136, 0.15),
|
|
||||||
inset 0 0 0 1px rgba(77, 128, 136, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 折叠状态悬停效果 - 仅未选中项 */
|
|
||||||
.modern-sider.ant-layout-sider-collapsed .ant-menu-item:not(.ant-menu-item-selected):hover {
|
|
||||||
transform: translateX(0) scale(1.08);
|
|
||||||
box-shadow:
|
|
||||||
0 4px 12px rgba(77, 128, 136, 0.2),
|
|
||||||
inset 0 0 0 1px rgba(77, 128, 136, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 菜单项选中状态 - 增强版 */
|
|
||||||
.modern-sider .ant-menu-item-selected {
|
|
||||||
background: linear-gradient(135deg,
|
|
||||||
var(--color-primary) 0%,
|
|
||||||
#5A9BA5 50%,
|
|
||||||
var(--color-primary-hover) 100%) !important;
|
|
||||||
box-shadow:
|
|
||||||
0 6px 20px rgba(77, 128, 136, 0.45),
|
|
||||||
0 3px 8px rgba(77, 128, 136, 0.25),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 选中项悬停 - 微发光效果 */
|
|
||||||
.modern-sider .ant-menu-item-selected:hover {
|
|
||||||
transform: translateX(0) !important;
|
|
||||||
box-shadow:
|
|
||||||
0 8px 24px rgba(77, 128, 136, 0.5),
|
|
||||||
0 4px 10px rgba(77, 128, 136, 0.3),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
|
||||||
filter: brightness(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modern-sider.ant-layout-sider-collapsed .ant-menu-item-selected:hover {
|
|
||||||
transform: scale(1.05) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 选中状态文字和图标颜色 */
|
|
||||||
.modern-sider .ant-menu-item-selected,
|
|
||||||
.modern-sider .ant-menu-item-selected a,
|
|
||||||
.modern-sider .ant-menu-item-selected .anticon {
|
|
||||||
color: #fff !important;
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 未选中状态颜色 */
|
|
||||||
.modern-sider .ant-menu-item:not(.ant-menu-item-selected),
|
|
||||||
.modern-sider .ant-menu-item:not(.ant-menu-item-selected) a {
|
|
||||||
color: var(--color-text-secondary) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modern-sider .ant-menu-item:not(.ant-menu-item-selected) .anticon {
|
|
||||||
color: var(--color-primary) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 菜单项悬停时文字和图标颜色 */
|
|
||||||
.modern-sider .ant-menu-item:not(.ant-menu-item-selected):hover,
|
|
||||||
.modern-sider .ant-menu-item:not(.ant-menu-item-selected):hover a {
|
|
||||||
color: var(--color-primary-active) !important;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modern-sider .ant-menu-item:not(.ant-menu-item-selected):hover .anticon {
|
|
||||||
color: var(--color-primary-active) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 图标样式优化 */
|
|
||||||
.modern-sider .ant-menu-item .anticon {
|
|
||||||
font-size: 18px !important;
|
|
||||||
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modern-sider.ant-layout-sider-collapsed .ant-menu-item .anticon {
|
|
||||||
font-size: 22px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 悬停时图标微动效 */
|
|
||||||
.modern-sider .ant-menu-item:not(.ant-menu-item-selected):hover .anticon {
|
|
||||||
transform: scale(1.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 折叠状态下图标去边距并隐藏文字容器,确保居中 */
|
|
||||||
.modern-sider.ant-layout-sider-collapsed .ant-menu-item .anticon {
|
|
||||||
margin-right: 0 !important;
|
|
||||||
font-size: 22px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 折叠状态下隐藏文字但保持点击区域 */
|
|
||||||
.modern-sider.ant-layout-sider-collapsed .ant-menu-item .ant-menu-title-content {
|
|
||||||
width: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保折叠状态下的 Link 覆盖整个菜单项区域 */
|
|
||||||
.modern-sider.ant-layout-sider-collapsed .ant-menu-item {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modern-sider.ant-layout-sider-collapsed .ant-menu-item a {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 选中项左侧指示条 */
|
|
||||||
.modern-sider .ant-menu-item-selected::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
width: 4px;
|
|
||||||
height: 24px;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
border-radius: 0 4px 4px 0;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 折叠状态隐藏指示条 */
|
|
||||||
.modern-sider.ant-layout-sider-collapsed .ant-menu-item-selected::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 链接文字样式 */
|
|
||||||
.modern-sider .ant-menu-item a {
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 侧边栏顶部装饰线 */
|
|
||||||
.modern-sider::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 3px;
|
|
||||||
background: linear-gradient(90deg,
|
|
||||||
var(--color-primary) 0%,
|
|
||||||
var(--color-primary-hover) 50%,
|
|
||||||
var(--color-primary) 100%);
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 侧边栏底部渐变遮罩 */
|
|
||||||
.modern-sider::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 60px;
|
|
||||||
background: linear-gradient(to top,
|
|
||||||
rgba(248, 246, 241, 0.95) 0%,
|
|
||||||
transparent 100%);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 菜单项波纹效果 */
|
|
||||||
.modern-sider .ant-menu-item::after {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 菜单标题样式 (如果有分组) */
|
|
||||||
.modern-sider .ant-menu-item-group-title {
|
|
||||||
color: var(--color-text-tertiary) !important;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
padding: 16px 24px 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tooltip 样式优化 (折叠状态) */
|
|
||||||
.ant-tooltip .ant-tooltip-inner {
|
.ant-tooltip .ant-tooltip-inner {
|
||||||
background: linear-gradient(135deg,
|
max-width: inherit;
|
||||||
var(--color-primary) 0%,
|
}
|
||||||
var(--color-primary-hover) 100%);
|
|
||||||
|
.ant-tooltip .ant-tooltip-inner {
|
||||||
|
background: var(--app-tooltip-bg, #884d5c);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
box-shadow: 0 4px 12px rgba(77, 128, 136, 0.3);
|
box-shadow: 0 4px 12px var(--app-tooltip-shadow, rgba(136, 77, 92, 0.3));
|
||||||
}
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.ant-tooltip {
|
||||||
|
max-width: calc(100vw - 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tooltip .ant-tooltip-inner {
|
||||||
|
padding: 8px 12px;
|
||||||
|
max-height: 50vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Spin, Result, Button, Modal, Input, message } from 'antd';
|
import { Spin, Result, Button, Modal, Input, message, theme } from 'antd';
|
||||||
import { authApi } from '../services/api';
|
import { authApi } from '../services/api';
|
||||||
import AnnouncementModal from '../components/AnnouncementModal';
|
import AnnouncementModal from '../components/AnnouncementModal';
|
||||||
|
|
||||||
@@ -10,6 +10,8 @@ export default function AuthCallback() {
|
|||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
const [showAnnouncement, setShowAnnouncement] = useState(false);
|
const [showAnnouncement, setShowAnnouncement] = useState(false);
|
||||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||||
interface PasswordStatus {
|
interface PasswordStatus {
|
||||||
has_password: boolean;
|
has_password: boolean;
|
||||||
has_custom_password: boolean;
|
has_custom_password: boolean;
|
||||||
@@ -92,11 +94,11 @@ export default function AuthCallback() {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
background: 'linear-gradient(135deg, #4D8088 0%, #5F9EA8 100%)',
|
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
|
||||||
}}>
|
}}>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<Spin size="large" />
|
<Spin size="large" />
|
||||||
<div style={{ marginTop: 20, color: 'white', fontSize: 16 }}>
|
<div style={{ marginTop: 20, color: token.colorWhite, fontSize: 16 }}>
|
||||||
正在处理登录...
|
正在处理登录...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,7 +113,7 @@ export default function AuthCallback() {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
background: 'linear-gradient(135deg, #4D8088 0%, #5F9EA8 100%)',
|
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
|
||||||
}}>
|
}}>
|
||||||
<Result
|
<Result
|
||||||
status="error"
|
status="error"
|
||||||
@@ -122,7 +124,7 @@ export default function AuthCallback() {
|
|||||||
返回登录
|
返回登录
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
style={{ background: 'white', padding: 40, borderRadius: 8 }}
|
style={{ background: token.colorBgContainer, padding: 40, borderRadius: 8 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -257,17 +259,17 @@ export default function AuthCallback() {
|
|||||||
<p>系统已为您自动生成默认密码,您可以选择设置自定义密码或继续使用默认密码。</p>
|
<p>系统已为您自动生成默认密码,您可以选择设置自定义密码或继续使用默认密码。</p>
|
||||||
{passwordStatus?.default_password && (
|
{passwordStatus?.default_password && (
|
||||||
<div style={{
|
<div style={{
|
||||||
background: '#f0f2f5',
|
background: token.colorFillTertiary,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
marginTop: 12
|
marginTop: 12
|
||||||
}}>
|
}}>
|
||||||
<strong>账号:</strong>{passwordStatus.username}<br />
|
<strong>账号:</strong>{passwordStatus.username}<br />
|
||||||
<strong>默认密码:</strong><code style={{
|
<strong>默认密码:</strong><code style={{
|
||||||
background: '#fff',
|
background: token.colorBgContainer,
|
||||||
padding: '2px 8px',
|
padding: '2px 8px',
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
color: '#1890ff',
|
color: token.colorPrimary,
|
||||||
fontSize: 14
|
fontSize: 14
|
||||||
}}>{passwordStatus.default_password}</code>
|
}}>{passwordStatus.default_password}</code>
|
||||||
</div>
|
</div>
|
||||||
@@ -301,13 +303,13 @@ export default function AuthCallback() {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
background: 'linear-gradient(135deg, #4D8088 0%, #5F9EA8 100%)',
|
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
|
||||||
}}>
|
}}>
|
||||||
<Result
|
<Result
|
||||||
status="success"
|
status="success"
|
||||||
title="登录成功"
|
title="登录成功"
|
||||||
subTitle={showPasswordModal ? "请设置账号密码..." : (showAnnouncement ? "欢迎使用..." : "正在跳转...")}
|
subTitle={showPasswordModal ? "请设置账号密码..." : (showAnnouncement ? "欢迎使用..." : "正在跳转...")}
|
||||||
style={{ background: 'white', padding: 40, borderRadius: 8 }}
|
style={{ background: alphaColor(token.colorBgContainer, 0.96), padding: 40, borderRadius: 8 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
InputNumber,
|
InputNumber,
|
||||||
List,
|
List,
|
||||||
message,
|
message,
|
||||||
|
Popconfirm,
|
||||||
Progress,
|
Progress,
|
||||||
Row,
|
Row,
|
||||||
Select,
|
Select,
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Typography,
|
Typography,
|
||||||
Upload,
|
Upload,
|
||||||
|
theme,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { UploadFile } from 'antd/es/upload/interface';
|
import type { UploadFile } from 'antd/es/upload/interface';
|
||||||
import { InboxOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined, WarningOutlined, RedoOutlined } from '@ant-design/icons';
|
import { InboxOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined, WarningOutlined, RedoOutlined } from '@ant-design/icons';
|
||||||
@@ -31,7 +33,7 @@ import type {
|
|||||||
BookImportTask,
|
BookImportTask,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text, Title } = Typography;
|
||||||
const { Dragger } = Upload;
|
const { Dragger } = Upload;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
@@ -106,6 +108,8 @@ function isNotFoundError(error: unknown): boolean {
|
|||||||
|
|
||||||
export default function BookImport() {
|
export default function BookImport() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const isMobile = window.innerWidth <= 768;
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
|
||||||
const [taskId, setTaskId] = useState<string | null>(null);
|
const [taskId, setTaskId] = useState<string | null>(null);
|
||||||
@@ -140,6 +144,40 @@ export default function BookImport() {
|
|||||||
return 1;
|
return 1;
|
||||||
}, [taskId, taskStatus, preview, applying, isApplyComplete]);
|
}, [taskId, taskStatus, preview, applying, isApplyComplete]);
|
||||||
|
|
||||||
|
const canRestart = useMemo(() => {
|
||||||
|
return Boolean(
|
||||||
|
file ||
|
||||||
|
taskId ||
|
||||||
|
taskStatus ||
|
||||||
|
preview ||
|
||||||
|
applyProgress > 0 ||
|
||||||
|
applyMessage ||
|
||||||
|
applyError ||
|
||||||
|
isApplyComplete ||
|
||||||
|
failedSteps.length > 0 ||
|
||||||
|
retrying
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
file,
|
||||||
|
taskId,
|
||||||
|
taskStatus,
|
||||||
|
preview,
|
||||||
|
applyProgress,
|
||||||
|
applyMessage,
|
||||||
|
applyError,
|
||||||
|
isApplyComplete,
|
||||||
|
failedSteps,
|
||||||
|
retrying,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const stepItems = [
|
||||||
|
{ title: '上传文件' },
|
||||||
|
{ title: '解析中' },
|
||||||
|
{ title: '预览修改' },
|
||||||
|
{ title: '生成导入' },
|
||||||
|
];
|
||||||
|
const currentStepText = stepItems[currentStep]?.title || '上传文件';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cache = loadBookImportCache();
|
const cache = loadBookImportCache();
|
||||||
if (cache) {
|
if (cache) {
|
||||||
@@ -487,6 +525,31 @@ export default function BookImport() {
|
|||||||
}
|
}
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
|
const restartImport = useCallback(() => {
|
||||||
|
clearBookImportCache();
|
||||||
|
importedProjectId.current = null;
|
||||||
|
|
||||||
|
setFile(null);
|
||||||
|
setTaskId(null);
|
||||||
|
setTaskStatus(null);
|
||||||
|
setPreview(null);
|
||||||
|
|
||||||
|
setCreatingTask(false);
|
||||||
|
setLoadingPreview(false);
|
||||||
|
setApplying(false);
|
||||||
|
setApplyProgress(0);
|
||||||
|
setApplyMessage('');
|
||||||
|
setApplyError(null);
|
||||||
|
setIsApplyComplete(false);
|
||||||
|
|
||||||
|
setFailedSteps([]);
|
||||||
|
setRetrying(false);
|
||||||
|
setRetryProgress(0);
|
||||||
|
setRetryMessage('');
|
||||||
|
|
||||||
|
message.success('已重新开始,请重新上传 TXT 并解析');
|
||||||
|
}, []);
|
||||||
|
|
||||||
const updateChapter = (index: number, patch: Partial<BookImportPreview['chapters'][number]>) => {
|
const updateChapter = (index: number, patch: Partial<BookImportPreview['chapters'][number]>) => {
|
||||||
setPreview(prev => {
|
setPreview(prev => {
|
||||||
if (!prev) return prev;
|
if (!prev) return prev;
|
||||||
@@ -497,18 +560,100 @@ export default function BookImport() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '100%', overflow: 'auto', paddingRight: 8 }}>
|
<div
|
||||||
<Card style={{ marginBottom: 16 }}>
|
style={{
|
||||||
<Steps
|
minHeight: '90vh',
|
||||||
current={currentStep}
|
overflow: 'auto',
|
||||||
items={[
|
background: `linear-gradient(180deg, ${token.colorBgLayout} 0%, ${token.colorFillSecondary} 100%)`,
|
||||||
{ title: '上传文件' },
|
padding: isMobile ? '20px 16px 70px' : '24px 24px 70px',
|
||||||
{ title: '解析中' },
|
}}
|
||||||
{ title: '预览修改' },
|
>
|
||||||
{ title: '生成导入' },
|
<div style={{ maxWidth: 1400, margin: '0 auto', width: '100%' }}>
|
||||||
]}
|
<Card
|
||||||
/>
|
variant="borderless"
|
||||||
</Card>
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`,
|
||||||
|
borderRadius: isMobile ? 16 : 20,
|
||||||
|
boxShadow: token.boxShadowSecondary,
|
||||||
|
marginBottom: isMobile ? 14 : 16,
|
||||||
|
border: 'none',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ position: 'absolute', top: -48, right: -48, width: 160, height: 160, borderRadius: '50%', background: token.colorWhite, opacity: 0.08, pointerEvents: 'none' }} />
|
||||||
|
<div style={{ position: 'absolute', bottom: -40, left: '26%', width: 110, height: 110, borderRadius: '50%', background: token.colorWhite, opacity: 0.05, pointerEvents: 'none' }} />
|
||||||
|
|
||||||
|
<Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
|
||||||
|
<Col xs={24} sm={12}>
|
||||||
|
<Space direction="vertical" size={4}>
|
||||||
|
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: token.colorWhite, textShadow: `0 2px 4px ${token.colorBgMask}` }}>
|
||||||
|
<InboxOutlined style={{ color: token.colorWhite, opacity: 0.9, marginRight: 8 }} />
|
||||||
|
拆书导入
|
||||||
|
</Title>
|
||||||
|
<Text style={{ fontSize: isMobile ? 12 : 14, color: token.colorTextLightSolid, opacity: 0.85, marginLeft: isMobile ? 40 : 48 }}>
|
||||||
|
上传TXT并自动解析为章节、预览并导入项目
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12}>
|
||||||
|
<Space
|
||||||
|
size={12}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: isMobile ? 'flex-start' : 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tag
|
||||||
|
style={{
|
||||||
|
marginInlineEnd: 0,
|
||||||
|
background: token.colorWhite,
|
||||||
|
border: `1px solid ${token.colorWhite}`,
|
||||||
|
color: token.colorPrimary,
|
||||||
|
fontWeight: 600,
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingInline: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
当前进度:{currentStepText}
|
||||||
|
</Tag>
|
||||||
|
<Popconfirm
|
||||||
|
title="确认重新开始?"
|
||||||
|
description="将清空当前拆书任务与缓存,并回到上传文件步骤。"
|
||||||
|
onConfirm={restartImport}
|
||||||
|
okText="重新开始"
|
||||||
|
cancelText="取消"
|
||||||
|
disabled={!canRestart}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
type="primary"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
disabled={!canRestart}
|
||||||
|
style={{ boxShadow: '0 6px 16px rgba(0, 0, 0, 0.2)', borderRadius: 10 }}
|
||||||
|
>
|
||||||
|
重新开始
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
variant="borderless"
|
||||||
|
style={{
|
||||||
|
marginTop: isMobile ? 14 : 18,
|
||||||
|
borderRadius: 12,
|
||||||
|
background: token.colorBgContainer,
|
||||||
|
border: `1px solid ${token.colorBorderSecondary}`,
|
||||||
|
boxShadow: token.boxShadow,
|
||||||
|
}}
|
||||||
|
styles={{ body: { padding: isMobile ? '10px 12px' : '12px 16px' } }}
|
||||||
|
>
|
||||||
|
<Steps current={currentStep} size={isMobile ? 'small' : 'default'} items={stepItems} />
|
||||||
|
</Card>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{currentStep === 0 && (
|
{currentStep === 0 && (
|
||||||
<Card title="上传 TXT 并开始解析" style={{ marginBottom: 16 }}>
|
<Card title="上传 TXT 并开始解析" style={{ marginBottom: 16 }}>
|
||||||
@@ -913,6 +1058,8 @@ export default function BookImport() {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Card, List, Button, Space, Empty, Tag, Spin, Alert, Switch, Drawer, message } from 'antd';
|
import { Card, List, Button, Space, Empty, Tag, Spin, Alert, Switch, Drawer, message, theme } from 'antd';
|
||||||
import {
|
import {
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
EyeInvisibleOutlined,
|
EyeInvisibleOutlined,
|
||||||
@@ -77,6 +77,7 @@ const ChapterAnalysis: React.FC = () => {
|
|||||||
const [scrollToContentAnnotation, setScrollToContentAnnotation] = useState<string | undefined>();
|
const [scrollToContentAnnotation, setScrollToContentAnnotation] = useState<string | undefined>();
|
||||||
const [scrollToSidebarAnnotation, setScrollToSidebarAnnotation] = useState<string | undefined>();
|
const [scrollToSidebarAnnotation, setScrollToSidebarAnnotation] = useState<string | undefined>();
|
||||||
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
// 监听窗口大小变化
|
// 监听窗口大小变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -196,7 +197,7 @@ const ChapterAnalysis: React.FC = () => {
|
|||||||
<div style={{
|
<div style={{
|
||||||
padding: '16px 0',
|
padding: '16px 0',
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
borderBottom: '1px solid #f0f0f0'
|
borderBottom: `1px solid ${token.colorBorderSecondary}`
|
||||||
}}>
|
}}>
|
||||||
<h2 style={{ margin: 0, fontSize: 24 }}>
|
<h2 style={{ margin: 0, fontSize: 24 }}>
|
||||||
<FundOutlined style={{ marginRight: 8 }} />
|
<FundOutlined style={{ marginRight: 8 }} />
|
||||||
@@ -231,8 +232,8 @@ const ChapterAnalysis: React.FC = () => {
|
|||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
background: selectedChapter?.id === chapter.id ? '#e6f7ff' : 'transparent',
|
background: selectedChapter?.id === chapter.id ? token.colorPrimaryBg : 'transparent',
|
||||||
borderLeft: selectedChapter?.id === chapter.id ? '3px solid #1890ff' : '3px solid transparent',
|
borderLeft: selectedChapter?.id === chapter.id ? `3px solid ${token.colorPrimary}` : '3px solid transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
@@ -278,8 +279,8 @@ const ChapterAnalysis: React.FC = () => {
|
|||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
background: selectedChapter?.id === chapter.id ? '#e6f7ff' : 'transparent',
|
background: selectedChapter?.id === chapter.id ? token.colorPrimaryBg : 'transparent',
|
||||||
borderLeft: selectedChapter?.id === chapter.id ? '3px solid #1890ff' : '3px solid transparent',
|
borderLeft: selectedChapter?.id === chapter.id ? `3px solid ${token.colorPrimary}` : '3px solid transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
@@ -430,7 +431,7 @@ const ChapterAnalysis: React.FC = () => {
|
|||||||
checkedChildren={<EyeOutlined />}
|
checkedChildren={<EyeOutlined />}
|
||||||
unCheckedChildren={<EyeInvisibleOutlined />}
|
unCheckedChildren={<EyeInvisibleOutlined />}
|
||||||
/>
|
/>
|
||||||
<span style={{ fontSize: 13, color: '#666' }}>显示标注</span>
|
<span style={{ fontSize: 13, color: token.colorTextSecondary }}>显示标注</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -441,7 +442,7 @@ const ChapterAnalysis: React.FC = () => {
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginTop: 12,
|
marginTop: 12,
|
||||||
fontSize: isMobile ? 11 : 12,
|
fontSize: isMobile ? 11 : 12,
|
||||||
color: '#999',
|
color: token.colorTextTertiary,
|
||||||
lineHeight: 1.5
|
lineHeight: 1.5
|
||||||
}}>
|
}}>
|
||||||
共有 {annotationsData.summary.total_annotations} 个标注:
|
共有 {annotationsData.summary.total_annotations} 个标注:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Card, Spin, Alert, Button, Space, Switch, Drawer, message, Progress } from 'antd';
|
import { Card, Spin, Alert, Button, Space, Switch, Drawer, message, Progress, theme } from 'antd';
|
||||||
import {
|
import {
|
||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined,
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
@@ -64,6 +64,8 @@ const ChapterReader: React.FC = () => {
|
|||||||
const { chapterId } = useParams<{ chapterId: string }>();
|
const { chapterId } = useParams<{ chapterId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [chapter, setChapter] = useState<ChapterData | null>(null);
|
const [chapter, setChapter] = useState<ChapterData | null>(null);
|
||||||
@@ -303,7 +305,7 @@ const ChapterReader: React.FC = () => {
|
|||||||
checkedChildren={<EyeOutlined />}
|
checkedChildren={<EyeOutlined />}
|
||||||
unCheckedChildren={<EyeInvisibleOutlined />}
|
unCheckedChildren={<EyeInvisibleOutlined />}
|
||||||
/>
|
/>
|
||||||
<span style={{ fontSize: 13, color: '#666' }}>显示标注</span>
|
<span style={{ fontSize: 13, color: token.colorTextSecondary }}>显示标注</span>
|
||||||
<Button
|
<Button
|
||||||
icon={<MenuOutlined />}
|
icon={<MenuOutlined />}
|
||||||
onClick={() => setSidebarVisible(true)}
|
onClick={() => setSidebarVisible(true)}
|
||||||
@@ -319,14 +321,14 @@ const ChapterReader: React.FC = () => {
|
|||||||
{analyzing && (
|
{analyzing && (
|
||||||
<div style={{ marginTop: 12 }}>
|
<div style={{ marginTop: 12 }}>
|
||||||
<Progress percent={analysisProgress} size="small" status="active" />
|
<Progress percent={analysisProgress} size="small" status="active" />
|
||||||
<span style={{ fontSize: 12, color: '#666', marginLeft: 8 }}>
|
<span style={{ fontSize: 12, color: token.colorTextSecondary, marginLeft: 8 }}>
|
||||||
正在分析章节...
|
正在分析章节...
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!analyzing && hasAnnotations && annotationsData && (
|
{!analyzing && hasAnnotations && annotationsData && (
|
||||||
<div style={{ marginTop: 12, fontSize: 12, color: '#999' }}>
|
<div style={{ marginTop: 12, fontSize: 12, color: token.colorTextTertiary }}>
|
||||||
共有 {annotationsData.summary.total_annotations} 个标注:
|
共有 {annotationsData.summary.total_annotations} 个标注:
|
||||||
{annotationsData.summary.hooks > 0 && ` 🎣${annotationsData.summary.hooks}个钩子`}
|
{annotationsData.summary.hooks > 0 && ` 🎣${annotationsData.summary.hooks}个钩子`}
|
||||||
{annotationsData.summary.foreshadows > 0 &&
|
{annotationsData.summary.foreshadows > 0 &&
|
||||||
@@ -383,7 +385,7 @@ const ChapterReader: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 底部翻页按钮 */}
|
{/* 底部翻页按钮 */}
|
||||||
<div style={{ marginTop: 48, paddingTop: 24, borderTop: '1px solid #f0f0f0' }}>
|
<div style={{ marginTop: 48, paddingTop: 24, borderTop: `1px solid ${token.colorBorderSecondary}` }}>
|
||||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||||
<Button
|
<Button
|
||||||
size="large"
|
size="large"
|
||||||
@@ -418,9 +420,9 @@ const ChapterReader: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 400,
|
width: 400,
|
||||||
borderLeft: '1px solid #f0f0f0',
|
borderLeft: `1px solid ${token.colorBorderSecondary}`,
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
background: '#fafafa',
|
background: token.colorBgLayout,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MemorySidebar
|
<MemorySidebar
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||||
import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, InputNumber, Alert, Radio, Descriptions, Collapse, Popconfirm, Pagination } from 'antd';
|
import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, InputNumber, Alert, Radio, Descriptions, Collapse, Popconfirm, Pagination, theme } from 'antd';
|
||||||
import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, RocketOutlined, StopOutlined, InfoCircleOutlined, CaretRightOutlined, DeleteOutlined, BookOutlined, FormOutlined, PlusOutlined, ReadOutlined } from '@ant-design/icons';
|
import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, RocketOutlined, StopOutlined, InfoCircleOutlined, CaretRightOutlined, DeleteOutlined, BookOutlined, FormOutlined, PlusOutlined, ReadOutlined } from '@ant-design/icons';
|
||||||
import { useStore } from '../store';
|
import { useStore } from '../store';
|
||||||
import { useChapterSync } from '../store/hooks';
|
import { useChapterSync } from '../store/hooks';
|
||||||
@@ -48,6 +48,7 @@ const setCachedWordCount = (value: number): void => {
|
|||||||
export default function Chapters() {
|
export default function Chapters() {
|
||||||
const { currentProject, chapters, outlines, setCurrentChapter, setCurrentProject } = useStore();
|
const { currentProject, chapters, outlines, setCurrentChapter, setCurrentProject } = useStore();
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
|
const { token } = theme.useToken();
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
||||||
const [isContinuing, setIsContinuing] = useState(false);
|
const [isContinuing, setIsContinuing] = useState(false);
|
||||||
@@ -900,11 +901,11 @@ export default function Chapters() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
background: 'var(--color-info-bg)',
|
background: token.colorInfoBg,
|
||||||
borderRadius: 4,
|
borderRadius: token.borderRadius,
|
||||||
border: '1px solid var(--color-info-border)'
|
border: `1px solid ${token.colorInfoBorder}`
|
||||||
}}>
|
}}>
|
||||||
<div style={{ marginBottom: 8, fontWeight: 500, color: 'var(--color-primary)' }}>
|
<div style={{ marginBottom: 8, fontWeight: 500, color: token.colorPrimary }}>
|
||||||
📚 将引用的前置章节(共{previousChapters.length}章):
|
📚 将引用的前置章节(共{previousChapters.length}章):
|
||||||
</div>
|
</div>
|
||||||
<div style={{ maxHeight: 150, overflowY: 'auto' }}>
|
<div style={{ maxHeight: 150, overflowY: 'auto' }}>
|
||||||
@@ -914,13 +915,13 @@ export default function Chapters() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 8, fontSize: 12, color: '#666' }}>
|
<div style={{ marginTop: 8, fontSize: 12, color: token.colorTextSecondary }}>
|
||||||
💡 AI会参考这些章节内容,确保情节连贯、角色状态一致
|
💡 AI会参考这些章节内容,确保情节连贯、角色状态一致
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p style={{ color: '#ff4d4f', marginTop: 16, marginBottom: 0 }}>
|
<p style={{ color: token.colorError, marginTop: 16, marginBottom: 0 }}>
|
||||||
⚠️ 注意:此操作将覆盖当前章节内容
|
⚠️ 注意:此操作将覆盖当前章节内容
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1406,7 +1407,7 @@ export default function Chapters() {
|
|||||||
// 显示冲突提示Modal
|
// 显示冲突提示Modal
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
title: '章节序号冲突',
|
title: '章节序号冲突',
|
||||||
icon: <InfoCircleOutlined style={{ color: '#ff4d4f' }} />,
|
icon: <InfoCircleOutlined style={{ color: token.colorError }} />,
|
||||||
width: 500,
|
width: 500,
|
||||||
centered: true,
|
centered: true,
|
||||||
content: (
|
content: (
|
||||||
@@ -1416,9 +1417,9 @@ export default function Chapters() {
|
|||||||
</p>
|
</p>
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: 12,
|
padding: 12,
|
||||||
background: '#fff7e6',
|
background: token.colorWarningBg,
|
||||||
borderRadius: 4,
|
borderRadius: token.borderRadius,
|
||||||
border: '1px solid #ffd591',
|
border: `1px solid ${token.colorWarningBorder}`,
|
||||||
marginBottom: 12
|
marginBottom: 12
|
||||||
}}>
|
}}>
|
||||||
<div><strong>标题:</strong>{conflictChapter.title}</div>
|
<div><strong>标题:</strong>{conflictChapter.title}</div>
|
||||||
@@ -1428,10 +1429,10 @@ export default function Chapters() {
|
|||||||
<div><strong>所属大纲:</strong>{conflictChapter.outline_title}</div>
|
<div><strong>所属大纲:</strong>{conflictChapter.outline_title}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p style={{ color: '#ff4d4f', marginBottom: 8 }}>
|
<p style={{ color: token.colorError, marginBottom: 8 }}>
|
||||||
⚠️ 是否删除旧章节并创建新章节?
|
⚠️ 是否删除旧章节并创建新章节?
|
||||||
</p>
|
</p>
|
||||||
<p style={{ fontSize: 12, color: '#666', marginBottom: 0 }}>
|
<p style={{ fontSize: 12, color: token.colorTextSecondary, marginBottom: 0 }}>
|
||||||
删除后将无法恢复,章节内容和分析结果都将被删除。
|
删除后将无法恢复,章节内容和分析结果都将被删除。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1551,7 +1552,7 @@ export default function Chapters() {
|
|||||||
modal.info({
|
modal.info({
|
||||||
title: (
|
title: (
|
||||||
<Space style={{ flexWrap: 'wrap' }}>
|
<Space style={{ flexWrap: 'wrap' }}>
|
||||||
<InfoCircleOutlined style={{ color: 'var(--color-primary)' }} />
|
<InfoCircleOutlined style={{ color: token.colorPrimary }} />
|
||||||
<span style={{ wordBreak: 'break-word' }}>第{chapter.chapter_number}章展开规划</span>
|
<span style={{ wordBreak: 'break-word' }}>第{chapter.chapter_number}章展开规划</span>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
@@ -1684,7 +1685,7 @@ export default function Chapters() {
|
|||||||
key={idx}
|
key={idx}
|
||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#fafafa',
|
backgroundColor: token.colorFillQuaternary,
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}}
|
}}
|
||||||
@@ -1875,10 +1876,10 @@ export default function Chapters() {
|
|||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
backgroundColor: 'var(--color-bg-container)',
|
backgroundColor: token.colorBgContainer,
|
||||||
padding: isMobile ? '12px 0' : '16px 0',
|
padding: isMobile ? '12px 0' : '16px 0',
|
||||||
marginBottom: isMobile ? 12 : 16,
|
marginBottom: isMobile ? 12 : 16,
|
||||||
borderBottom: '1px solid #f0f0f0',
|
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: isMobile ? 'column' : 'row',
|
flexDirection: isMobile ? 'column' : 'row',
|
||||||
gap: isMobile ? 12 : 0,
|
gap: isMobile ? 12 : 0,
|
||||||
@@ -1925,7 +1926,7 @@ export default function Chapters() {
|
|||||||
disabled={chapters.length === 0 || batchAnalyzableChapterCount === 0}
|
disabled={chapters.length === 0 || batchAnalyzableChapterCount === 0}
|
||||||
block={isMobile}
|
block={isMobile}
|
||||||
size={isMobile ? 'middle' : 'middle'}
|
size={isMobile ? 'middle' : 'middle'}
|
||||||
style={{ background: '#fa8c16', borderColor: '#fa8c16' }}
|
style={{ background: token.colorWarning, borderColor: token.colorWarning }}
|
||||||
title={batchAnalyzableChapterCount === 0 ? '暂无可一键分析章节' : `可一键分析 ${batchAnalyzableChapterCount} 章`}
|
title={batchAnalyzableChapterCount === 0 ? '暂无可一键分析章节' : `可一键分析 ${batchAnalyzableChapterCount} 章`}
|
||||||
>
|
>
|
||||||
一键分析{batchAnalyzableChapterCount > 0 ? ` (${batchAnalyzableChapterCount})` : ''}
|
一键分析{batchAnalyzableChapterCount > 0 ? ` (${batchAnalyzableChapterCount})` : ''}
|
||||||
@@ -1937,7 +1938,7 @@ export default function Chapters() {
|
|||||||
disabled={chapters.length === 0}
|
disabled={chapters.length === 0}
|
||||||
block={isMobile}
|
block={isMobile}
|
||||||
size={isMobile ? 'middle' : 'middle'}
|
size={isMobile ? 'middle' : 'middle'}
|
||||||
style={{ background: '#722ed1', borderColor: '#722ed1' }}
|
style={{ background: token.colorInfo, borderColor: token.colorInfo }}
|
||||||
>
|
>
|
||||||
批量生成
|
批量生成
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1969,9 +1970,9 @@ export default function Chapters() {
|
|||||||
style={{
|
style={{
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
background: '#fff',
|
background: token.colorBgContainer,
|
||||||
borderRadius: 8,
|
borderRadius: token.borderRadius,
|
||||||
border: '1px solid #f0f0f0',
|
border: `1px solid ${token.colorBorderSecondary}`,
|
||||||
flexDirection: isMobile ? 'column' : 'row',
|
flexDirection: isMobile ? 'column' : 'row',
|
||||||
alignItems: isMobile ? 'flex-start' : 'center',
|
alignItems: isMobile ? 'flex-start' : 'center',
|
||||||
}}
|
}}
|
||||||
@@ -2025,7 +2026,7 @@ export default function Chapters() {
|
|||||||
>
|
>
|
||||||
<div style={{ width: '100%' }}>
|
<div style={{ width: '100%' }}>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: 'var(--color-primary)' }} />}
|
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: token.colorPrimary }} />}
|
||||||
title={
|
title={
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -2039,7 +2040,7 @@ export default function Chapters() {
|
|||||||
</span>
|
</span>
|
||||||
<Space wrap size={isMobile ? 4 : 8}>
|
<Space wrap size={isMobile ? 4 : 8}>
|
||||||
<Tag color={getStatusColor(item.status)}>{getStatusText(item.status)}</Tag>
|
<Tag color={getStatusColor(item.status)}>{getStatusText(item.status)}</Tag>
|
||||||
<Badge count={`${item.word_count || 0}字`} style={{ backgroundColor: 'var(--color-success)' }} />
|
<Badge count={`${item.word_count || 0}字`} style={{ backgroundColor: token.colorSuccess }} />
|
||||||
{renderAnalysisStatus(item.id)}
|
{renderAnalysisStatus(item.id)}
|
||||||
{!canGenerateChapter(item) && (
|
{!canGenerateChapter(item) && (
|
||||||
<Tag icon={<LockOutlined />} color="warning" title={getGenerateDisabledReason(item)}>
|
<Tag icon={<LockOutlined />} color="warning" title={getGenerateDisabledReason(item)}>
|
||||||
@@ -2051,12 +2052,12 @@ export default function Chapters() {
|
|||||||
}
|
}
|
||||||
description={
|
description={
|
||||||
item.content ? (
|
item.content ? (
|
||||||
<div style={{ marginTop: 8, color: 'rgba(0,0,0,0.65)', lineHeight: 1.6, fontSize: isMobile ? 12 : 14 }}>
|
<div style={{ marginTop: 8, color: token.colorTextSecondary, lineHeight: 1.6, fontSize: isMobile ? 12 : 14 }}>
|
||||||
{item.content.substring(0, isMobile ? 80 : 150)}
|
{item.content.substring(0, isMobile ? 80 : 150)}
|
||||||
{item.content.length > (isMobile ? 80 : 150) && '...'}
|
{item.content.length > (isMobile ? 80 : 150) && '...'}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span style={{ color: 'rgba(0,0,0,0.45)', fontSize: isMobile ? 12 : 14 }}>暂无内容</span>
|
<span style={{ color: token.colorTextTertiary, fontSize: isMobile ? 12 : 14 }}>暂无内容</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -2134,19 +2135,19 @@ export default function Chapters() {
|
|||||||
</span>
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
count={`${group.chapters.length} 章`}
|
count={`${group.chapters.length} 章`}
|
||||||
style={{ backgroundColor: 'var(--color-success)' }}
|
style={{ backgroundColor: token.colorSuccess }}
|
||||||
/>
|
/>
|
||||||
<Badge
|
<Badge
|
||||||
count={`${group.chapters.reduce((sum, ch) => sum + (ch.word_count || 0), 0)} 字`}
|
count={`${group.chapters.reduce((sum, ch) => sum + (ch.word_count || 0), 0)} 字`}
|
||||||
style={{ backgroundColor: 'var(--color-primary)' }}
|
style={{ backgroundColor: token.colorPrimary }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
background: '#fff',
|
background: token.colorBgContainer,
|
||||||
borderRadius: 8,
|
borderRadius: token.borderRadius,
|
||||||
border: '1px solid #f0f0f0',
|
border: `1px solid ${token.colorBorderSecondary}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<List
|
<List
|
||||||
@@ -2230,7 +2231,7 @@ export default function Chapters() {
|
|||||||
>
|
>
|
||||||
<div style={{ width: '100%' }}>
|
<div style={{ width: '100%' }}>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: 'var(--color-primary)' }} />}
|
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: token.colorPrimary }} />}
|
||||||
title={
|
title={
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -2244,7 +2245,7 @@ export default function Chapters() {
|
|||||||
</span>
|
</span>
|
||||||
<Space wrap size={isMobile ? 4 : 8}>
|
<Space wrap size={isMobile ? 4 : 8}>
|
||||||
<Tag color={getStatusColor(item.status)}>{getStatusText(item.status)}</Tag>
|
<Tag color={getStatusColor(item.status)}>{getStatusText(item.status)}</Tag>
|
||||||
<Badge count={`${item.word_count || 0}字`} style={{ backgroundColor: 'var(--color-success)' }} />
|
<Badge count={`${item.word_count || 0}字`} style={{ backgroundColor: token.colorSuccess }} />
|
||||||
{renderAnalysisStatus(item.id)}
|
{renderAnalysisStatus(item.id)}
|
||||||
{!canGenerateChapter(item) && (
|
{!canGenerateChapter(item) && (
|
||||||
<Tag icon={<LockOutlined />} color="warning" title={getGenerateDisabledReason(item)}>
|
<Tag icon={<LockOutlined />} color="warning" title={getGenerateDisabledReason(item)}>
|
||||||
@@ -2255,7 +2256,7 @@ export default function Chapters() {
|
|||||||
{item.expansion_plan && (
|
{item.expansion_plan && (
|
||||||
<InfoCircleOutlined
|
<InfoCircleOutlined
|
||||||
title="查看展开详情"
|
title="查看展开详情"
|
||||||
style={{ color: 'var(--color-primary)', cursor: 'pointer', fontSize: 16 }}
|
style={{ color: token.colorPrimary, cursor: 'pointer', fontSize: 16 }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
showExpansionPlanModal(item);
|
showExpansionPlanModal(item);
|
||||||
@@ -2264,7 +2265,7 @@ export default function Chapters() {
|
|||||||
)}
|
)}
|
||||||
<FormOutlined
|
<FormOutlined
|
||||||
title={item.expansion_plan ? "编辑规划信息" : "创建规划信息"}
|
title={item.expansion_plan ? "编辑规划信息" : "创建规划信息"}
|
||||||
style={{ color: 'var(--color-success)', cursor: 'pointer', fontSize: 16 }}
|
style={{ color: token.colorSuccess, cursor: 'pointer', fontSize: 16 }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleOpenPlanEditor(item);
|
handleOpenPlanEditor(item);
|
||||||
@@ -2276,12 +2277,12 @@ export default function Chapters() {
|
|||||||
}
|
}
|
||||||
description={
|
description={
|
||||||
item.content ? (
|
item.content ? (
|
||||||
<div style={{ marginTop: 8, color: 'rgba(0,0,0,0.65)', lineHeight: 1.6, fontSize: isMobile ? 12 : 14 }}>
|
<div style={{ marginTop: 8, color: token.colorTextSecondary, lineHeight: 1.6, fontSize: isMobile ? 12 : 14 }}>
|
||||||
{item.content.substring(0, isMobile ? 80 : 150)}
|
{item.content.substring(0, isMobile ? 80 : 150)}
|
||||||
{item.content.length > (isMobile ? 80 : 150) && '...'}
|
{item.content.length > (isMobile ? 80 : 150) && '...'}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span style={{ color: 'rgba(0,0,0,0.45)', fontSize: isMobile ? 12 : 14 }}>暂无内容</span>
|
<span style={{ color: token.colorTextTertiary, fontSize: isMobile ? 12 : 14 }}>暂无内容</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -2539,7 +2540,7 @@ export default function Chapters() {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
{!selectedStyleId && (
|
{!selectedStyleId && (
|
||||||
<div style={{ color: '#ff4d4f', fontSize: 12, marginTop: 4 }}>请选择写作风格</div>
|
<div style={{ color: token.colorError, fontSize: 12, marginTop: 4 }}>请选择写作风格</div>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -2560,7 +2561,7 @@ export default function Chapters() {
|
|||||||
<Select.Option value="全知视角">全知视角</Select.Option>
|
<Select.Option value="全知视角">全知视角</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
{temporaryNarrativePerspective && (
|
{temporaryNarrativePerspective && (
|
||||||
<div style={{ color: 'var(--color-success)', fontSize: 12, marginTop: 4 }}>
|
<div style={{ color: token.colorSuccess, fontSize: 12, marginTop: 4 }}>
|
||||||
✓ {getNarrativePerspectiveText(temporaryNarrativePerspective)}
|
✓ {getNarrativePerspectiveText(temporaryNarrativePerspective)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -2703,7 +2704,7 @@ export default function Chapters() {
|
|||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<RocketOutlined style={{ color: '#722ed1' }} />
|
<RocketOutlined style={{ color: token.colorInfo }} />
|
||||||
<span>批量生成章节内容</span>
|
<span>批量生成章节内容</span>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -2879,7 +2880,7 @@ export default function Chapters() {
|
|||||||
>
|
>
|
||||||
<Radio.Group disabled>
|
<Radio.Group disabled>
|
||||||
<Radio value={true}>
|
<Radio value={true}>
|
||||||
<span style={{ fontSize: 12, color: '#52c41a' }}>✓ 自动更新角色状态</span>
|
<span style={{ fontSize: 12, color: token.colorSuccess }}>✓ 自动更新角色状态</span>
|
||||||
</Radio>
|
</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Button, Modal, Form, Input, Select, message, Row, Col, Empty, Tabs, Divider, Typography, Space, InputNumber, Checkbox } from 'antd';
|
import { Button, Modal, Form, Input, Select, message, Row, Col, Empty, Tabs, Divider, Typography, Space, InputNumber, Checkbox, theme } from 'antd';
|
||||||
import { ThunderboltOutlined, UserOutlined, TeamOutlined, PlusOutlined, ExportOutlined, ImportOutlined, DownloadOutlined } from '@ant-design/icons';
|
import { ThunderboltOutlined, UserOutlined, TeamOutlined, PlusOutlined, ExportOutlined, ImportOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||||
import { useStore } from '../store';
|
import { useStore } from '../store';
|
||||||
import { useCharacterSync } from '../store/hooks';
|
import { useCharacterSync } from '../store/hooks';
|
||||||
import { characterGridConfig } from '../components/CardStyles';
|
import { charactersPageGridConfig } from '../components/CardStyles';
|
||||||
import { CharacterCard } from '../components/CharacterCard';
|
import { CharacterCard } from '../components/CharacterCard';
|
||||||
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
|
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
|
||||||
import type { Character, ApiError } from '../types';
|
import type { Character, ApiError } from '../types';
|
||||||
@@ -94,6 +94,7 @@ interface CharacterUpdateData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Characters() {
|
export default function Characters() {
|
||||||
|
const { token } = theme.useToken();
|
||||||
const { currentProject, characters } = useStore();
|
const { currentProject, characters } = useStore();
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
@@ -390,7 +391,7 @@ export default function Characters() {
|
|||||||
content: (
|
content: (
|
||||||
<div>
|
<div>
|
||||||
{validation.errors.map((error, index) => (
|
{validation.errors.map((error, index) => (
|
||||||
<div key={index} style={{ color: 'red' }}>• {error}</div>
|
<div key={index} style={{ color: token.colorError }}>• {error}</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -415,10 +416,10 @@ export default function Characters() {
|
|||||||
{validation.warnings.length > 0 && (
|
{validation.warnings.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Divider style={{ margin: '12px 0' }} />
|
<Divider style={{ margin: '12px 0' }} />
|
||||||
<p style={{ color: '#faad14' }}><strong>⚠️ 警告:</strong></p>
|
<p style={{ color: token.colorWarning }}><strong>⚠️ 警告:</strong></p>
|
||||||
<ul style={{ marginLeft: 20 }}>
|
<ul style={{ marginLeft: 20 }}>
|
||||||
{validation.warnings.map((warning, index) => (
|
{validation.warnings.map((warning, index) => (
|
||||||
<li key={index} style={{ color: '#faad14' }}>{warning}</li>
|
<li key={index} style={{ color: token.colorWarning }}>{warning}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
@@ -463,10 +464,10 @@ export default function Characters() {
|
|||||||
{result.statistics.skipped > 0 && (
|
{result.statistics.skipped > 0 && (
|
||||||
<>
|
<>
|
||||||
<Divider style={{ margin: '12px 0' }} />
|
<Divider style={{ margin: '12px 0' }} />
|
||||||
<p style={{ color: '#faad14' }}>⚠️ 跳过: {result.statistics.skipped} 个</p>
|
<p style={{ color: token.colorWarning }}>⚠️ 跳过: {result.statistics.skipped} 个</p>
|
||||||
<ul style={{ marginLeft: 20 }}>
|
<ul style={{ marginLeft: 20 }}>
|
||||||
{result.details.skipped.map((name, index) => (
|
{result.details.skipped.map((name, index) => (
|
||||||
<li key={index} style={{ color: '#faad14' }}>{name}</li>
|
<li key={index} style={{ color: token.colorWarning }}>{name}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
@@ -474,10 +475,10 @@ export default function Characters() {
|
|||||||
{result.warnings.length > 0 && (
|
{result.warnings.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Divider style={{ margin: '12px 0' }} />
|
<Divider style={{ margin: '12px 0' }} />
|
||||||
<p style={{ color: '#faad14' }}>⚠️ 警告:</p>
|
<p style={{ color: token.colorWarning }}>⚠️ 警告:</p>
|
||||||
<ul style={{ marginLeft: 20 }}>
|
<ul style={{ marginLeft: 20 }}>
|
||||||
{result.warnings.map((warning, index) => (
|
{result.warnings.map((warning, index) => (
|
||||||
<li key={index} style={{ color: '#faad14' }}>{warning}</li>
|
<li key={index} style={{ color: token.colorWarning }}>{warning}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
@@ -485,10 +486,10 @@ export default function Characters() {
|
|||||||
{result.details.errors.length > 0 && (
|
{result.details.errors.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Divider style={{ margin: '12px 0' }} />
|
<Divider style={{ margin: '12px 0' }} />
|
||||||
<p style={{ color: 'red' }}>❌ 失败: {result.statistics.errors} 个</p>
|
<p style={{ color: token.colorError }}>❌ 失败: {result.statistics.errors} 个</p>
|
||||||
<ul style={{ marginLeft: 20 }}>
|
<ul style={{ marginLeft: 20 }}>
|
||||||
{result.details.errors.map((error, index) => (
|
{result.details.errors.map((error, index) => (
|
||||||
<li key={index} style={{ color: 'red' }}>{error}</li>
|
<li key={index} style={{ color: token.colorError }}>{error}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
@@ -777,7 +778,7 @@ export default function Characters() {
|
|||||||
<Empty description="还没有角色或组织,开始创建吧!" />
|
<Empty description="还没有角色或组织,开始创建吧!" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Row gutter={isMobile ? [8, 8] : characterGridConfig.gutter}>
|
<Row gutter={isMobile ? [8, 8] : charactersPageGridConfig.gutter}>
|
||||||
{activeTab === 'all' && (
|
{activeTab === 'all' && (
|
||||||
<>
|
<>
|
||||||
{characterList.length > 0 && (
|
{characterList.length > 0 && (
|
||||||
@@ -793,10 +794,10 @@ export default function Characters() {
|
|||||||
{characterList.map((character) => (
|
{characterList.map((character) => (
|
||||||
<Col
|
<Col
|
||||||
xs={24}
|
xs={24}
|
||||||
sm={characterGridConfig.sm}
|
sm={charactersPageGridConfig.sm}
|
||||||
md={characterGridConfig.md}
|
md={charactersPageGridConfig.md}
|
||||||
lg={characterGridConfig.lg}
|
lg={charactersPageGridConfig.lg}
|
||||||
xl={characterGridConfig.xl}
|
xl={charactersPageGridConfig.xl}
|
||||||
key={character.id}
|
key={character.id}
|
||||||
style={{ padding: isMobile ? '4px' : '8px' }}
|
style={{ padding: isMobile ? '4px' : '8px' }}
|
||||||
>
|
>
|
||||||
@@ -831,10 +832,10 @@ export default function Characters() {
|
|||||||
{organizationList.map((org) => (
|
{organizationList.map((org) => (
|
||||||
<Col
|
<Col
|
||||||
xs={24}
|
xs={24}
|
||||||
sm={characterGridConfig.sm}
|
sm={charactersPageGridConfig.sm}
|
||||||
md={characterGridConfig.md}
|
md={charactersPageGridConfig.md}
|
||||||
lg={characterGridConfig.lg}
|
lg={charactersPageGridConfig.lg}
|
||||||
xl={characterGridConfig.xl}
|
xl={charactersPageGridConfig.xl}
|
||||||
key={org.id}
|
key={org.id}
|
||||||
style={{ padding: isMobile ? '4px' : '8px' }}
|
style={{ padding: isMobile ? '4px' : '8px' }}
|
||||||
>
|
>
|
||||||
@@ -861,10 +862,10 @@ export default function Characters() {
|
|||||||
{activeTab === 'character' && characterList.map((character) => (
|
{activeTab === 'character' && characterList.map((character) => (
|
||||||
<Col
|
<Col
|
||||||
xs={24}
|
xs={24}
|
||||||
sm={characterGridConfig.sm}
|
sm={charactersPageGridConfig.sm}
|
||||||
md={characterGridConfig.md}
|
md={charactersPageGridConfig.md}
|
||||||
lg={characterGridConfig.lg}
|
lg={charactersPageGridConfig.lg}
|
||||||
xl={characterGridConfig.xl}
|
xl={charactersPageGridConfig.xl}
|
||||||
key={character.id}
|
key={character.id}
|
||||||
style={{ padding: isMobile ? '4px' : '8px' }}
|
style={{ padding: isMobile ? '4px' : '8px' }}
|
||||||
>
|
>
|
||||||
@@ -887,10 +888,10 @@ export default function Characters() {
|
|||||||
{activeTab === 'organization' && organizationList.map((org) => (
|
{activeTab === 'organization' && organizationList.map((org) => (
|
||||||
<Col
|
<Col
|
||||||
xs={24}
|
xs={24}
|
||||||
sm={characterGridConfig.sm}
|
sm={charactersPageGridConfig.sm}
|
||||||
md={characterGridConfig.md}
|
md={charactersPageGridConfig.md}
|
||||||
lg={characterGridConfig.lg}
|
lg={charactersPageGridConfig.lg}
|
||||||
xl={characterGridConfig.xl}
|
xl={charactersPageGridConfig.xl}
|
||||||
key={org.id}
|
key={org.id}
|
||||||
style={{ padding: isMobile ? '4px' : '8px' }}
|
style={{ padding: isMobile ? '4px' : '8px' }}
|
||||||
>
|
>
|
||||||
@@ -1020,7 +1021,7 @@ export default function Characters() {
|
|||||||
value={editingCharacter.relationships}
|
value={editingCharacter.relationships}
|
||||||
readOnly
|
readOnly
|
||||||
autoSize={{ minRows: 1, maxRows: 3 }}
|
autoSize={{ minRows: 1, maxRows: 3 }}
|
||||||
style={{ backgroundColor: '#f5f5f5', cursor: 'default' }}
|
style={{ backgroundColor: token.colorFillTertiary, cursor: 'default' }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
@@ -1195,10 +1196,10 @@ export default function Characters() {
|
|||||||
disabled
|
disabled
|
||||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||||
placeholder="暂无成员,请在组织管理中添加"
|
placeholder="暂无成员,请在组织管理中添加"
|
||||||
style={{ color: '#333', backgroundColor: '#fafafa' }}
|
style={{ color: token.colorText, backgroundColor: token.colorFillAlter }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<div style={{ marginBottom: 12, fontSize: 12, color: '#8c8c8c' }}>
|
<div style={{ marginBottom: 12, fontSize: 12, color: token.colorTextTertiary }}>
|
||||||
💡 请前往「组织管理」页面添加或管理组织成员
|
💡 请前往「组织管理」页面添加或管理组织成员
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom';
|
|||||||
import {
|
import {
|
||||||
Card, Table, Button, Tag, Space, Modal, Form, Input, Select,
|
Card, Table, Button, Tag, Space, Modal, Form, Input, Select,
|
||||||
InputNumber, Switch, message, Tooltip, Popconfirm, Statistic,
|
InputNumber, Switch, message, Tooltip, Popconfirm, Statistic,
|
||||||
Row, Col, Empty, Divider, Badge, Alert, Pagination, Dropdown
|
Row, Col, Empty, Divider, Badge, Alert, Pagination, Dropdown, theme
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { MenuProps } from 'antd';
|
import type { MenuProps } from 'antd';
|
||||||
import {
|
import {
|
||||||
@@ -73,6 +73,7 @@ export default function Foreshadows() {
|
|||||||
// 表格容器引用,用于计算滚动高度
|
// 表格容器引用,用于计算滚动高度
|
||||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const [tableScrollY, setTableScrollY] = useState<number>(400);
|
const [tableScrollY, setTableScrollY] = useState<number>(400);
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
// 加载伏笔列表
|
// 加载伏笔列表
|
||||||
const loadForeshadows = useCallback(async () => {
|
const loadForeshadows = useCallback(async () => {
|
||||||
@@ -508,22 +509,22 @@ export default function Foreshadows() {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col span={3}>
|
<Col span={3}>
|
||||||
<Card size="small">
|
<Card size="small">
|
||||||
<Statistic title="待埋入" value={stats.pending} valueStyle={{ color: '#8c8c8c' }} />
|
<Statistic title="待埋入" value={stats.pending} valueStyle={{ color: token.colorTextSecondary }} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={3}>
|
<Col span={3}>
|
||||||
<Card size="small">
|
<Card size="small">
|
||||||
<Statistic title="已埋入" value={stats.planted} valueStyle={{ color: '#52c41a' }} />
|
<Statistic title="已埋入" value={stats.planted} valueStyle={{ color: token.colorSuccess }} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={3}>
|
<Col span={3}>
|
||||||
<Card size="small">
|
<Card size="small">
|
||||||
<Statistic title="已回收" value={stats.resolved} valueStyle={{ color: '#1890ff' }} />
|
<Statistic title="已回收" value={stats.resolved} valueStyle={{ color: token.colorPrimary }} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={3}>
|
<Col span={3}>
|
||||||
<Card size="small">
|
<Card size="small">
|
||||||
<Statistic title="长线伏笔" value={stats.long_term_count} valueStyle={{ color: '#722ed1' }} />
|
<Statistic title="长线伏笔" value={stats.long_term_count} valueStyle={{ color: token.colorInfo }} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={3}>
|
<Col span={3}>
|
||||||
@@ -531,7 +532,7 @@ export default function Foreshadows() {
|
|||||||
<Statistic
|
<Statistic
|
||||||
title="超期未回收"
|
title="超期未回收"
|
||||||
value={stats.overdue_count}
|
value={stats.overdue_count}
|
||||||
valueStyle={{ color: stats.overdue_count > 0 ? '#ff4d4f' : '#8c8c8c' }}
|
valueStyle={{ color: stats.overdue_count > 0 ? token.colorError : token.colorTextSecondary }}
|
||||||
prefix={stats.overdue_count > 0 ? <WarningOutlined /> : null}
|
prefix={stats.overdue_count > 0 ? <WarningOutlined /> : null}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -659,12 +660,12 @@ export default function Foreshadows() {
|
|||||||
{/* 分页器 - 固定在底部居中 */}
|
{/* 分页器 - 固定在底部居中 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '12px 0',
|
padding: '12px 0',
|
||||||
borderTop: '1px solid #f0f0f0',
|
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
background: '#fff',
|
background: token.colorBgContainer,
|
||||||
}}>
|
}}>
|
||||||
<Pagination
|
<Pagination
|
||||||
current={currentPage}
|
current={currentPage}
|
||||||
@@ -868,7 +869,7 @@ export default function Foreshadows() {
|
|||||||
{currentForeshadow.hint_text && (
|
{currentForeshadow.hint_text && (
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<strong>暗示文本:</strong>
|
<strong>暗示文本:</strong>
|
||||||
<p style={{ marginTop: 8, whiteSpace: 'pre-wrap', color: '#666' }}>
|
<p style={{ marginTop: 8, whiteSpace: 'pre-wrap', color: token.colorTextSecondary }}>
|
||||||
{currentForeshadow.hint_text}
|
{currentForeshadow.hint_text}
|
||||||
</p>
|
</p>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -877,7 +878,7 @@ export default function Foreshadows() {
|
|||||||
{currentForeshadow.resolution_text && (
|
{currentForeshadow.resolution_text && (
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<strong>揭示文本:</strong>
|
<strong>揭示文本:</strong>
|
||||||
<p style={{ marginTop: 8, whiteSpace: 'pre-wrap', color: '#666' }}>
|
<p style={{ marginTop: 8, whiteSpace: 'pre-wrap', color: token.colorTextSecondary }}>
|
||||||
{currentForeshadow.resolution_text}
|
{currentForeshadow.resolution_text}
|
||||||
</p>
|
</p>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -920,7 +921,7 @@ export default function Foreshadows() {
|
|||||||
{currentForeshadow.notes && (
|
{currentForeshadow.notes && (
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<strong>备注:</strong>
|
<strong>备注:</strong>
|
||||||
<p style={{ marginTop: 8, color: '#666' }}>{currentForeshadow.notes}</p>
|
<p style={{ marginTop: 8, color: token.colorTextSecondary }}>{currentForeshadow.notes}</p>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Card, Input, Button, Space, Typography, message, Spin, Modal } from 'antd';
|
import { Card, Input, Button, Space, Typography, message, Spin, Modal, theme } from 'antd';
|
||||||
import { SendOutlined, ArrowLeftOutlined, ReloadOutlined } from '@ant-design/icons';
|
import { SendOutlined, ArrowLeftOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||||
import { inspirationApi } from '../services/api';
|
import { inspirationApi } from '../services/api';
|
||||||
import { AIProjectGenerator, type GenerationConfig } from '../components/AIProjectGenerator';
|
import { AIProjectGenerator, type GenerationConfig } from '../components/AIProjectGenerator';
|
||||||
@@ -52,6 +52,7 @@ const Inspiration: React.FC = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [currentStep, setCurrentStep] = useState<Step>('idea');
|
const [currentStep, setCurrentStep] = useState<Step>('idea');
|
||||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
@@ -852,7 +853,7 @@ const Inspiration: React.FC = () => {
|
|||||||
height: isMobile ? 'calc(100vh - 280px)' : 600,
|
height: isMobile ? 'calc(100vh - 280px)' : 600,
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
|
boxShadow: `0 8px 24px color-mix(in srgb, ${token.colorTextBase} 20%, transparent)`,
|
||||||
scrollBehavior: 'smooth'
|
scrollBehavior: 'smooth'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -873,16 +874,16 @@ const Inspiration: React.FC = () => {
|
|||||||
maxWidth: '80%',
|
maxWidth: '80%',
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: msg.type === 'ai' ? 'var(--color-bg-container)' : 'var(--color-primary)',
|
background: msg.type === 'ai' ? token.colorBgContainer : token.colorPrimary,
|
||||||
color: msg.type === 'ai' ? 'var(--color-text-primary)' : '#fff',
|
color: msg.type === 'ai' ? token.colorText : token.colorWhite,
|
||||||
boxShadow: msg.type === 'ai'
|
boxShadow: msg.type === 'ai'
|
||||||
? 'var(--shadow-card)'
|
? `0 2px 10px color-mix(in srgb, ${token.colorTextBase} 12%, transparent)`
|
||||||
: 'var(--shadow-primary)',
|
: `0 4px 14px color-mix(in srgb, ${token.colorPrimary} 30%, transparent)`,
|
||||||
}}>
|
}}>
|
||||||
<Paragraph
|
<Paragraph
|
||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
color: msg.type === 'ai' ? 'var(--color-text-primary)' : '#fff',
|
color: msg.type === 'ai' ? token.colorText : token.colorWhite,
|
||||||
whiteSpace: 'pre-wrap'
|
whiteSpace: 'pre-wrap'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -904,13 +905,13 @@ const Inspiration: React.FC = () => {
|
|||||||
style={{
|
style={{
|
||||||
cursor: msg.optionsDisabled ? 'not-allowed' : 'pointer',
|
cursor: msg.optionsDisabled ? 'not-allowed' : 'pointer',
|
||||||
border: msg.isMultiSelect && selectedOptions.includes(option)
|
border: msg.isMultiSelect && selectedOptions.includes(option)
|
||||||
? '2px solid var(--color-primary)'
|
? `2px solid ${token.colorPrimary}`
|
||||||
: '1px solid var(--color-border)',
|
: `1px solid ${token.colorBorder}`,
|
||||||
background: msg.optionsDisabled
|
background: msg.optionsDisabled
|
||||||
? 'var(--color-bg-layout)'
|
? token.colorBgLayout
|
||||||
: msg.isMultiSelect && selectedOptions.includes(option)
|
: msg.isMultiSelect && selectedOptions.includes(option)
|
||||||
? 'var(--color-bg-spotlight)'
|
? token.colorPrimaryBg
|
||||||
: 'var(--color-bg-container)',
|
: token.colorBgContainer,
|
||||||
opacity: msg.optionsDisabled ? 0.6 : 1,
|
opacity: msg.optionsDisabled ? 0.6 : 1,
|
||||||
animation: 'floatIn 0.6s ease-out',
|
animation: 'floatIn 0.6s ease-out',
|
||||||
animationDelay: `${optIndex * 0.1}s`,
|
animationDelay: `${optIndex * 0.1}s`,
|
||||||
@@ -920,7 +921,7 @@ const Inspiration: React.FC = () => {
|
|||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (!msg.optionsDisabled) {
|
if (!msg.optionsDisabled) {
|
||||||
e.currentTarget.style.transform = 'translateY(-2px) scale(1.02)';
|
e.currentTarget.style.transform = 'translateY(-2px) scale(1.02)';
|
||||||
e.currentTarget.style.boxShadow = 'var(--shadow-elevated)';
|
e.currentTarget.style.boxShadow = `0 8px 22px color-mix(in srgb, ${token.colorTextBase} 14%, transparent)`;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
@@ -947,7 +948,7 @@ const Inspiration: React.FC = () => {
|
|||||||
|
|
||||||
{/* 反馈优化区域 - 新增 */}
|
{/* 反馈优化区域 - 新增 */}
|
||||||
{msg.canRefine && !msg.optionsDisabled && !msg.isMultiSelect && (
|
{msg.canRefine && !msg.optionsDisabled && !msg.isMultiSelect && (
|
||||||
<div style={{ marginTop: 8, paddingTop: 8, borderTop: '1px dashed var(--color-border)' }}>
|
<div style={{ marginTop: 8, paddingTop: 8, borderTop: `1px dashed ${token.colorBorder}` }}>
|
||||||
{showFeedbackInput === index ? (
|
{showFeedbackInput === index ? (
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||||
<TextArea
|
<TextArea
|
||||||
@@ -1018,7 +1019,7 @@ const Inspiration: React.FC = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
style={{ boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}
|
style={{ boxShadow: `0 4px 12px color-mix(in srgb, ${token.colorTextBase} 14%, transparent)` }}
|
||||||
styles={{ body: { padding: 12 } }}
|
styles={{ body: { padding: 12 } }}
|
||||||
>
|
>
|
||||||
<Space.Compact style={{ width: '100%' }}>
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
@@ -1059,7 +1060,7 @@ const Inspiration: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
minHeight: '100dvh',
|
minHeight: '100dvh',
|
||||||
background: 'var(--color-bg-base)',
|
background: token.colorBgBase,
|
||||||
}}>
|
}}>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<style>
|
<style>
|
||||||
@@ -1105,8 +1106,8 @@ const Inspiration: React.FC = () => {
|
|||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
background: 'var(--color-primary)',
|
background: token.colorPrimary,
|
||||||
boxShadow: 'var(--shadow-header)',
|
boxShadow: `0 6px 20px color-mix(in srgb, ${token.colorPrimary} 30%, transparent)`,
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
maxWidth: 1200,
|
maxWidth: 1200,
|
||||||
@@ -1121,9 +1122,9 @@ const Inspiration: React.FC = () => {
|
|||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
size={isMobile ? 'middle' : 'large'}
|
size={isMobile ? 'middle' : 'large'}
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(255,255,255,0.2)',
|
background: `color-mix(in srgb, ${token.colorWhite} 20%, transparent)`,
|
||||||
borderColor: 'rgba(255,255,255,0.3)',
|
borderColor: `color-mix(in srgb, ${token.colorWhite} 30%, transparent)`,
|
||||||
color: '#fff',
|
color: token.colorWhite,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isMobile ? '返回' : '返回首页'}
|
{isMobile ? '返回' : '返回首页'}
|
||||||
@@ -1134,8 +1135,8 @@ const Inspiration: React.FC = () => {
|
|||||||
level={isMobile ? 4 : 2}
|
level={isMobile ? 4 : 2}
|
||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
color: '#fff',
|
color: token.colorWhite,
|
||||||
textShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
textShadow: '0 2px 4px color-mix(in srgb, var(--ant-color-black) 18%, transparent)',
|
||||||
lineHeight: 1.2
|
lineHeight: 1.2
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1162,9 +1163,9 @@ const Inspiration: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
size={isMobile ? 'middle' : 'large'}
|
size={isMobile ? 'middle' : 'large'}
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(255,255,255,0.2)',
|
background: `color-mix(in srgb, ${token.colorWhite} 20%, transparent)`,
|
||||||
borderColor: 'rgba(255,255,255,0.3)',
|
borderColor: `color-mix(in srgb, ${token.colorWhite} 30%, transparent)`,
|
||||||
color: '#fff',
|
color: token.colorWhite,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isMobile ? '重新' : '重新开始'}
|
{isMobile ? '重新' : '重新开始'}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
|
theme,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
@@ -39,7 +40,32 @@ const { TextArea } = Input;
|
|||||||
export default function MCPPluginsPage() {
|
export default function MCPPluginsPage() {
|
||||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||||
|
|
||||||
|
const statusStyles = {
|
||||||
|
success: {
|
||||||
|
bg: token.colorSuccessBg,
|
||||||
|
border: token.colorSuccessBorder,
|
||||||
|
text: token.colorSuccessText,
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
bg: token.colorInfoBg,
|
||||||
|
border: token.colorInfoBorder,
|
||||||
|
text: token.colorInfoText,
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
bg: token.colorWarningBg,
|
||||||
|
border: token.colorWarningBorder,
|
||||||
|
text: token.colorWarningText,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
bg: token.colorErrorBg,
|
||||||
|
border: token.colorErrorBorder,
|
||||||
|
text: token.colorErrorText,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// 响应式监听窗口大小变化
|
// 响应式监听窗口大小变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
@@ -261,25 +287,25 @@ export default function MCPPluginsPage() {
|
|||||||
width: isMobile ? '95%' : 700,
|
width: isMobile ? '95%' : 700,
|
||||||
content: (
|
content: (
|
||||||
<div style={{ padding: '8px 0' }}>
|
<div style={{ padding: '8px 0' }}>
|
||||||
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
|
<div style={{ marginBottom: 16, padding: 12, background: statusStyles.success.bg, border: `1px solid ${statusStyles.success.border}`, borderRadius: 8 }}>
|
||||||
<Typography.Text strong style={{ color: 'var(--color-success)', fontSize: 14 }}>
|
<Typography.Text strong style={{ color: statusStyles.success.text, fontSize: 14 }}>
|
||||||
✓ {result.message}
|
✓ {result.message}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 16 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 16 }}>
|
||||||
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
|
<div style={{ padding: 12, background: token.colorBgLayout, borderRadius: 8 }}>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>可用工具数</Text>
|
<Text type="secondary" style={{ fontSize: 12 }}>可用工具数</Text>
|
||||||
<div><Text strong style={{ fontSize: 20 }}>{result.tools_count || 0}</Text></div>
|
<div><Text strong style={{ fontSize: 20 }}>{result.tools_count || 0}</Text></div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
|
<div style={{ padding: 12, background: token.colorBgLayout, borderRadius: 8 }}>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>总响应时间</Text>
|
<Text type="secondary" style={{ fontSize: 12 }}>总响应时间</Text>
|
||||||
<div><Text strong style={{ fontSize: 20 }}>{result.response_time_ms?.toFixed(0) || 0}ms</Text></div>
|
<div><Text strong style={{ fontSize: 20 }}>{result.response_time_ms?.toFixed(0) || 0}ms</Text></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{aiChoice && (
|
{aiChoice && (
|
||||||
<div style={{ marginBottom: 12, padding: 12, background: 'var(--color-info-bg)', borderRadius: 8, border: '1px solid var(--color-info-border)' }}>
|
<div style={{ marginBottom: 12, padding: 12, background: statusStyles.info.bg, borderRadius: 8, border: `1px solid ${statusStyles.info.border}` }}>
|
||||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🤖 AI选择的工具</Text>
|
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🤖 AI选择的工具</Text>
|
||||||
<Text code strong>{aiChoice}</Text>
|
<Text code strong>{aiChoice}</Text>
|
||||||
{callTime && <Tag color="blue" style={{ marginLeft: 8 }}>{callTime}</Tag>}
|
{callTime && <Tag color="blue" style={{ marginLeft: 8 }}>{callTime}</Tag>}
|
||||||
@@ -289,7 +315,7 @@ export default function MCPPluginsPage() {
|
|||||||
{paramsStr && (
|
{paramsStr && (
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📝 调用参数</Text>
|
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📝 调用参数</Text>
|
||||||
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 12, overflow: 'auto', maxHeight: 100 }}>
|
<pre style={{ margin: 0, padding: 8, background: token.colorBgLayout, borderRadius: 4, fontSize: 12, overflow: 'auto', maxHeight: 100 }}>
|
||||||
{(() => { try { return JSON.stringify(JSON.parse(paramsStr), null, 2); } catch { return paramsStr; } })()}
|
{(() => { try { return JSON.stringify(JSON.parse(paramsStr), null, 2); } catch { return paramsStr; } })()}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -298,7 +324,7 @@ export default function MCPPluginsPage() {
|
|||||||
{resultStr && (
|
{resultStr && (
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📊 返回结果预览</Text>
|
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📊 返回结果预览</Text>
|
||||||
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 150, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
<pre style={{ margin: 0, padding: 8, background: token.colorBgLayout, borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 150, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||||
{resultStr}
|
{resultStr}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -326,13 +352,13 @@ export default function MCPPluginsPage() {
|
|||||||
{result.error && (
|
{result.error && (
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: 16,
|
padding: 16,
|
||||||
background: 'var(--color-error-bg)',
|
background: statusStyles.error.bg,
|
||||||
border: '1px solid var(--color-error-border)',
|
border: `1px solid ${statusStyles.error.border}`,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: 16
|
marginBottom: 16
|
||||||
}}>
|
}}>
|
||||||
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>错误信息:</Text>
|
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>错误信息:</Text>
|
||||||
<Text style={{ fontSize: 13, color: 'var(--color-error)', fontFamily: 'monospace', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
<Text style={{ fontSize: 13, color: statusStyles.error.text, fontFamily: 'monospace', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||||
{result.error}
|
{result.error}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -341,8 +367,8 @@ export default function MCPPluginsPage() {
|
|||||||
{result.suggestions && result.suggestions.length > 0 && (
|
{result.suggestions && result.suggestions.length > 0 && (
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: 16,
|
padding: 16,
|
||||||
background: 'var(--color-warning-bg)',
|
background: statusStyles.warning.bg,
|
||||||
border: '1px solid var(--color-warning-border)',
|
border: `1px solid ${statusStyles.warning.border}`,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: 16
|
marginBottom: 16
|
||||||
}}>
|
}}>
|
||||||
@@ -418,24 +444,24 @@ export default function MCPPluginsPage() {
|
|||||||
width: isMobile ? '95%' : 700,
|
width: isMobile ? '95%' : 700,
|
||||||
content: (
|
content: (
|
||||||
<div style={{ padding: '8px 0' }}>
|
<div style={{ padding: '8px 0' }}>
|
||||||
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
|
<div style={{ marginBottom: 16, padding: 12, background: statusStyles.success.bg, border: `1px solid ${statusStyles.success.border}`, borderRadius: 8 }}>
|
||||||
<Typography.Text strong style={{ color: 'var(--color-success)', fontSize: 14 }}>
|
<Typography.Text strong style={{ color: statusStyles.success.text, fontSize: 14 }}>
|
||||||
✓ {result.message}
|
✓ {result.message}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 16 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 16 }}>
|
||||||
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
|
<div style={{ padding: 12, background: token.colorBgLayout, borderRadius: 8 }}>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>API 提供商</Text>
|
<Text type="secondary" style={{ fontSize: 12 }}>API 提供商</Text>
|
||||||
<div><Text strong style={{ fontSize: 16 }}>{result.provider}</Text></div>
|
<div><Text strong style={{ fontSize: 16 }}>{result.provider}</Text></div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
|
<div style={{ padding: 12, background: token.colorBgLayout, borderRadius: 8 }}>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>响应时间</Text>
|
<Text type="secondary" style={{ fontSize: 12 }}>响应时间</Text>
|
||||||
<div><Text strong style={{ fontSize: 16 }}>{result.response_time_ms?.toFixed(0) || 0}ms</Text></div>
|
<div><Text strong style={{ fontSize: 16 }}>{result.response_time_ms?.toFixed(0) || 0}ms</Text></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 12, padding: 12, background: 'var(--color-info-bg)', borderRadius: 8, border: '1px solid var(--color-info-border)' }}>
|
<div style={{ marginBottom: 12, padding: 12, background: statusStyles.info.bg, borderRadius: 8, border: `1px solid ${statusStyles.info.border}` }}>
|
||||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🔧 模型信息</Text>
|
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🔧 模型信息</Text>
|
||||||
<Text code strong>{result.model}</Text>
|
<Text code strong>{result.model}</Text>
|
||||||
{result.details?.finish_reason && (
|
{result.details?.finish_reason && (
|
||||||
@@ -446,7 +472,7 @@ export default function MCPPluginsPage() {
|
|||||||
{result.details && (
|
{result.details && (
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📊 检测详情</Text>
|
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📊 检测详情</Text>
|
||||||
<div style={{ padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 12 }}>
|
<div style={{ padding: 8, background: token.colorBgLayout, borderRadius: 4, fontSize: 12 }}>
|
||||||
<div>✓ 工具调用数量: {result.details.tool_call_count || 0}</div>
|
<div>✓ 工具调用数量: {result.details.tool_call_count || 0}</div>
|
||||||
<div>✓ 测试工具: {result.details.test_tool || 'N/A'}</div>
|
<div>✓ 测试工具: {result.details.test_tool || 'N/A'}</div>
|
||||||
<div>✓ 响应类型: {result.details.response_type || 'N/A'}</div>
|
<div>✓ 响应类型: {result.details.response_type || 'N/A'}</div>
|
||||||
@@ -457,14 +483,14 @@ export default function MCPPluginsPage() {
|
|||||||
{result.tool_calls && result.tool_calls.length > 0 && (
|
{result.tool_calls && result.tool_calls.length > 0 && (
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🔨 工具调用示例</Text>
|
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🔨 工具调用示例</Text>
|
||||||
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 150 }}>
|
<pre style={{ margin: 0, padding: 8, background: token.colorBgLayout, borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 150 }}>
|
||||||
{JSON.stringify(result.tool_calls[0], null, 2)}
|
{JSON.stringify(result.tool_calls[0], null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{result.suggestions && result.suggestions.length > 0 && (
|
{result.suggestions && result.suggestions.length > 0 && (
|
||||||
<div style={{ padding: 12, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
|
<div style={{ padding: 12, background: statusStyles.success.bg, border: `1px solid ${statusStyles.success.border}`, borderRadius: 8 }}>
|
||||||
<Text strong style={{ fontSize: 13, display: 'block', marginBottom: 8 }}>💡 建议</Text>
|
<Text strong style={{ fontSize: 13, display: 'block', marginBottom: 8 }}>💡 建议</Text>
|
||||||
<ul style={{ margin: 0, paddingLeft: 20, fontSize: 12 }}>
|
<ul style={{ margin: 0, paddingLeft: 20, fontSize: 12 }}>
|
||||||
{result.suggestions.map((s: string, i: number) => (
|
{result.suggestions.map((s: string, i: number) => (
|
||||||
@@ -495,8 +521,8 @@ export default function MCPPluginsPage() {
|
|||||||
{result.error && (
|
{result.error && (
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: 16,
|
padding: 16,
|
||||||
background: 'var(--color-warning-bg)',
|
background: statusStyles.warning.bg,
|
||||||
border: '1px solid var(--color-warning-border)',
|
border: `1px solid ${statusStyles.warning.border}`,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: 16
|
marginBottom: 16
|
||||||
}}>
|
}}>
|
||||||
@@ -510,7 +536,7 @@ export default function MCPPluginsPage() {
|
|||||||
{result.response_preview && (
|
{result.response_preview && (
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📝 模型返回内容(前200字符)</Text>
|
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📝 模型返回内容(前200字符)</Text>
|
||||||
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 100, whiteSpace: 'pre-wrap' }}>
|
<pre style={{ margin: 0, padding: 8, background: token.colorBgLayout, borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 100, whiteSpace: 'pre-wrap' }}>
|
||||||
{result.response_preview}
|
{result.response_preview}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -519,8 +545,8 @@ export default function MCPPluginsPage() {
|
|||||||
{result.suggestions && result.suggestions.length > 0 && (
|
{result.suggestions && result.suggestions.length > 0 && (
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: 16,
|
padding: 16,
|
||||||
background: 'var(--color-info-bg)',
|
background: statusStyles.info.bg,
|
||||||
border: '1px solid var(--color-info-border)',
|
border: `1px solid ${statusStyles.info.border}`,
|
||||||
borderRadius: 8
|
borderRadius: 8
|
||||||
}}>
|
}}>
|
||||||
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>💡 建议:</Text>
|
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>💡 建议:</Text>
|
||||||
@@ -599,7 +625,7 @@ export default function MCPPluginsPage() {
|
|||||||
{contextHolder}
|
{contextHolder}
|
||||||
<div style={{
|
<div style={{
|
||||||
minHeight: '90vh',
|
minHeight: '90vh',
|
||||||
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)',
|
background: `linear-gradient(180deg, ${token.colorBgLayout} 0%, ${alphaColor(token.colorPrimary, 0.08)} 100%)`,
|
||||||
padding: isMobile ? '20px 16px 70px' : '24px 24px 70px',
|
padding: isMobile ? '20px 16px 70px' : '24px 24px 70px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -616,9 +642,9 @@ export default function MCPPluginsPage() {
|
|||||||
<Card
|
<Card
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, #5A9BA5 50%, var(--color-primary-hover) 100%)',
|
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${alphaColor(token.colorPrimary, 0.8)} 50%, ${token.colorPrimaryHover} 100%)`,
|
||||||
borderRadius: isMobile ? 16 : 24,
|
borderRadius: isMobile ? 16 : 24,
|
||||||
boxShadow: '0 12px 40px rgba(77, 128, 136, 0.25), 0 4px 12px rgba(0, 0, 0, 0.06)',
|
boxShadow: `0 12px 40px ${alphaColor(token.colorPrimary, 0.25)}, 0 4px 12px ${alphaColor(token.colorText, 0.08)}`,
|
||||||
marginBottom: isMobile ? 20 : 24,
|
marginBottom: isMobile ? 20 : 24,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -626,20 +652,20 @@ export default function MCPPluginsPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 装饰性背景元素 */}
|
{/* 装饰性背景元素 */}
|
||||||
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.08)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: alphaColor(token.colorWhite, 0.08), pointerEvents: 'none' }} />
|
||||||
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.05)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: alphaColor(token.colorWhite, 0.05), pointerEvents: 'none' }} />
|
||||||
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.06)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: alphaColor(token.colorWhite, 0.06), pointerEvents: 'none' }} />
|
||||||
|
|
||||||
<Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
|
<Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
|
||||||
<Col xs={24} sm={12}>
|
<Col xs={24} sm={12}>
|
||||||
<Space direction="vertical" size={4}>
|
<Space direction="vertical" size={4}>
|
||||||
<Space align="center">
|
<Space align="center">
|
||||||
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: '#fff', textShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
|
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: token.colorWhite, textShadow: `0 2px 4px ${alphaColor(token.colorText, 0.2)}` }}>
|
||||||
<ToolOutlined style={{ color: 'rgba(255,255,255,0.9)', marginRight: 8 }} />
|
<ToolOutlined style={{ color: alphaColor(token.colorWhite, 0.9), marginRight: 8 }} />
|
||||||
MCP插件管理
|
MCP插件管理
|
||||||
</Title>
|
</Title>
|
||||||
</Space>
|
</Space>
|
||||||
<Text style={{ fontSize: isMobile ? 12 : 14, color: 'rgba(255,255,255,0.85)', marginLeft: isMobile ? 40 : 48 }}>
|
<Text style={{ fontSize: isMobile ? 12 : 14, color: alphaColor(token.colorWhite, 0.85), marginLeft: isMobile ? 40 : 48 }}>
|
||||||
扩展AI能力,连接外部工具与服务
|
扩展AI能力,连接外部工具与服务
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -652,10 +678,10 @@ export default function MCPPluginsPage() {
|
|||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: 'rgba(255, 193, 7, 0.95)',
|
background: alphaColor(token.colorWarning, 0.95),
|
||||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
border: `1px solid ${alphaColor(token.colorWhite, 0.3)}`,
|
||||||
boxShadow: '0 4px 16px rgba(255, 193, 7, 0.4)',
|
boxShadow: `0 4px 16px ${alphaColor(token.colorWarning, 0.4)}`,
|
||||||
color: '#fff',
|
color: token.colorWhite,
|
||||||
fontWeight: 600
|
fontWeight: 600
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -671,10 +697,10 @@ export default function MCPPluginsPage() {
|
|||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: 'rgba(255, 255, 255, 0.9)',
|
background: alphaColor(token.colorBgContainer, 0.9),
|
||||||
border: '1px solid rgba(255, 255, 255, 0.6)',
|
border: `1px solid ${alphaColor(token.colorBorder, 0.6)}`,
|
||||||
backdropFilter: 'blur(10px)',
|
backdropFilter: 'blur(10px)',
|
||||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.03)'
|
boxShadow: `0 4px 12px ${alphaColor(token.colorText, 0.06)}`
|
||||||
}}
|
}}
|
||||||
styles={{ body: { padding: isMobile ? 14 : 20 } }}
|
styles={{ body: { padding: isMobile ? 14 : 20 } }}
|
||||||
>
|
>
|
||||||
@@ -690,21 +716,21 @@ export default function MCPPluginsPage() {
|
|||||||
width: isMobile ? 36 : 40,
|
width: isMobile ? 36 : 40,
|
||||||
height: isMobile ? 36 : 40,
|
height: isMobile ? 36 : 40,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: modelSupportStatus === 'supported' ? 'var(--color-success-bg)' : modelSupportStatus === 'unsupported' ? 'var(--color-error-bg)' : 'var(--color-info-bg)',
|
background: modelSupportStatus === 'supported' ? statusStyles.success.bg : modelSupportStatus === 'unsupported' ? statusStyles.error.bg : statusStyles.info.bg,
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
border: `1px solid ${modelSupportStatus === 'supported' ? 'var(--color-success-border)' : modelSupportStatus === 'unsupported' ? 'var(--color-error-border)' : 'var(--color-info-border)'}`,
|
border: `1px solid ${modelSupportStatus === 'supported' ? statusStyles.success.border : modelSupportStatus === 'unsupported' ? statusStyles.error.border : statusStyles.info.border}`,
|
||||||
flexShrink: 0
|
flexShrink: 0
|
||||||
}}>
|
}}>
|
||||||
{modelSupportStatus === 'supported' ? (
|
{modelSupportStatus === 'supported' ? (
|
||||||
<CheckCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: 'var(--color-success)' }} />
|
<CheckCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: statusStyles.success.text }} />
|
||||||
) : modelSupportStatus === 'unsupported' ? (
|
) : modelSupportStatus === 'unsupported' ? (
|
||||||
<CloseCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: 'var(--color-error)' }} />
|
<CloseCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: statusStyles.error.text }} />
|
||||||
) : (
|
) : (
|
||||||
<QuestionCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: 'var(--color-info)' }} />
|
<QuestionCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: statusStyles.info.text }} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Text strong style={{ fontSize: isMobile ? 14 : 16, display: 'block', color: 'var(--color-text-primary)' }}>模型能力检查</Text>
|
<Text strong style={{ fontSize: isMobile ? 14 : 16, display: 'block', color: token.colorText }}>模型能力检查</Text>
|
||||||
<Text type="secondary" style={{ fontSize: isMobile ? 12 : 13, display: 'block', lineHeight: 1.5 }}>
|
<Text type="secondary" style={{ fontSize: isMobile ? 12 : 13, display: 'block', lineHeight: 1.5 }}>
|
||||||
{modelSupportStatus === 'supported'
|
{modelSupportStatus === 'supported'
|
||||||
? '当前模型支持 Function Calling,可正常使用 MCP 插件'
|
? '当前模型支持 Function Calling,可正常使用 MCP 插件'
|
||||||
@@ -732,18 +758,18 @@ export default function MCPPluginsPage() {
|
|||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: 'rgba(230, 247, 255, 0.6)',
|
background: alphaColor(token.colorInfoBg, 0.7),
|
||||||
border: '1px solid rgba(145, 213, 255, 0.6)',
|
border: `1px solid ${alphaColor(token.colorInfoBorder, 0.8)}`,
|
||||||
backdropFilter: 'blur(10px)',
|
backdropFilter: 'blur(10px)',
|
||||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.03)'
|
boxShadow: `0 4px 12px ${alphaColor(token.colorText, 0.06)}`
|
||||||
}}
|
}}
|
||||||
styles={{ body: { padding: isMobile ? 14 : 20 } }}
|
styles={{ body: { padding: isMobile ? 14 : 20 } }}
|
||||||
>
|
>
|
||||||
<Space align="start">
|
<Space align="start">
|
||||||
<InfoCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: 'var(--color-primary)', marginTop: 2, flexShrink: 0 }} />
|
<InfoCircleOutlined style={{ fontSize: isMobile ? 18 : 20, color: token.colorPrimary, marginTop: 2, flexShrink: 0 }} />
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Text strong style={{ fontSize: isMobile ? 14 : 16, display: 'block', color: 'var(--color-text-primary)', marginBottom: 4 }}>什么是 MCP 插件?</Text>
|
<Text strong style={{ fontSize: isMobile ? 14 : 16, display: 'block', color: token.colorText, marginBottom: 4 }}>什么是 MCP 插件?</Text>
|
||||||
<Text style={{ fontSize: isMobile ? 12 : 13, display: 'block', color: 'var(--color-text-secondary)', lineHeight: 1.6 }}>
|
<Text style={{ fontSize: isMobile ? 12 : 13, display: 'block', color: token.colorTextSecondary, lineHeight: 1.6 }}>
|
||||||
MCP (Model Context Protocol) 协议允许 AI 调用外部工具获取数据。通过添加插件,AI 可以访问搜索引擎、数据库、API 等服务,大幅增强创作能力。
|
MCP (Model Context Protocol) 协议允许 AI 调用外部工具获取数据。通过添加插件,AI 可以访问搜索引擎、数据库、API 等服务,大幅增强创作能力。
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -794,7 +820,7 @@ export default function MCPPluginsPage() {
|
|||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
border: '1px solid #f0f0f0',
|
border: `1px solid ${token.colorBorderSecondary}`,
|
||||||
}}
|
}}
|
||||||
styles={{ body: { padding: isMobile ? 12 : 16 } }}
|
styles={{ body: { padding: isMobile ? 12 : 16 } }}
|
||||||
>
|
>
|
||||||
@@ -932,7 +958,7 @@ export default function MCPPluginsPage() {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: isMobile ? 8 : 8,
|
gap: isMobile ? 8 : 8,
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
borderTop: isMobile ? '1px solid #f0f0f0' : 'none',
|
borderTop: isMobile ? `1px solid ${token.colorBorderSecondary}` : 'none',
|
||||||
paddingTop: isMobile ? 12 : 0
|
paddingTop: isMobile ? 12 : 0
|
||||||
}}>
|
}}>
|
||||||
{/* 桌面端显示开关 */}
|
{/* 桌面端显示开关 */}
|
||||||
@@ -1055,7 +1081,7 @@ export default function MCPPluginsPage() {
|
|||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<ToolOutlined style={{ color: 'var(--color-primary)' }} />
|
<ToolOutlined style={{ color: token.colorPrimary }} />
|
||||||
<span>可用工具列表</span>
|
<span>可用工具列表</span>
|
||||||
{viewingTools && viewingTools.tools.length > 0 && (
|
{viewingTools && viewingTools.tools.length > 0 && (
|
||||||
<Tag color="blue">{viewingTools.tools.length} 个工具</Tag>
|
<Tag color="blue">{viewingTools.tools.length} 个工具</Tag>
|
||||||
@@ -1094,12 +1120,12 @@ export default function MCPPluginsPage() {
|
|||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
border: '1px solid var(--color-border-secondary)',
|
border: `1px solid ${token.colorBorderSecondary}`,
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.05)'
|
boxShadow: `0 2px 4px ${alphaColor(token.colorText, 0.08)}`
|
||||||
}}
|
}}
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<Text code strong style={{ fontSize: isMobile ? '13px' : '14px', color: 'var(--color-primary)' }}>
|
<Text code strong style={{ fontSize: isMobile ? '13px' : '14px', color: token.colorPrimary }}>
|
||||||
{tool.name}
|
{tool.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Tag color="processing" style={{ fontSize: '11px' }}>
|
<Tag color="processing" style={{ fontSize: '11px' }}>
|
||||||
@@ -1119,9 +1145,9 @@ export default function MCPPluginsPage() {
|
|||||||
margin: 0,
|
margin: 0,
|
||||||
fontSize: isMobile ? '12px' : '13px',
|
fontSize: isMobile ? '12px' : '13px',
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
background: 'var(--color-bg-layout)',
|
background: token.colorBgLayout,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
borderLeft: '3px solid var(--color-info)'
|
borderLeft: `3px solid ${token.colorInfo}`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tool.description}
|
{tool.description}
|
||||||
@@ -1137,12 +1163,12 @@ export default function MCPPluginsPage() {
|
|||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: isMobile ? '8px' : '12px',
|
padding: isMobile ? '8px' : '12px',
|
||||||
background: 'var(--color-bg-layout)',
|
background: token.colorBgLayout,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
fontSize: isMobile ? '11px' : '12px',
|
fontSize: isMobile ? '11px' : '12px',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
maxHeight: '200px',
|
maxHeight: '200px',
|
||||||
border: '1px solid var(--color-border-secondary)',
|
border: `1px solid ${token.colorBorderSecondary}`,
|
||||||
lineHeight: 1.6
|
lineHeight: 1.6
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, InputNumber, Input, Descriptions, Drawer } from 'antd';
|
import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, InputNumber, Input, Descriptions, Drawer, theme } from 'antd';
|
||||||
import { PlusOutlined, UserOutlined, EditOutlined, DeleteOutlined, UnorderedListOutlined, BankOutlined } from '@ant-design/icons';
|
import { PlusOutlined, UserOutlined, EditOutlined, DeleteOutlined, UnorderedListOutlined, BankOutlined } from '@ant-design/icons';
|
||||||
import { useStore } from '../store';
|
import { useStore } from '../store';
|
||||||
import { useCharacterSync } from '../store/hooks';
|
import { useCharacterSync } from '../store/hooks';
|
||||||
@@ -58,6 +58,7 @@ export default function Organizations() {
|
|||||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
const [orgListVisible, setOrgListVisible] = useState(false);
|
const [orgListVisible, setOrgListVisible] = useState(false);
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
@@ -310,7 +311,7 @@ export default function Organizations() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
padding: '16px 0',
|
padding: '16px 0',
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
borderBottom: '1px solid #f0f0f0'
|
borderBottom: `1px solid ${token.colorBorderSecondary}`
|
||||||
}}>
|
}}>
|
||||||
<h2 style={{ margin: 0, fontSize: 24 }}>
|
<h2 style={{ margin: 0, fontSize: 24 }}>
|
||||||
<BankOutlined style={{ marginRight: 8 }} />
|
<BankOutlined style={{ marginRight: 8 }} />
|
||||||
@@ -335,7 +336,7 @@ export default function Organizations() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
>
|
>
|
||||||
{organizations.length === 0 ? (
|
{organizations.length === 0 ? (
|
||||||
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#999' }}>
|
<div style={{ textAlign: 'center', padding: '40px 20px', color: token.colorTextTertiary }}>
|
||||||
暂无组织
|
暂无组织
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -347,15 +348,15 @@ export default function Organizations() {
|
|||||||
hoverable
|
hoverable
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
border: selectedOrg?.id === org.id ? '2px solid #1890ff' : '1px solid #d9d9d9',
|
border: selectedOrg?.id === org.id ? `2px solid ${token.colorPrimary}` : `1px solid ${token.colorBorder}`,
|
||||||
background: selectedOrg?.id === org.id ? '#e6f7ff' : 'transparent'
|
background: selectedOrg?.id === org.id ? token.colorPrimaryBg : 'transparent'
|
||||||
}}
|
}}
|
||||||
onClick={() => handleSelectOrganization(org)}
|
onClick={() => handleSelectOrganization(org)}
|
||||||
>
|
>
|
||||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||||
<strong style={{ fontSize: 14 }}>{org.name}</strong>
|
<strong style={{ fontSize: 14 }}>{org.name}</strong>
|
||||||
<Tag color="blue">{org.type}</Tag>
|
<Tag color="blue">{org.type}</Tag>
|
||||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
<div style={{ fontSize: '12px', color: token.colorTextSecondary }}>
|
||||||
成员: {org.member_count} | 势力: {org.power_level}
|
成员: {org.member_count} | 势力: {org.power_level}
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -377,7 +378,7 @@ export default function Organizations() {
|
|||||||
styles={{ body: { padding: 0 } }}
|
styles={{ body: { padding: 0 } }}
|
||||||
>
|
>
|
||||||
{organizations.length === 0 ? (
|
{organizations.length === 0 ? (
|
||||||
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#999' }}>
|
<div style={{ textAlign: 'center', padding: '40px 20px', color: token.colorTextTertiary }}>
|
||||||
暂无组织
|
暂无组织
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -389,8 +390,8 @@ export default function Organizations() {
|
|||||||
hoverable
|
hoverable
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
border: selectedOrg?.id === org.id ? '2px solid #1890ff' : '1px solid #d9d9d9',
|
border: selectedOrg?.id === org.id ? `2px solid ${token.colorPrimary}` : `1px solid ${token.colorBorder}`,
|
||||||
background: selectedOrg?.id === org.id ? '#e6f7ff' : 'transparent'
|
background: selectedOrg?.id === org.id ? token.colorPrimaryBg : 'transparent'
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSelectOrganization(org);
|
handleSelectOrganization(org);
|
||||||
@@ -400,7 +401,7 @@ export default function Organizations() {
|
|||||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||||
<strong style={{ fontSize: 14 }}>{org.name}</strong>
|
<strong style={{ fontSize: 14 }}>{org.name}</strong>
|
||||||
<Tag color="blue">{org.type}</Tag>
|
<Tag color="blue">{org.type}</Tag>
|
||||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
<div style={{ fontSize: '12px', color: token.colorTextSecondary }}>
|
||||||
成员: {org.member_count} | 势力: {org.power_level}
|
成员: {org.member_count} | 势力: {org.power_level}
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -415,7 +416,7 @@ export default function Organizations() {
|
|||||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||||
{!selectedOrg ? (
|
{!selectedOrg ? (
|
||||||
<Card style={{ height: '100%' }}>
|
<Card style={{ height: '100%' }}>
|
||||||
<div style={{ textAlign: 'center', padding: '100px 20px', color: '#999' }}>
|
<div style={{ textAlign: 'center', padding: '100px 20px', color: token.colorTextTertiary }}>
|
||||||
{isMobile && organizations.length > 0 && (
|
{isMobile && organizations.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|||||||
+142
-131
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { Button, List, Modal, Form, Input, message, Empty, Space, Popconfirm, Card, Select, Radio, Tag, InputNumber, Tabs, Pagination } from 'antd';
|
import { Button, List, Modal, Form, Input, message, Empty, Space, Popconfirm, Card, Select, Radio, Tag, InputNumber, Tabs, Pagination, theme } from 'antd';
|
||||||
import { EditOutlined, DeleteOutlined, ThunderboltOutlined, BranchesOutlined, AppstoreAddOutlined, CheckCircleOutlined, ExclamationCircleOutlined, PlusOutlined, FileTextOutlined } from '@ant-design/icons';
|
import { EditOutlined, DeleteOutlined, ThunderboltOutlined, BranchesOutlined, AppstoreAddOutlined, CheckCircleOutlined, ExclamationCircleOutlined, PlusOutlined, FileTextOutlined } from '@ant-design/icons';
|
||||||
import { useStore } from '../store';
|
import { useStore } from '../store';
|
||||||
import { useOutlineSync } from '../store/hooks';
|
import { useOutlineSync } from '../store/hooks';
|
||||||
@@ -119,6 +119,9 @@ export default function Outline() {
|
|||||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||||
const [isExpanding, setIsExpanding] = useState(false);
|
const [isExpanding, setIsExpanding] = useState(false);
|
||||||
const [projectCharacters, setProjectCharacters] = useState<Array<{ label: string; value: string }>>([]);
|
const [projectCharacters, setProjectCharacters] = useState<Array<{ label: string; value: string }>>([]);
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const alphaColor = (color: string, alpha: number) =>
|
||||||
|
`color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||||
|
|
||||||
// ✅ 新增:记录场景区域的展开/折叠状态
|
// ✅ 新增:记录场景区域的展开/折叠状态
|
||||||
const [scenesExpandStatus, setScenesExpandStatus] = useState<Record<string, boolean>>({});
|
const [scenesExpandStatus, setScenesExpandStatus] = useState<Record<string, boolean>>({});
|
||||||
@@ -776,7 +779,7 @@ export default function Outline() {
|
|||||||
console.log('已同步到Form,当前Form值:', generateForm.getFieldsValue());
|
console.log('已同步到Form,当前Form值:', generateForm.getFieldsValue());
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div style={{ color: 'var(--color-text-tertiary)', fontSize: 12, marginTop: 4 }}>
|
<div style={{ color: token.colorTextTertiary, fontSize: 12, marginTop: 4 }}>
|
||||||
{defaultModel ? `当前默认模型: ${loadedModels.find(m => m.value === defaultModel)?.label || defaultModel}` : '未配置默认模型'}
|
{defaultModel ? `当前默认模型: ${loadedModels.find(m => m.value === defaultModel)?.label || defaultModel}` : '未配置默认模型'}
|
||||||
</div>
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -853,19 +856,19 @@ export default function Outline() {
|
|||||||
<p>序号 <strong>{values.order_index}</strong> 已被使用:</p>
|
<p>序号 <strong>{values.order_index}</strong> 已被使用:</p>
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: 12,
|
padding: 12,
|
||||||
background: 'var(--color-warning-bg)',
|
background: token.colorWarningBg,
|
||||||
borderRadius: 4,
|
borderRadius: token.borderRadius,
|
||||||
border: '1px solid var(--color-warning-border)',
|
border: `1px solid ${token.colorWarningBorder}`,
|
||||||
marginTop: 8
|
marginTop: 8
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontWeight: 500, color: 'var(--color-warning)' }}>
|
<div style={{ fontWeight: 500, color: token.colorWarning }}>
|
||||||
{currentProject?.outline_mode === 'one-to-one'
|
{currentProject?.outline_mode === 'one-to-one'
|
||||||
? `第${existingOutline.order_index}章`
|
? `第${existingOutline.order_index}章`
|
||||||
: `第${existingOutline.order_index}卷`
|
: `第${existingOutline.order_index}卷`
|
||||||
}:{existingOutline.title}
|
}:{existingOutline.title}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p style={{ marginTop: 12, color: 'var(--color-text-secondary)' }}>
|
<p style={{ marginTop: 12, color: token.colorTextSecondary }}>
|
||||||
💡 建议使用序号 <strong>{nextOrderIndex}</strong>,或选择其他未使用的序号
|
💡 建议使用序号 <strong>{nextOrderIndex}</strong>,或选择其他未使用的序号
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -928,18 +931,18 @@ export default function Outline() {
|
|||||||
</p>
|
</p>
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: 12,
|
padding: 12,
|
||||||
background: 'var(--color-warning-bg)',
|
background: token.colorWarningBg,
|
||||||
borderRadius: 4,
|
borderRadius: token.borderRadius,
|
||||||
border: '1px solid var(--color-warning-border)'
|
border: `1px solid ${token.colorWarningBorder}`
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontWeight: 500, marginBottom: 8, color: 'var(--color-warning)' }}>
|
<div style={{ fontWeight: 500, marginBottom: 8, color: token.colorWarning }}>
|
||||||
⚠️ 需要先展开:
|
⚠️ 需要先展开:
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: 'var(--color-text-secondary)' }}>
|
<div style={{ color: token.colorTextSecondary }}>
|
||||||
第{prevOutline.order_index}卷:《{prevOutline.title}》
|
第{prevOutline.order_index}卷:《{prevOutline.title}》
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p style={{ marginTop: 12, color: 'var(--color-text-secondary)', fontSize: 13 }}>
|
<p style={{ marginTop: 12, color: token.colorTextSecondary, fontSize: 13 }}>
|
||||||
💡 提示:您也可以使用「批量展开」功能,系统会自动按顺序处理所有大纲。
|
💡 提示:您也可以使用「批量展开」功能,系统会自动按顺序处理所有大纲。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -978,9 +981,9 @@ export default function Outline() {
|
|||||||
centered: true,
|
centered: true,
|
||||||
content: (
|
content: (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-bg-layout)', borderRadius: 4 }}>
|
<div style={{ marginBottom: 16, padding: 12, background: token.colorBgLayout, borderRadius: token.borderRadius }}>
|
||||||
<div style={{ fontWeight: 500, marginBottom: 4 }}>大纲标题</div>
|
<div style={{ fontWeight: 500, marginBottom: 4 }}>大纲标题</div>
|
||||||
<div style={{ color: 'var(--color-text-secondary)' }}>{outlineTitle}</div>
|
<div style={{ color: token.colorTextSecondary }}>{outlineTitle}</div>
|
||||||
</div>
|
</div>
|
||||||
<Form
|
<Form
|
||||||
form={expansionForm}
|
form={expansionForm}
|
||||||
@@ -1132,7 +1135,7 @@ export default function Outline() {
|
|||||||
modalApi.info({
|
modalApi.info({
|
||||||
title: (
|
title: (
|
||||||
<Space style={{ flexWrap: 'wrap' }}>
|
<Space style={{ flexWrap: 'wrap' }}>
|
||||||
<CheckCircleOutlined style={{ color: 'var(--color-success)' }} />
|
<CheckCircleOutlined style={{ color: token.colorSuccess }} />
|
||||||
<span>《{outlineTitle}》展开信息</span>
|
<span>《{outlineTitle}》展开信息</span>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
@@ -1164,10 +1167,10 @@ export default function Outline() {
|
|||||||
content: (
|
content: (
|
||||||
<div>
|
<div>
|
||||||
<p>此操作将删除大纲《{outlineTitle}》展开的所有 <strong>{data.chapter_count}</strong> 个章节。</p>
|
<p>此操作将删除大纲《{outlineTitle}》展开的所有 <strong>{data.chapter_count}</strong> 个章节。</p>
|
||||||
<p style={{ color: 'var(--color-primary)', marginTop: 8 }}>
|
<p style={{ color: token.colorPrimary, marginTop: 8 }}>
|
||||||
📝 注意:大纲本身会保留,您可以重新展开
|
📝 注意:大纲本身会保留,您可以重新展开
|
||||||
</p>
|
</p>
|
||||||
<p style={{ color: '#ff4d4f', marginTop: 8 }}>
|
<p style={{ color: token.colorError, marginTop: 8 }}>
|
||||||
⚠️ 警告:章节内容将永久删除且无法恢复!
|
⚠️ 警告:章节内容将永久删除且无法恢复!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1324,7 +1327,7 @@ export default function Outline() {
|
|||||||
key={sceneIdx}
|
key={sceneIdx}
|
||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#fafafa',
|
backgroundColor: token.colorFillQuaternary,
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}}
|
}}
|
||||||
@@ -1374,7 +1377,7 @@ export default function Outline() {
|
|||||||
modalApi.confirm({
|
modalApi.confirm({
|
||||||
title: (
|
title: (
|
||||||
<Space>
|
<Space>
|
||||||
<CheckCircleOutlined style={{ color: 'var(--color-success)' }} />
|
<CheckCircleOutlined style={{ color: token.colorSuccess }} />
|
||||||
<span>展开规划预览</span>
|
<span>展开规划预览</span>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
@@ -1438,7 +1441,7 @@ export default function Outline() {
|
|||||||
<Card size="small" title="场景">
|
<Card size="small" title="场景">
|
||||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||||
{plan.scenes.map((scene, sceneIdx) => (
|
{plan.scenes.map((scene, sceneIdx) => (
|
||||||
<Card key={sceneIdx} size="small" style={{ backgroundColor: '#fafafa' }}>
|
<Card key={sceneIdx} size="small" style={{ backgroundColor: token.colorFillQuaternary }}>
|
||||||
<div><strong>地点:</strong>{scene.location}</div>
|
<div><strong>地点:</strong>{scene.location}</div>
|
||||||
<div><strong>角色:</strong>{scene.characters.join('、')}</div>
|
<div><strong>角色:</strong>{scene.characters.join('、')}</div>
|
||||||
<div><strong>目的:</strong>{scene.purpose}</div>
|
<div><strong>目的:</strong>{scene.purpose}</div>
|
||||||
@@ -1511,8 +1514,16 @@ export default function Outline() {
|
|||||||
centered: true,
|
centered: true,
|
||||||
content: (
|
content: (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-warning-bg)', borderRadius: 4 }}>
|
<div
|
||||||
<div style={{ color: '#856404' }}>
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
padding: 12,
|
||||||
|
background: token.colorWarningBg,
|
||||||
|
borderRadius: token.borderRadius,
|
||||||
|
border: `1px solid ${token.colorWarningBorder}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ color: token.colorWarningText }}>
|
||||||
⚠️ 将对当前项目的所有 {outlines.length} 个大纲进行展开
|
⚠️ 将对当前项目的所有 {outlines.length} 个大纲进行展开
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1639,16 +1650,16 @@ export default function Outline() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
background: 'var(--color-warning-bg)',
|
background: token.colorWarningBg,
|
||||||
borderRadius: 4,
|
borderRadius: token.borderRadius,
|
||||||
border: '1px solid #ffe58f'
|
border: `1px solid ${token.colorWarningBorder}`
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontWeight: 500, marginBottom: 8, color: 'var(--color-warning)' }}>
|
<div style={{ fontWeight: 500, marginBottom: 8, color: token.colorWarning }}>
|
||||||
⚠️ 以下大纲已展开过,已自动跳过:
|
⚠️ 以下大纲已展开过,已自动跳过:
|
||||||
</div>
|
</div>
|
||||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||||
{batchPreviewData.skipped_outlines.map((skipped: SkippedOutlineInfo, idx: number) => (
|
{batchPreviewData.skipped_outlines.map((skipped: SkippedOutlineInfo, idx: number) => (
|
||||||
<div key={idx} style={{ fontSize: 13, color: '#666' }}>
|
<div key={idx} style={{ fontSize: 13, color: token.colorTextSecondary }}>
|
||||||
• {skipped.outline_title} <Tag color="default" style={{ fontSize: 11 }}>{skipped.reason}</Tag>
|
• {skipped.outline_title} <Tag color="default" style={{ fontSize: 11 }}>{skipped.reason}</Tag>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -1661,11 +1672,11 @@ export default function Outline() {
|
|||||||
{/* 左栏:大纲列表 */}
|
{/* 左栏:大纲列表 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 280,
|
width: 280,
|
||||||
borderRight: '1px solid #f0f0f0',
|
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||||
paddingRight: 12,
|
paddingRight: 12,
|
||||||
overflowY: 'auto'
|
overflowY: 'auto'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontWeight: 500, marginBottom: 8, color: '#666' }}>大纲列表</div>
|
<div style={{ fontWeight: 500, marginBottom: 8, color: token.colorTextSecondary }}>大纲列表</div>
|
||||||
<List
|
<List
|
||||||
size="small"
|
size="small"
|
||||||
dataSource={batchPreviewData.expansion_results}
|
dataSource={batchPreviewData.expansion_results}
|
||||||
@@ -1679,10 +1690,10 @@ export default function Outline() {
|
|||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
background: selectedOutlineIdx === idx ? '#e6f7ff' : 'transparent',
|
background: selectedOutlineIdx === idx ? token.colorPrimaryBg : 'transparent',
|
||||||
borderRadius: 4,
|
borderRadius: token.borderRadius,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
border: selectedOutlineIdx === idx ? '1px solid var(--color-primary)' : '1px solid transparent'
|
border: selectedOutlineIdx === idx ? `1px solid ${token.colorPrimary}` : '1px solid transparent'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ width: '100%' }}>
|
<div style={{ width: '100%' }}>
|
||||||
@@ -1702,11 +1713,11 @@ export default function Outline() {
|
|||||||
{/* 中栏:章节列表 */}
|
{/* 中栏:章节列表 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 320,
|
width: 320,
|
||||||
borderRight: '1px solid #f0f0f0',
|
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||||
paddingRight: 12,
|
paddingRight: 12,
|
||||||
overflowY: 'auto'
|
overflowY: 'auto'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontWeight: 500, marginBottom: 8, color: '#666' }}>
|
<div style={{ fontWeight: 500, marginBottom: 8, color: token.colorTextSecondary }}>
|
||||||
章节列表 ({batchPreviewData.expansion_results[selectedOutlineIdx]?.actual_chapter_count || 0} 章)
|
章节列表 ({batchPreviewData.expansion_results[selectedOutlineIdx]?.actual_chapter_count || 0} 章)
|
||||||
</div>
|
</div>
|
||||||
{batchPreviewData.expansion_results[selectedOutlineIdx] && (
|
{batchPreviewData.expansion_results[selectedOutlineIdx] && (
|
||||||
@@ -1720,10 +1731,10 @@ export default function Outline() {
|
|||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
background: selectedChapterIdx === idx ? '#e6f7ff' : 'transparent',
|
background: selectedChapterIdx === idx ? token.colorPrimaryBg : 'transparent',
|
||||||
borderRadius: 4,
|
borderRadius: token.borderRadius,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
border: selectedChapterIdx === idx ? '1px solid var(--color-primary)' : '1px solid transparent'
|
border: selectedChapterIdx === idx ? `1px solid ${token.colorPrimary}` : '1px solid transparent'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ width: '100%' }}>
|
<div style={{ width: '100%' }}>
|
||||||
@@ -1744,7 +1755,7 @@ export default function Outline() {
|
|||||||
|
|
||||||
{/* 右栏:章节详情 */}
|
{/* 右栏:章节详情 */}
|
||||||
<div style={{ flex: 1, overflowY: 'auto', paddingLeft: 12 }}>
|
<div style={{ flex: 1, overflowY: 'auto', paddingLeft: 12 }}>
|
||||||
<div style={{ fontWeight: 500, marginBottom: 12, color: '#666' }}>章节详情</div>
|
<div style={{ fontWeight: 500, marginBottom: 12, color: token.colorTextSecondary }}>章节详情</div>
|
||||||
{batchPreviewData.expansion_results[selectedOutlineIdx]?.chapter_plans[selectedChapterIdx] ? (
|
{batchPreviewData.expansion_results[selectedOutlineIdx]?.chapter_plans[selectedChapterIdx] ? (
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
<Card size="small" title="情节概要" bordered={false}>
|
<Card size="small" title="情节概要" bordered={false}>
|
||||||
@@ -1775,7 +1786,7 @@ export default function Outline() {
|
|||||||
<Card size="small" title="场景" bordered={false}>
|
<Card size="small" title="场景" bordered={false}>
|
||||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||||
{batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].scenes!.map((scene: SceneInfo, sceneIdx: number) => (
|
{batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].scenes!.map((scene: SceneInfo, sceneIdx: number) => (
|
||||||
<Card key={sceneIdx} size="small" style={{ backgroundColor: '#fafafa' }}>
|
<Card key={sceneIdx} size="small" style={{ backgroundColor: token.colorFillQuaternary }}>
|
||||||
<div><strong>地点:</strong>{scene.location}</div>
|
<div><strong>地点:</strong>{scene.location}</div>
|
||||||
<div><strong>角色:</strong>{scene.characters.join('、')}</div>
|
<div><strong>角色:</strong>{scene.characters.join('、')}</div>
|
||||||
<div><strong>目的:</strong>{scene.purpose}</div>
|
<div><strong>目的:</strong>{scene.purpose}</div>
|
||||||
@@ -1876,7 +1887,7 @@ export default function Outline() {
|
|||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<CheckCircleOutlined style={{ color: 'var(--color-success)' }} />
|
<CheckCircleOutlined style={{ color: token.colorSuccess }} />
|
||||||
<span>批量展开规划预览</span>
|
<span>批量展开规划预览</span>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -1907,10 +1918,10 @@ export default function Outline() {
|
|||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
backgroundColor: 'var(--color-bg-container)',
|
backgroundColor: token.colorBgContainer,
|
||||||
padding: isMobile ? '12px 0' : '16px 0',
|
padding: isMobile ? '12px 0' : '16px 0',
|
||||||
marginBottom: isMobile ? 12 : 16,
|
marginBottom: isMobile ? 12 : 16,
|
||||||
borderBottom: '1px solid #f0f0f0',
|
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: isMobile ? 'column' : 'row',
|
flexDirection: isMobile ? 'column' : 'row',
|
||||||
gap: isMobile ? 12 : 0,
|
gap: isMobile ? 12 : 0,
|
||||||
@@ -1995,8 +2006,8 @@ export default function Outline() {
|
|||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
borderRadius: isMobile ? 6 : 8,
|
borderRadius: isMobile ? 6 : 8,
|
||||||
border: '1px solid #f0f0f0',
|
border: `1px solid ${token.colorBorderSecondary}`,
|
||||||
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.03)',
|
boxShadow: `0 1px 2px ${alphaColor(token.colorTextBase, 0.08)}`,
|
||||||
transition: 'all 0.3s ease'
|
transition: 'all 0.3s ease'
|
||||||
}}
|
}}
|
||||||
bodyStyle={{
|
bodyStyle={{
|
||||||
@@ -2004,14 +2015,14 @@ export default function Outline() {
|
|||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.08)';
|
e.currentTarget.style.boxShadow = `0 4px 12px ${alphaColor(token.colorTextBase, 0.16)}`;
|
||||||
e.currentTarget.style.borderColor = 'var(--color-primary)';
|
e.currentTarget.style.borderColor = token.colorPrimary;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
e.currentTarget.style.boxShadow = '0 1px 2px rgba(0, 0, 0, 0.03)';
|
e.currentTarget.style.boxShadow = `0 1px 2px ${alphaColor(token.colorTextBase, 0.08)}`;
|
||||||
e.currentTarget.style.borderColor = '#f0f0f0';
|
e.currentTarget.style.borderColor = token.colorBorderSecondary;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -2019,7 +2030,7 @@ export default function Outline() {
|
|||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
title={
|
title={
|
||||||
<Space size="small" style={{ fontSize: isMobile ? 13 : 16, flexWrap: 'wrap', lineHeight: isMobile ? '1.4' : '1.5' }}>
|
<Space size="small" style={{ fontSize: isMobile ? 13 : 16, flexWrap: 'wrap', lineHeight: isMobile ? '1.4' : '1.5' }}>
|
||||||
<span style={{ color: 'var(--color-primary)', fontWeight: 'bold', fontSize: isMobile ? 13 : 16 }}>
|
<span style={{ color: token.colorPrimary, fontWeight: 'bold', fontSize: isMobile ? 13 : 16 }}>
|
||||||
{currentProject?.outline_mode === 'one-to-one'
|
{currentProject?.outline_mode === 'one-to-one'
|
||||||
? `第${item.order_index || '?'}章`
|
? `第${item.order_index || '?'}章`
|
||||||
: `第${item.order_index || '?'}卷`
|
: `第${item.order_index || '?'}卷`
|
||||||
@@ -2042,16 +2053,16 @@ export default function Outline() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginBottom: isMobile ? 10 : 12,
|
marginBottom: isMobile ? 10 : 12,
|
||||||
padding: isMobile ? '8px 10px' : '10px 12px',
|
padding: isMobile ? '8px 10px' : '10px 12px',
|
||||||
background: 'linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%)',
|
background: token.colorFillQuaternary,
|
||||||
borderLeft: '3px solid #8c8c8c',
|
borderLeft: `3px solid ${token.colorBorderSecondary}`,
|
||||||
borderRadius: isMobile ? 4 : 6,
|
borderRadius: token.borderRadius,
|
||||||
fontSize: isMobile ? 12 : 13,
|
fontSize: isMobile ? 12 : 13,
|
||||||
color: '#262626',
|
color: token.colorText,
|
||||||
lineHeight: '1.6'
|
lineHeight: '1.6'
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: '#595959',
|
color: token.colorTextSecondary,
|
||||||
marginBottom: isMobile ? 4 : 6,
|
marginBottom: isMobile ? 4 : 6,
|
||||||
fontSize: isMobile ? 12 : 13
|
fontSize: isMobile ? 12 : 13
|
||||||
}}>
|
}}>
|
||||||
@@ -2059,11 +2070,11 @@ export default function Outline() {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: isMobile ? '6px 8px' : '6px 10px',
|
padding: isMobile ? '6px 8px' : '6px 10px',
|
||||||
background: '#ffffff',
|
background: token.colorBgContainer,
|
||||||
border: '1px solid #d9d9d9',
|
border: `1px solid ${token.colorBorder}`,
|
||||||
borderRadius: 4,
|
borderRadius: token.borderRadiusSM,
|
||||||
fontSize: isMobile ? 12 : 13,
|
fontSize: isMobile ? 12 : 13,
|
||||||
color: '#262626',
|
color: token.colorText,
|
||||||
lineHeight: '1.6'
|
lineHeight: '1.6'
|
||||||
}}>
|
}}>
|
||||||
{item.content}
|
{item.content}
|
||||||
@@ -2075,9 +2086,9 @@ export default function Outline() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginTop: isMobile ? 10 : 12,
|
marginTop: isMobile ? 10 : 12,
|
||||||
padding: isMobile ? '8px 10px' : '10px 12px',
|
padding: isMobile ? '8px 10px' : '10px 12px',
|
||||||
background: 'linear-gradient(135deg, #f5f3ff 0%, #faf5ff 100%)',
|
background: token.colorPrimaryBg,
|
||||||
borderLeft: '3px solid #9333ea',
|
borderLeft: `3px solid ${token.colorPrimary}`,
|
||||||
borderRadius: isMobile ? 4 : 6
|
borderRadius: token.borderRadius
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -2088,7 +2099,7 @@ export default function Outline() {
|
|||||||
<span style={{
|
<span style={{
|
||||||
fontSize: isMobile ? 12 : 13,
|
fontSize: isMobile ? 12 : 13,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: '#7c3aed',
|
color: token.colorPrimary,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4
|
gap: 4
|
||||||
@@ -2118,9 +2129,9 @@ export default function Outline() {
|
|||||||
padding: isMobile ? '2px 8px' : '3px 10px',
|
padding: isMobile ? '2px 8px' : '3px 10px',
|
||||||
fontSize: isMobile ? 11 : 12,
|
fontSize: isMobile ? 11 : 12,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
border: '1px solid #e9d5ff',
|
border: `1px solid ${token.colorPrimaryBorder}`,
|
||||||
background: '#ffffff',
|
background: token.colorBgContainer,
|
||||||
color: '#7c3aed',
|
color: token.colorPrimary,
|
||||||
whiteSpace: 'normal',
|
whiteSpace: 'normal',
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
@@ -2139,9 +2150,9 @@ export default function Outline() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginTop: isMobile ? 10 : 12,
|
marginTop: isMobile ? 10 : 12,
|
||||||
padding: isMobile ? '8px 10px' : '10px 12px',
|
padding: isMobile ? '8px 10px' : '10px 12px',
|
||||||
background: 'linear-gradient(135deg, #fff7ed 0%, #fffbeb 100%)',
|
background: token.colorWarningBg,
|
||||||
borderLeft: '3px solid #ea580c',
|
borderLeft: `3px solid ${token.colorWarning}`,
|
||||||
borderRadius: isMobile ? 4 : 6
|
borderRadius: token.borderRadius
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -2152,7 +2163,7 @@ export default function Outline() {
|
|||||||
<span style={{
|
<span style={{
|
||||||
fontSize: isMobile ? 12 : 13,
|
fontSize: isMobile ? 12 : 13,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: '#ea580c',
|
color: token.colorWarning,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4
|
gap: 4
|
||||||
@@ -2182,9 +2193,9 @@ export default function Outline() {
|
|||||||
padding: isMobile ? '2px 8px' : '3px 10px',
|
padding: isMobile ? '2px 8px' : '3px 10px',
|
||||||
fontSize: isMobile ? 11 : 12,
|
fontSize: isMobile ? 11 : 12,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
border: '1px solid #fed7aa',
|
border: `1px solid ${token.colorWarningBorder}`,
|
||||||
background: '#ffffff',
|
background: token.colorBgContainer,
|
||||||
color: '#ea580c',
|
color: token.colorWarning,
|
||||||
whiteSpace: 'normal',
|
whiteSpace: 'normal',
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
@@ -2209,9 +2220,9 @@ export default function Outline() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginTop: isMobile ? 10 : 12,
|
marginTop: isMobile ? 10 : 12,
|
||||||
padding: isMobile ? '8px 10px' : '10px 12px',
|
padding: isMobile ? '8px 10px' : '10px 12px',
|
||||||
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
|
background: token.colorInfoBg,
|
||||||
borderLeft: '3px solid #0ea5e9',
|
borderLeft: `3px solid ${token.colorInfo}`,
|
||||||
borderRadius: isMobile ? 4 : 6
|
borderRadius: token.borderRadius
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -2224,7 +2235,7 @@ export default function Outline() {
|
|||||||
<span style={{
|
<span style={{
|
||||||
fontSize: isMobile ? 12 : 13,
|
fontSize: isMobile ? 12 : 13,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: '#0284c7',
|
color: token.colorInfo,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4
|
gap: 4
|
||||||
@@ -2254,7 +2265,7 @@ export default function Outline() {
|
|||||||
fontSize: isMobile ? 10 : 11,
|
fontSize: isMobile ? 10 : 11,
|
||||||
height: isMobile ? 20 : 22,
|
height: isMobile ? 20 : 22,
|
||||||
padding: isMobile ? '0 6px' : '0 8px',
|
padding: isMobile ? '0 6px' : '0 8px',
|
||||||
color: '#0284c7'
|
color: token.colorInfo
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isExpanded ? '收起 ▲' : `展开 (${structureData.scenes!.length - maxVisibleScenes}+) ▼`}
|
{isExpanded ? '收起 ▲' : `展开 (${structureData.scenes!.length - maxVisibleScenes}+) ▼`}
|
||||||
@@ -2278,11 +2289,11 @@ export default function Outline() {
|
|||||||
key={idx}
|
key={idx}
|
||||||
style={{
|
style={{
|
||||||
padding: isMobile ? '6px 8px' : '8px 10px',
|
padding: isMobile ? '6px 8px' : '8px 10px',
|
||||||
background: '#ffffff',
|
background: token.colorBgContainer,
|
||||||
border: '1px solid #bae6fd',
|
border: `1px solid ${token.colorInfoBorder}`,
|
||||||
borderRadius: isMobile ? 4 : 6,
|
borderRadius: token.borderRadius,
|
||||||
fontSize: isMobile ? 11 : 12,
|
fontSize: isMobile ? 11 : 12,
|
||||||
color: '#0c4a6e',
|
color: token.colorText,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
gap: isMobile ? 6 : 8,
|
gap: isMobile ? 6 : 8,
|
||||||
@@ -2294,13 +2305,13 @@ export default function Outline() {
|
|||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
e.currentTarget.style.borderColor = '#0ea5e9';
|
e.currentTarget.style.borderColor = token.colorInfo;
|
||||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.15)';
|
e.currentTarget.style.boxShadow = `0 2px 8px ${alphaColor(token.colorInfo, 0.25)}`;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
e.currentTarget.style.borderColor = '#bae6fd';
|
e.currentTarget.style.borderColor = token.colorInfoBorder;
|
||||||
e.currentTarget.style.boxShadow = 'none';
|
e.currentTarget.style.boxShadow = 'none';
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -2332,9 +2343,9 @@ export default function Outline() {
|
|||||||
key={idx}
|
key={idx}
|
||||||
style={{
|
style={{
|
||||||
padding: isMobile ? '8px 10px' : '10px 12px',
|
padding: isMobile ? '8px 10px' : '10px 12px',
|
||||||
background: '#ffffff',
|
background: token.colorBgContainer,
|
||||||
border: '1px solid #bae6fd',
|
border: `1px solid ${token.colorInfoBorder}`,
|
||||||
borderRadius: isMobile ? 4 : 6,
|
borderRadius: token.borderRadius,
|
||||||
fontSize: isMobile ? 11 : 12,
|
fontSize: isMobile ? 11 : 12,
|
||||||
transition: 'all 0.2s ease',
|
transition: 'all 0.2s ease',
|
||||||
cursor: 'default',
|
cursor: 'default',
|
||||||
@@ -2344,13 +2355,13 @@ export default function Outline() {
|
|||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
e.currentTarget.style.borderColor = '#0ea5e9';
|
e.currentTarget.style.borderColor = token.colorInfo;
|
||||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.15)';
|
e.currentTarget.style.boxShadow = `0 2px 8px ${alphaColor(token.colorInfo, 0.25)}`;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
e.currentTarget.style.borderColor = '#bae6fd';
|
e.currentTarget.style.borderColor = token.colorInfoBorder;
|
||||||
e.currentTarget.style.boxShadow = 'none';
|
e.currentTarget.style.boxShadow = 'none';
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -2374,7 +2385,7 @@ export default function Outline() {
|
|||||||
</Tag>
|
</Tag>
|
||||||
<span style={{
|
<span style={{
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: '#0c4a6e',
|
color: token.colorText,
|
||||||
fontSize: isMobile ? 12 : 13,
|
fontSize: isMobile ? 12 : 13,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@@ -2387,7 +2398,7 @@ export default function Outline() {
|
|||||||
{scene.characters && scene.characters.length > 0 && (
|
{scene.characters && scene.characters.length > 0 && (
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: isMobile ? 10 : 11,
|
fontSize: isMobile ? 10 : 11,
|
||||||
color: '#64748b',
|
color: token.colorTextSecondary,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
paddingLeft: isMobile ? 2 : 4,
|
paddingLeft: isMobile ? 2 : 4,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@@ -2401,7 +2412,7 @@ export default function Outline() {
|
|||||||
{scene.purpose && (
|
{scene.purpose && (
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: isMobile ? 10 : 11,
|
fontSize: isMobile ? 10 : 11,
|
||||||
color: '#64748b',
|
color: token.colorTextSecondary,
|
||||||
paddingLeft: isMobile ? 2 : 4,
|
paddingLeft: isMobile ? 2 : 4,
|
||||||
lineHeight: '1.5',
|
lineHeight: '1.5',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@@ -2426,9 +2437,9 @@ export default function Outline() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginTop: 12,
|
marginTop: 12,
|
||||||
padding: '10px 12px',
|
padding: '10px 12px',
|
||||||
background: 'linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%)',
|
background: token.colorWarningBg,
|
||||||
borderLeft: '3px solid #f97316',
|
borderLeft: `3px solid ${token.colorWarning}`,
|
||||||
borderRadius: 6
|
borderRadius: token.borderRadius
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -2439,7 +2450,7 @@ export default function Outline() {
|
|||||||
<span style={{
|
<span style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: '#ea580c',
|
color: token.colorWarning,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4
|
gap: 4
|
||||||
@@ -2464,11 +2475,11 @@ export default function Outline() {
|
|||||||
key={idx}
|
key={idx}
|
||||||
style={{
|
style={{
|
||||||
padding: '6px 10px',
|
padding: '6px 10px',
|
||||||
background: '#ffffff',
|
background: token.colorBgContainer,
|
||||||
border: '1px solid #fed7aa',
|
border: `1px solid ${token.colorWarningBorder}`,
|
||||||
borderRadius: 4,
|
borderRadius: token.borderRadiusSM,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#9a3412',
|
color: token.colorWarningText,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
gap: 8
|
gap: 8
|
||||||
@@ -2503,9 +2514,9 @@ export default function Outline() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginTop: 12,
|
marginTop: 12,
|
||||||
padding: '10px 12px',
|
padding: '10px 12px',
|
||||||
background: 'linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%)',
|
background: token.colorSuccessBg,
|
||||||
borderLeft: '3px solid #22c55e',
|
borderLeft: `3px solid ${token.colorSuccess}`,
|
||||||
borderRadius: 6
|
borderRadius: token.borderRadius
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -2516,7 +2527,7 @@ export default function Outline() {
|
|||||||
<span style={{
|
<span style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: '#15803d',
|
color: token.colorSuccess,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4
|
gap: 4
|
||||||
@@ -2548,11 +2559,11 @@ export default function Outline() {
|
|||||||
key={idx}
|
key={idx}
|
||||||
style={{
|
style={{
|
||||||
padding: isMobile ? '6px 8px' : '8px 10px',
|
padding: isMobile ? '6px 8px' : '8px 10px',
|
||||||
background: '#ffffff',
|
background: token.colorBgContainer,
|
||||||
border: '1px solid #bbf7d0',
|
border: `1px solid ${token.colorSuccessBorder}`,
|
||||||
borderRadius: isMobile ? 4 : 6,
|
borderRadius: token.borderRadius,
|
||||||
fontSize: isMobile ? 11 : 12,
|
fontSize: isMobile ? 11 : 12,
|
||||||
color: '#166534',
|
color: token.colorText,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
gap: isMobile ? 6 : 8,
|
gap: isMobile ? 6 : 8,
|
||||||
@@ -2564,13 +2575,13 @@ export default function Outline() {
|
|||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
e.currentTarget.style.borderColor = '#22c55e';
|
e.currentTarget.style.borderColor = token.colorSuccess;
|
||||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(34, 197, 94, 0.15)';
|
e.currentTarget.style.boxShadow = `0 2px 8px ${alphaColor(token.colorSuccess, 0.25)}`;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
e.currentTarget.style.borderColor = '#bbf7d0';
|
e.currentTarget.style.borderColor = token.colorSuccessBorder;
|
||||||
e.currentTarget.style.boxShadow = 'none';
|
e.currentTarget.style.boxShadow = 'none';
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -2604,9 +2615,9 @@ export default function Outline() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginTop: 12,
|
marginTop: 12,
|
||||||
padding: '10px 12px',
|
padding: '10px 12px',
|
||||||
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
|
background: token.colorWarningBg,
|
||||||
borderLeft: '3px solid #f59e0b',
|
borderLeft: `3px solid ${token.colorWarning}`,
|
||||||
borderRadius: 6,
|
borderRadius: token.borderRadius,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 8
|
gap: 8
|
||||||
@@ -2614,7 +2625,7 @@ export default function Outline() {
|
|||||||
<span style={{
|
<span style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: '#b45309'
|
color: token.colorWarning
|
||||||
}}>
|
}}>
|
||||||
💫 情感基调:
|
💫 情感基调:
|
||||||
</span>
|
</span>
|
||||||
@@ -2625,9 +2636,9 @@ export default function Outline() {
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
padding: '2px 12px',
|
padding: '2px 12px',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: '#ffffff',
|
background: token.colorBgContainer,
|
||||||
border: '1px solid #fbbf24',
|
border: `1px solid ${token.colorWarningBorder}`,
|
||||||
color: '#b45309'
|
color: token.colorWarningText
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{structureData.emotion}
|
{structureData.emotion}
|
||||||
@@ -2640,26 +2651,26 @@ export default function Outline() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginTop: 12,
|
marginTop: 12,
|
||||||
padding: '10px 12px',
|
padding: '10px 12px',
|
||||||
background: 'linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)',
|
background: token.colorInfoBg,
|
||||||
borderLeft: '3px solid #3b82f6',
|
borderLeft: `3px solid ${token.colorInfo}`,
|
||||||
borderRadius: 6
|
borderRadius: token.borderRadius
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: '#1e40af',
|
color: token.colorInfo,
|
||||||
marginBottom: 6
|
marginBottom: 6
|
||||||
}}>
|
}}>
|
||||||
🎯 叙事目标
|
🎯 叙事目标
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#1e3a8a',
|
color: token.colorText,
|
||||||
lineHeight: '1.6',
|
lineHeight: '1.6',
|
||||||
padding: '6px 10px',
|
padding: '6px 10px',
|
||||||
background: '#ffffff',
|
background: token.colorBgContainer,
|
||||||
border: '1px solid #93c5fd',
|
border: `1px solid ${token.colorInfoBorder}`,
|
||||||
borderRadius: 4,
|
borderRadius: token.borderRadiusSM,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap'
|
whiteSpace: 'nowrap'
|
||||||
@@ -2676,7 +2687,7 @@ export default function Outline() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
paddingTop: 12,
|
paddingTop: 12,
|
||||||
borderTop: '1px solid #f0f0f0',
|
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
gap: 8
|
gap: 8
|
||||||
@@ -2729,8 +2740,8 @@ export default function Outline() {
|
|||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
backgroundColor: 'var(--color-bg-container)',
|
backgroundColor: token.colorBgContainer,
|
||||||
borderTop: '1px solid #f0f0f0',
|
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||||||
padding: isMobile ? '8px 0' : '10px 0',
|
padding: isMobile ? '8px 0' : '10px 0',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'flex-end'
|
justifyContent: 'flex-end'
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Card, Input, Button, message, Space } from 'antd';
|
|
||||||
import { ThunderboltOutlined } from '@ant-design/icons';
|
|
||||||
import { polishApi } from '../services/api';
|
|
||||||
|
|
||||||
const { TextArea } = Input;
|
|
||||||
|
|
||||||
export default function Polish() {
|
|
||||||
const [originalText, setOriginalText] = useState('');
|
|
||||||
const [polishedText, setPolishedText] = useState('');
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const handlePolish = async () => {
|
|
||||||
if (!originalText.trim()) {
|
|
||||||
message.warning('请输入要去味的文本');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const result = await polishApi.polishText({ text: originalText });
|
|
||||||
setPolishedText(result.polished_text);
|
|
||||||
message.success('AI去味完成');
|
|
||||||
} catch {
|
|
||||||
message.error('AI去味失败');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopy = () => {
|
|
||||||
navigator.clipboard.writeText(polishedText);
|
|
||||||
message.success('已复制到剪贴板');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2 style={{ marginBottom: 16 }}>AI去味工具</h2>
|
|
||||||
<p style={{ color: 'rgba(0,0,0,0.45)', marginBottom: 24 }}>
|
|
||||||
将AI生成的文本变得更自然、更像人类作家的手笔
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
|
||||||
<Card title="原始文本" extra={
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<ThunderboltOutlined />}
|
|
||||||
onClick={handlePolish}
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
开始去味
|
|
||||||
</Button>
|
|
||||||
}>
|
|
||||||
<TextArea
|
|
||||||
rows={10}
|
|
||||||
placeholder="粘贴或输入需要去味的文本..."
|
|
||||||
value={originalText}
|
|
||||||
onChange={(e) => setOriginalText(e.target.value)}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{polishedText && (
|
|
||||||
<Card title="去味后文本" extra={
|
|
||||||
<Button onClick={handleCopy}>复制文本</Button>
|
|
||||||
}>
|
|
||||||
<TextArea
|
|
||||||
rows={10}
|
|
||||||
value={polishedText}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useParams, useNavigate, Outlet, Link, useLocation } from 'react-router-dom';
|
import { useParams, useNavigate, Outlet, Link, useLocation } from 'react-router-dom';
|
||||||
import { Layout, Menu, Spin, Button, Drawer } from 'antd';
|
import { Layout, Menu, Spin, Button, Drawer, theme } from 'antd';
|
||||||
import {
|
import {
|
||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
@@ -18,10 +18,14 @@ import {
|
|||||||
TrophyOutlined,
|
TrophyOutlined,
|
||||||
BulbOutlined,
|
BulbOutlined,
|
||||||
CloudOutlined,
|
CloudOutlined,
|
||||||
|
MoonOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useStore } from '../store';
|
import { useStore } from '../store';
|
||||||
import { useCharacterSync, useOutlineSync, useChapterSync } from '../store/hooks';
|
import { useCharacterSync, useOutlineSync, useChapterSync } from '../store/hooks';
|
||||||
import { projectApi } from '../services/api';
|
import { projectApi } from '../services/api';
|
||||||
|
import ThemeSwitch from '../components/ThemeSwitch';
|
||||||
|
import { useThemeMode } from '../theme/useThemeMode';
|
||||||
|
import { getStoredSidebarCollapsed, setStoredSidebarCollapsed } from '../utils/sidebarState';
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout;
|
const { Header, Sider, Content } = Layout;
|
||||||
|
|
||||||
@@ -32,9 +36,17 @@ export default function ProjectDetail() {
|
|||||||
const { projectId } = useParams<{ projectId: string }>();
|
const { projectId } = useParams<{ projectId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState<boolean>(() => getStoredSidebarCollapsed());
|
||||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||||
const [mobile, setMobile] = useState(isMobile());
|
const [mobile, setMobile] = useState(isMobile());
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||||
|
const { mode, resolvedMode, setMode } = useThemeMode();
|
||||||
|
const cycleThemeMode = () => {
|
||||||
|
const nextMode = mode === 'light' ? 'dark' : mode === 'dark' ? 'system' : 'light';
|
||||||
|
setMode(nextMode);
|
||||||
|
};
|
||||||
|
const collapsedThemeIcon = mode === 'light' ? <BulbOutlined /> : mode === 'dark' ? <MoonOutlined /> : <CloudOutlined />;
|
||||||
|
|
||||||
// 监听窗口大小变化
|
// 监听窗口大小变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -47,6 +59,10 @@ export default function ProjectDetail() {
|
|||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
return () => window.removeEventListener('resize', handleResize);
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStoredSidebarCollapsed(collapsed);
|
||||||
|
}, [collapsed]);
|
||||||
const {
|
const {
|
||||||
currentProject,
|
currentProject,
|
||||||
setCurrentProject,
|
setCurrentProject,
|
||||||
@@ -97,6 +113,81 @@ export default function ProjectDetail() {
|
|||||||
// Hook 内部已经更新了 store,不需要再次刷新
|
// Hook 内部已经更新了 store,不需要再次刷新
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
|
{
|
||||||
|
key: 'sponsor',
|
||||||
|
icon: <HeartOutlined />,
|
||||||
|
label: <Link to={`/project/${projectId}/sponsor`}>赞助支持</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'group' as const,
|
||||||
|
label: '创作管理',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'world-setting',
|
||||||
|
icon: <GlobalOutlined />,
|
||||||
|
label: <Link to={`/project/${projectId}/world-setting`}>世界设定</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'characters',
|
||||||
|
icon: <TeamOutlined />,
|
||||||
|
label: <Link to={`/project/${projectId}/characters`}>角色管理</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'organizations',
|
||||||
|
icon: <BankOutlined />,
|
||||||
|
label: <Link to={`/project/${projectId}/organizations`}>组织管理</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'careers',
|
||||||
|
icon: <TrophyOutlined />,
|
||||||
|
label: <Link to={`/project/${projectId}/careers`}>职业管理</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'relationships',
|
||||||
|
icon: <ApartmentOutlined />,
|
||||||
|
label: <Link to={`/project/${projectId}/relationships`}>关系管理</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'outline',
|
||||||
|
icon: <FileTextOutlined />,
|
||||||
|
label: <Link to={`/project/${projectId}/outline`}>大纲管理</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'chapters',
|
||||||
|
icon: <BookOutlined />,
|
||||||
|
label: <Link to={`/project/${projectId}/chapters`}>章节管理</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'chapter-analysis',
|
||||||
|
icon: <FundOutlined />,
|
||||||
|
label: <Link to={`/project/${projectId}/chapter-analysis`}>剧情分析</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'foreshadows',
|
||||||
|
icon: <BulbOutlined />,
|
||||||
|
label: <Link to={`/project/${projectId}/foreshadows`}>伏笔管理</Link>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'group' as const,
|
||||||
|
label: '创作工具',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'writing-styles',
|
||||||
|
icon: <EditOutlined />,
|
||||||
|
label: <Link to={`/project/${projectId}/writing-styles`}>写作风格</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'prompt-workshop',
|
||||||
|
icon: <CloudOutlined />,
|
||||||
|
label: <Link to={`/project/${projectId}/prompt-workshop`}>提示词工坊</Link>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const menuItemsCollapsed = [
|
||||||
{
|
{
|
||||||
key: 'sponsor',
|
key: 'sponsor',
|
||||||
icon: <HeartOutlined />,
|
icon: <HeartOutlined />,
|
||||||
@@ -157,11 +248,6 @@ export default function ProjectDetail() {
|
|||||||
icon: <CloudOutlined />,
|
icon: <CloudOutlined />,
|
||||||
label: <Link to={`/project/${projectId}/prompt-workshop`}>提示词工坊</Link>,
|
label: <Link to={`/project/${projectId}/prompt-workshop`}>提示词工坊</Link>,
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// key: 'polish',
|
|
||||||
// icon: <ToolOutlined />,
|
|
||||||
// label: <Link to={`/project/${projectId}/polish`}>AI去味</Link>,
|
|
||||||
// },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// 根据当前路径动态确定选中的菜单项
|
// 根据当前路径动态确定选中的菜单项
|
||||||
@@ -204,9 +290,9 @@ export default function ProjectDetail() {
|
|||||||
selectedKeys={[selectedKey]}
|
selectedKeys={[selectedKey]}
|
||||||
style={{
|
style={{
|
||||||
borderRight: 0,
|
borderRight: 0,
|
||||||
paddingTop: '16px'
|
paddingTop: '12px'
|
||||||
}}
|
}}
|
||||||
items={menuItems}
|
items={collapsed ? menuItemsCollapsed : menuItems}
|
||||||
onClick={() => mobile && setDrawerVisible(false)}
|
onClick={() => mobile && setDrawerVisible(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,54 +301,43 @@ export default function ProjectDetail() {
|
|||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: '100vh', height: '100vh', overflow: 'hidden' }}>
|
<Layout style={{ minHeight: '100vh', height: '100vh', overflow: 'hidden' }}>
|
||||||
<Header style={{
|
<Header style={{
|
||||||
background: 'var(--color-primary)',
|
background: token.colorPrimary,
|
||||||
padding: mobile ? '0 12px' : '0 24px',
|
padding: mobile ? '0 12px' : '0 24px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: mobile ? 0 : (collapsed ? 60 : 220),
|
||||||
right: 0,
|
right: 0,
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
boxShadow: 'var(--shadow-header)',
|
boxShadow: `0 2px 10px ${alphaColor(token.colorText, 0.16)}`,
|
||||||
height: mobile ? 56 : 70
|
height: mobile ? 56 : 70,
|
||||||
|
transition: 'left 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
overflow: 'hidden'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', zIndex: 1 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', zIndex: 1 }}>
|
||||||
<Button
|
{mobile && (
|
||||||
type="text"
|
|
||||||
icon={mobile ? <MenuUnfoldOutlined /> : (collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />)}
|
|
||||||
onClick={() => mobile ? setDrawerVisible(true) : setCollapsed(!collapsed)}
|
|
||||||
style={{
|
|
||||||
fontSize: mobile ? '18px' : '20px',
|
|
||||||
color: '#fff',
|
|
||||||
width: mobile ? '36px' : '40px',
|
|
||||||
height: mobile ? '36px' : '40px'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{!mobile && (
|
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<ArrowLeftOutlined />}
|
icon={<MenuUnfoldOutlined />}
|
||||||
onClick={() => navigate('/')}
|
onClick={() => setDrawerVisible(true)}
|
||||||
style={{
|
style={{
|
||||||
fontSize: '16px',
|
fontSize: '18px',
|
||||||
color: '#fff',
|
color: token.colorWhite,
|
||||||
height: '40px',
|
width: '36px',
|
||||||
padding: '0 16px'
|
height: '36px'
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
返回主页
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 style={{
|
<h2 style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
color: '#fff',
|
color: token.colorWhite,
|
||||||
fontSize: mobile ? '16px' : '24px',
|
fontSize: mobile ? '16px' : '24px',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
textShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
textShadow: `0 2px 4px ${alphaColor(token.colorText, 0.2)}`,
|
||||||
position: mobile ? 'static' : 'absolute',
|
position: mobile ? 'static' : 'absolute',
|
||||||
left: mobile ? 'auto' : '50%',
|
left: mobile ? 'auto' : '50%',
|
||||||
transform: mobile ? 'none' : 'translateX(-50%)',
|
transform: mobile ? 'none' : 'translateX(-50%)',
|
||||||
@@ -284,7 +359,7 @@ export default function ProjectDetail() {
|
|||||||
onClick={() => navigate('/')}
|
onClick={() => navigate('/')}
|
||||||
style={{
|
style={{
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
color: '#fff',
|
color: token.colorWhite,
|
||||||
height: '36px',
|
height: '36px',
|
||||||
padding: '0 8px',
|
padding: '0 8px',
|
||||||
zIndex: 1
|
zIndex: 1
|
||||||
@@ -315,23 +390,23 @@ export default function ProjectDetail() {
|
|||||||
minWidth: '56px',
|
minWidth: '56px',
|
||||||
height: '56px',
|
height: '56px',
|
||||||
padding: '0 12px',
|
padding: '0 12px',
|
||||||
boxShadow: 'inset 0 0 15px rgba(255, 255, 255, 0.15), 0 4px 10px rgba(0, 0, 0, 0.1)',
|
boxShadow: `inset 0 0 15px ${alphaColor(token.colorWhite, 0.15)}, 0 4px 10px ${alphaColor(token.colorText, 0.1)}`,
|
||||||
cursor: 'default',
|
cursor: 'default',
|
||||||
transition: 'all 0.3s ease',
|
transition: 'all 0.3s ease',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.transform = 'translateY(-3px) scale(1.02)';
|
e.currentTarget.style.transform = 'translateY(-3px) scale(1.02)';
|
||||||
e.currentTarget.style.boxShadow = 'inset 0 0 20px rgba(255, 255, 255, 0.25), 0 8px 16px rgba(0, 0, 0, 0.15)';
|
e.currentTarget.style.boxShadow = `inset 0 0 20px ${alphaColor(token.colorWhite, 0.25)}, 0 8px 16px ${alphaColor(token.colorText, 0.15)}`;
|
||||||
e.currentTarget.style.border = '1px solid rgba(255, 255, 255, 0.1)';
|
e.currentTarget.style.border = `1px solid ${alphaColor(token.colorWhite, 0.1)}`;
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.transform = 'translateY(0) scale(1)';
|
e.currentTarget.style.transform = 'translateY(0) scale(1)';
|
||||||
e.currentTarget.style.boxShadow = 'inset 0 0 15px rgba(255, 255, 255, 0.15), 0 4px 10px rgba(0, 0, 0, 0.1)';
|
e.currentTarget.style.boxShadow = `inset 0 0 15px ${alphaColor(token.colorWhite, 0.15)}, 0 4px 10px ${alphaColor(token.colorText, 0.1)}`;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{
|
<span style={{
|
||||||
fontSize: '11px',
|
fontSize: '11px',
|
||||||
color: 'rgba(255, 255, 255, 0.9)',
|
color: alphaColor(token.colorWhite, 0.9),
|
||||||
marginBottom: '2px',
|
marginBottom: '2px',
|
||||||
lineHeight: 1
|
lineHeight: 1
|
||||||
}}>
|
}}>
|
||||||
@@ -340,7 +415,7 @@ export default function ProjectDetail() {
|
|||||||
<span style={{
|
<span style={{
|
||||||
fontSize: '15px',
|
fontSize: '15px',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#fff',
|
color: token.colorWhite,
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
fontFamily: 'Monaco, monospace'
|
fontFamily: 'Monaco, monospace'
|
||||||
}}>
|
}}>
|
||||||
@@ -357,7 +432,24 @@ export default function ProjectDetail() {
|
|||||||
<Layout style={{ marginTop: mobile ? 56 : 70 }}>
|
<Layout style={{ marginTop: mobile ? 56 : 70 }}>
|
||||||
{mobile ? (
|
{mobile ? (
|
||||||
<Drawer
|
<Drawer
|
||||||
title="导航菜单"
|
title={
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
|
<div style={{
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
background: token.colorPrimary,
|
||||||
|
borderRadius: 8,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: token.colorWhite,
|
||||||
|
fontSize: 16,
|
||||||
|
}}>
|
||||||
|
<BookOutlined />
|
||||||
|
</div>
|
||||||
|
<span style={{ fontWeight: 600, fontSize: 16 }}>MuMuAINovel</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
placement="left"
|
placement="left"
|
||||||
onClose={() => setDrawerVisible(false)}
|
onClose={() => setDrawerVisible(false)}
|
||||||
open={drawerVisible}
|
open={drawerVisible}
|
||||||
@@ -365,6 +457,13 @@ export default function ProjectDetail() {
|
|||||||
styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column' } }}
|
styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column' } }}
|
||||||
>
|
>
|
||||||
{renderMenu()}
|
{renderMenu()}
|
||||||
|
<div style={{ padding: 16, borderTop: `1px solid ${token.colorBorderSecondary}` }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 12, color: token.colorTextTertiary, marginBottom: 8 }}>
|
||||||
|
<span>主题模式</span>
|
||||||
|
<span>{resolvedMode === 'dark' ? '深色' : '浅色'}</span>
|
||||||
|
</div>
|
||||||
|
<ThemeSwitch block />
|
||||||
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
) : (
|
) : (
|
||||||
<Sider
|
<Sider
|
||||||
@@ -374,15 +473,18 @@ export default function ProjectDetail() {
|
|||||||
trigger={null}
|
trigger={null}
|
||||||
width={220}
|
width={220}
|
||||||
collapsedWidth={60}
|
collapsedWidth={60}
|
||||||
className="modern-sider"
|
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 70,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
height: 'calc(100vh - 70px)'
|
height: '100vh',
|
||||||
|
background: token.colorBgContainer,
|
||||||
|
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||||
|
boxShadow: `4px 0 16px ${alphaColor(token.colorText, 0.06)}`,
|
||||||
|
zIndex: 1000
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -390,18 +492,148 @@ export default function ProjectDetail() {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column'
|
flexDirection: 'column'
|
||||||
}}>
|
}}>
|
||||||
|
<div style={{
|
||||||
|
height: 70,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: collapsed ? 0 : '0 12px',
|
||||||
|
background: token.colorPrimary,
|
||||||
|
flexShrink: 0,
|
||||||
|
justifyContent: collapsed ? 'center' : 'space-between',
|
||||||
|
gap: 8
|
||||||
|
}}>
|
||||||
|
{collapsed ? (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<MenuUnfoldOutlined />}
|
||||||
|
onClick={() => setCollapsed(false)}
|
||||||
|
style={{
|
||||||
|
color: token.colorWhite,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
padding: 0,
|
||||||
|
borderRadius: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0, overflow: 'hidden' }}>
|
||||||
|
<div style={{
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
background: alphaColor(token.colorWhite, 0.2),
|
||||||
|
borderRadius: 8,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: token.colorWhite,
|
||||||
|
fontSize: 16,
|
||||||
|
backdropFilter: 'blur(4px)'
|
||||||
|
}}>
|
||||||
|
<BookOutlined />
|
||||||
|
</div>
|
||||||
|
<span style={{
|
||||||
|
color: token.colorWhite,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 15,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis'
|
||||||
|
}}>
|
||||||
|
MuMuAINovel
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<MenuFoldOutlined />}
|
||||||
|
onClick={() => setCollapsed(true)}
|
||||||
|
style={{
|
||||||
|
color: token.colorWhite,
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
padding: 0,
|
||||||
|
flexShrink: 0
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{renderMenu()}
|
{renderMenu()}
|
||||||
|
<div style={{
|
||||||
|
padding: collapsed ? '12px 8px' : '12px',
|
||||||
|
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||||||
|
flexShrink: 0
|
||||||
|
}}>
|
||||||
|
{collapsed ? (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={collapsedThemeIcon}
|
||||||
|
onClick={cycleThemeMode}
|
||||||
|
title={`主题模式:${mode === 'light' ? '浅色' : mode === 'dark' ? '深色' : '跟随系统'}(点击切换)`}
|
||||||
|
style={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
background: alphaColor(token.colorBgContainer, 0.65),
|
||||||
|
border: `1px solid ${token.colorBorder}`,
|
||||||
|
color: token.colorText,
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
style={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
background: alphaColor(token.colorBgContainer, 0.65),
|
||||||
|
border: `1px solid ${token.colorBorder}`,
|
||||||
|
color: token.colorText,
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 12, color: token.colorTextTertiary }}>
|
||||||
|
<span>主题模式</span>
|
||||||
|
<span>{resolvedMode === 'dark' ? '深色' : '浅色'}</span>
|
||||||
|
</div>
|
||||||
|
<ThemeSwitch block />
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
block
|
||||||
|
style={{
|
||||||
|
color: token.colorText,
|
||||||
|
height: 40,
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
padding: '0 12px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
返回主页
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Sider>
|
</Sider>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Layout style={{
|
<Layout style={{
|
||||||
marginLeft: mobile ? 0 : (collapsed ? 60 : 220),
|
marginLeft: mobile ? 0 : (collapsed ? 60 : 220),
|
||||||
transition: 'all 0.2s'
|
transition: 'margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
|
||||||
}}>
|
}}>
|
||||||
<Content
|
<Content
|
||||||
style={{
|
style={{
|
||||||
background: 'var(--color-bg-base)',
|
background: token.colorBgLayout,
|
||||||
padding: mobile ? 12 : 24,
|
padding: mobile ? 12 : 24,
|
||||||
height: mobile ? 'calc(100vh - 56px)' : 'calc(100vh - 70px)',
|
height: mobile ? 'calc(100vh - 56px)' : 'calc(100vh - 70px)',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@@ -410,10 +642,10 @@ export default function ProjectDetail() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{
|
<div style={{
|
||||||
background: 'var(--color-bg-container)',
|
background: token.colorBgContainer,
|
||||||
padding: mobile ? 12 : 24,
|
padding: mobile ? 12 : 24,
|
||||||
borderRadius: mobile ? '8px' : '12px',
|
borderRadius: mobile ? '8px' : '12px',
|
||||||
boxShadow: 'var(--shadow-card)',
|
boxShadow: `0 8px 24px ${alphaColor(token.colorText, 0.08)}`,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Form, Input, InputNumber, Select, Button, Card,
|
Form, Input, InputNumber, Select, Button, Card,
|
||||||
Row, Col, Typography, Space, message, Radio
|
Row, Col, Typography, Space, message, Radio, theme
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
RocketOutlined, ArrowLeftOutlined, CheckCircleOutlined
|
RocketOutlined, ArrowLeftOutlined, CheckCircleOutlined
|
||||||
@@ -18,6 +18,7 @@ export default function ProjectWizardNew() {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const [currentStep, setCurrentStep] = useState<'form' | 'generating'>('form');
|
const [currentStep, setCurrentStep] = useState<'form' | 'generating'>('form');
|
||||||
@@ -196,7 +197,7 @@ export default function ProjectWizardNew() {
|
|||||||
<Card
|
<Card
|
||||||
hoverable
|
hoverable
|
||||||
style={{
|
style={{
|
||||||
borderColor: form.getFieldValue('outline_mode') === 'one-to-one' ? 'var(--color-primary)' : 'var(--color-border)',
|
// borderColor: form.getFieldValue('outline_mode') === 'one-to-one' ? token.colorPrimary : token.colorBorder,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
}}
|
}}
|
||||||
@@ -205,13 +206,13 @@ export default function ProjectWizardNew() {
|
|||||||
<Radio value="one-to-one" style={{ width: '100%' }}>
|
<Radio value="one-to-one" style={{ width: '100%' }}>
|
||||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||||
<div style={{ fontSize: 16, fontWeight: 'bold' }}>
|
<div style={{ fontSize: 16, fontWeight: 'bold' }}>
|
||||||
<CheckCircleOutlined style={{ marginRight: 8, color: 'var(--color-success)' }} />
|
<CheckCircleOutlined style={{ marginRight: 8, color: token.colorSuccess }} />
|
||||||
传统模式 (1→1)
|
传统模式 (1→1)
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 12, color: '#666' }}>
|
<div style={{ fontSize: 12, color: token.colorTextSecondary }}>
|
||||||
一个大纲对应一个章节,简单直接
|
一个大纲对应一个章节,简单直接
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 11, color: '#999' }}>
|
<div style={{ fontSize: 11, color: token.colorTextTertiary }}>
|
||||||
💡 适合:简单剧情、快速创作、短篇小说
|
💡 适合:简单剧情、快速创作、短篇小说
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -223,7 +224,7 @@ export default function ProjectWizardNew() {
|
|||||||
<Card
|
<Card
|
||||||
hoverable
|
hoverable
|
||||||
style={{
|
style={{
|
||||||
borderColor: form.getFieldValue('outline_mode') === 'one-to-many' ? 'var(--color-primary)' : 'var(--color-border)',
|
// borderColor: form.getFieldValue('outline_mode') === 'one-to-many' ? token.colorPrimary : token.colorBorder,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
}}
|
}}
|
||||||
@@ -232,13 +233,13 @@ export default function ProjectWizardNew() {
|
|||||||
<Radio value="one-to-many" style={{ width: '100%' }}>
|
<Radio value="one-to-many" style={{ width: '100%' }}>
|
||||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||||
<div style={{ fontSize: 16, fontWeight: 'bold' }}>
|
<div style={{ fontSize: 16, fontWeight: 'bold' }}>
|
||||||
<CheckCircleOutlined style={{ marginRight: 8, color: 'var(--color-success)' }} />
|
<CheckCircleOutlined style={{ marginRight: 8, color: token.colorSuccess }} />
|
||||||
细化模式 (1→N) 推荐
|
细化模式 (1→N) 推荐
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 12, color: '#666' }}>
|
<div style={{ fontSize: 12, color: token.colorTextSecondary }}>
|
||||||
一个大纲可展开为多个章节,灵活控制
|
一个大纲可展开为多个章节,灵活控制
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 11, color: '#999' }}>
|
<div style={{ fontSize: 11, color: token.colorTextTertiary }}>
|
||||||
💡 适合:复杂剧情、长篇创作、需要细化控制
|
💡 适合:复杂剧情、长篇创作、需要细化控制
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -322,15 +323,15 @@ export default function ProjectWizardNew() {
|
|||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
minHeight: '100dvh',
|
minHeight: '100dvh',
|
||||||
background: 'var(--color-bg-base)',
|
background: token.colorBgBase,
|
||||||
}}>
|
}}>
|
||||||
{/* 顶部标题栏 - 固定不滚动 */}
|
{/* 顶部标题栏 - 固定不滚动 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
background: 'var(--color-primary)',
|
background: token.colorPrimary,
|
||||||
boxShadow: 'var(--shadow-header)',
|
boxShadow: `0 6px 20px color-mix(in srgb, ${token.colorPrimary} 30%, transparent)`,
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
maxWidth: 1200,
|
maxWidth: 1200,
|
||||||
@@ -346,9 +347,9 @@ export default function ProjectWizardNew() {
|
|||||||
size={isMobile ? 'middle' : 'large'}
|
size={isMobile ? 'middle' : 'large'}
|
||||||
disabled={currentStep === 'generating'}
|
disabled={currentStep === 'generating'}
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(255,255,255,0.2)',
|
background: `color-mix(in srgb, ${token.colorWhite} 20%, transparent)`,
|
||||||
borderColor: 'rgba(255,255,255,0.3)',
|
borderColor: `color-mix(in srgb, ${token.colorWhite} 30%, transparent)`,
|
||||||
color: '#fff',
|
color: token.colorWhite,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isMobile ? '返回' : '返回首页'}
|
{isMobile ? '返回' : '返回首页'}
|
||||||
@@ -356,8 +357,8 @@ export default function ProjectWizardNew() {
|
|||||||
|
|
||||||
<Title level={isMobile ? 4 : 2} style={{
|
<Title level={isMobile ? 4 : 2} style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
color: '#fff',
|
color: token.colorWhite,
|
||||||
textShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
textShadow: '0 2px 4px color-mix(in srgb, var(--ant-color-black) 18%, transparent)',
|
||||||
}}>
|
}}>
|
||||||
<RocketOutlined style={{ marginRight: 8 }} />
|
<RocketOutlined style={{ marginRight: 8 }} />
|
||||||
项目创建向导
|
项目创建向导
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Upload,
|
Upload,
|
||||||
Spin,
|
Spin,
|
||||||
Empty
|
Empty,
|
||||||
|
theme
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
@@ -27,7 +28,7 @@ import {
|
|||||||
InfoCircleOutlined
|
InfoCircleOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { cardStyles, cardHoverHandlers, gridConfig } from '../components/CardStyles';
|
import { promptTemplateCardStyles, promptTemplateCardHoverHandlers, promptTemplateGridConfig } from '../components/CardStyles';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
const { Title, Text, Paragraph } = Typography;
|
const { Title, Text, Paragraph } = Typography;
|
||||||
@@ -54,6 +55,7 @@ interface CategoryGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function PromptTemplates() {
|
export default function PromptTemplates() {
|
||||||
|
const { token } = theme.useToken();
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
const [categories, setCategories] = useState<CategoryGroup[]>([]);
|
const [categories, setCategories] = useState<CategoryGroup[]>([]);
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string>('0');
|
const [selectedCategory, setSelectedCategory] = useState<string>('0');
|
||||||
@@ -246,13 +248,15 @@ export default function PromptTemplates() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const currentTemplates = getCurrentTemplates();
|
const currentTemplates = getCurrentTemplates();
|
||||||
|
const pageBackground = `linear-gradient(180deg, ${token.colorBgLayout} 0%, ${token.colorFillSecondary} 100%)`;
|
||||||
|
const headerBackground = `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<div style={{
|
<div style={{
|
||||||
minHeight: '90vh',
|
minHeight: '90vh',
|
||||||
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)',
|
background: pageBackground,
|
||||||
padding: isMobile ? '20px 16px 70px' : '24px 24px 70px',
|
padding: isMobile ? '20px 16px 70px' : '24px 24px 70px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -269,9 +273,9 @@ export default function PromptTemplates() {
|
|||||||
<Card
|
<Card
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, #5A9BA5 50%, var(--color-primary-hover) 100%)',
|
background: headerBackground,
|
||||||
borderRadius: isMobile ? 16 : 24,
|
borderRadius: isMobile ? 16 : 24,
|
||||||
boxShadow: '0 12px 40px rgba(77, 128, 136, 0.25), 0 4px 12px rgba(0, 0, 0, 0.06)',
|
boxShadow: token.boxShadowSecondary,
|
||||||
marginBottom: isMobile ? 20 : 24,
|
marginBottom: isMobile ? 20 : 24,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -279,18 +283,18 @@ export default function PromptTemplates() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 装饰性背景元素 */}
|
{/* 装饰性背景元素 */}
|
||||||
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.08)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: token.colorWhite, opacity: 0.08, pointerEvents: 'none' }} />
|
||||||
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.05)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: token.colorWhite, opacity: 0.05, pointerEvents: 'none' }} />
|
||||||
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.06)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: token.colorWhite, opacity: 0.06, pointerEvents: 'none' }} />
|
||||||
|
|
||||||
<Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
|
<Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
|
||||||
<Col xs={24} sm={12} md={14}>
|
<Col xs={24} sm={12} md={14}>
|
||||||
<Space direction="vertical" size={4}>
|
<Space direction="vertical" size={4}>
|
||||||
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: '#fff', textShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
|
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: token.colorWhite, textShadow: `0 2px 4px ${token.colorBgMask}` }}>
|
||||||
<FileSearchOutlined style={{ color: 'rgba(255,255,255,0.9)', marginRight: 8 }} />
|
<FileSearchOutlined style={{ color: token.colorWhite, opacity: 0.9, marginRight: 8 }} />
|
||||||
提示词模板管理
|
提示词模板管理
|
||||||
</Title>
|
</Title>
|
||||||
<Text style={{ fontSize: isMobile ? 12 : 14, color: 'rgba(255,255,255,0.85)', marginLeft: isMobile ? 40 : 48 }}>
|
<Text style={{ fontSize: isMobile ? 12 : 14, color: token.colorTextLightSolid, opacity: 0.85, marginLeft: isMobile ? 40 : 48 }}>
|
||||||
自定义AI生成提示词,打造个性化创作体验
|
自定义AI生成提示词,打造个性化创作体验
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -303,10 +307,11 @@ export default function PromptTemplates() {
|
|||||||
size={isMobile ? 'small' : 'middle'}
|
size={isMobile ? 'small' : 'middle'}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: 'rgba(255, 255, 255, 0.15)',
|
background: token.colorWhite,
|
||||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
border: `1px solid ${token.colorWhite}`,
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
boxShadow: token.boxShadow,
|
||||||
color: '#fff',
|
color: token.colorPrimary,
|
||||||
|
fontWeight: 600,
|
||||||
backdropFilter: 'blur(10px)',
|
backdropFilter: 'blur(10px)',
|
||||||
transition: 'all 0.3s ease'
|
transition: 'all 0.3s ease'
|
||||||
}}
|
}}
|
||||||
@@ -323,10 +328,11 @@ export default function PromptTemplates() {
|
|||||||
size={isMobile ? 'small' : 'middle'}
|
size={isMobile ? 'small' : 'middle'}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: 'rgba(255, 255, 255, 0.15)',
|
background: token.colorWhite,
|
||||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
border: `1px solid ${token.colorWhite}`,
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
boxShadow: token.boxShadow,
|
||||||
color: '#fff',
|
color: token.colorPrimary,
|
||||||
|
fontWeight: 600,
|
||||||
backdropFilter: 'blur(10px)',
|
backdropFilter: 'blur(10px)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -341,7 +347,7 @@ export default function PromptTemplates() {
|
|||||||
<Alert
|
<Alert
|
||||||
message={
|
message={
|
||||||
<Space align="center">
|
<Space align="center">
|
||||||
<InfoCircleOutlined style={{ fontSize: 16, color: 'var(--color-primary)' }} />
|
<InfoCircleOutlined style={{ fontSize: 16, color: token.colorPrimary }} />
|
||||||
<Text strong style={{ fontSize: isMobile ? 13 : 14 }}>使用说明</Text>
|
<Text strong style={{ fontSize: isMobile ? 13 : 14 }}>使用说明</Text>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -360,8 +366,8 @@ export default function PromptTemplates() {
|
|||||||
style={{
|
style={{
|
||||||
marginTop: isMobile ? 16 : 24,
|
marginTop: isMobile ? 16 : 24,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: 'var(--color-info-bg)',
|
background: token.colorInfoBg,
|
||||||
border: '1px solid var(--color-info-border)'
|
border: `1px solid ${token.colorInfoBorder}`
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -374,9 +380,9 @@ export default function PromptTemplates() {
|
|||||||
<Card
|
<Card
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(255, 255, 255, 0.95)',
|
background: token.colorBgContainer,
|
||||||
borderRadius: isMobile ? 12 : 16,
|
borderRadius: isMobile ? 12 : 16,
|
||||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
boxShadow: token.boxShadowSecondary,
|
||||||
marginBottom: isMobile ? 16 : 24
|
marginBottom: isMobile ? 16 : 24
|
||||||
}}
|
}}
|
||||||
styles={{ body: { padding: isMobile ? '12px' : '16px' } }}
|
styles={{ body: { padding: isMobile ? '12px' : '16px' } }}
|
||||||
@@ -400,9 +406,9 @@ export default function PromptTemplates() {
|
|||||||
<Card
|
<Card
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(255, 255, 255, 0.95)',
|
background: token.colorBgContainer,
|
||||||
borderRadius: isMobile ? 12 : 16,
|
borderRadius: isMobile ? 12 : 16,
|
||||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
boxShadow: token.boxShadowSecondary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Empty
|
<Empty
|
||||||
@@ -413,25 +419,25 @@ export default function PromptTemplates() {
|
|||||||
) : (
|
) : (
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
{currentTemplates.map(template => (
|
{currentTemplates.map(template => (
|
||||||
<Col {...gridConfig} key={template.id}>
|
<Col {...promptTemplateGridConfig} key={template.id}>
|
||||||
<Card
|
<Card
|
||||||
hoverable
|
hoverable
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
style={cardStyles.project}
|
style={promptTemplateCardStyles.templateCard}
|
||||||
styles={{ body: { padding: 0, overflow: 'hidden' } }}
|
styles={{ body: { padding: 0, overflow: 'hidden' } }}
|
||||||
{...cardHoverHandlers}
|
{...promptTemplateCardHoverHandlers}
|
||||||
>
|
>
|
||||||
{/* 头部 */}
|
{/* 头部 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
background: template.is_system_default
|
background: template.is_system_default
|
||||||
? 'var(--color-bg-layout)'
|
? token.colorFillTertiary
|
||||||
: 'var(--color-primary)',
|
: token.colorPrimary,
|
||||||
padding: isMobile ? '16px' : '20px',
|
padding: isMobile ? '16px' : '20px',
|
||||||
position: 'relative'
|
position: 'relative'
|
||||||
}}>
|
}}>
|
||||||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<Title level={isMobile ? 5 : 4} style={{ margin: 0, color: template.is_system_default ? 'var(--color-text-primary)' : '#fff', flex: 1 }} ellipsis>
|
<Title level={isMobile ? 5 : 4} style={{ margin: 0, color: template.is_system_default ? token.colorText : token.colorWhite, flex: 1 }} ellipsis>
|
||||||
{template.template_name}
|
{template.template_name}
|
||||||
</Title>
|
</Title>
|
||||||
{!template.is_system_default && (
|
{!template.is_system_default && (
|
||||||
@@ -444,10 +450,10 @@ export default function PromptTemplates() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Tag color={template.is_system_default ? 'default' : 'rgba(255,255,255,0.3)'} style={{ color: template.is_system_default ? 'var(--color-text-secondary)' : '#fff', border: 'none' }}>
|
<Tag color={template.is_system_default ? 'default' : 'rgba(255,255,255,0.3)'} style={{ color: template.is_system_default ? token.colorTextSecondary : token.colorWhite, border: 'none' }}>
|
||||||
{template.category}
|
{template.category}
|
||||||
</Tag>
|
</Tag>
|
||||||
<Tag color={template.is_system_default ? 'default' : 'rgba(255,255,255,0.3)'} style={{ color: template.is_system_default ? 'var(--color-text-secondary)' : '#fff', border: 'none' }}>
|
<Tag color={template.is_system_default ? 'default' : 'rgba(255,255,255,0.3)'} style={{ color: template.is_system_default ? token.colorTextSecondary : token.colorWhite, border: 'none' }}>
|
||||||
{template.is_system_default ? '系统默认' : '已自定义'}
|
{template.is_system_default ? '系统默认' : '已自定义'}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
Pagination,
|
Pagination,
|
||||||
Alert,
|
Alert,
|
||||||
Statistic,
|
Statistic,
|
||||||
|
theme,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
@@ -120,6 +121,7 @@ export default function PromptWorkshop() {
|
|||||||
const [activeTab, setActiveTab] = useState<string>('browse');
|
const [activeTab, setActiveTab] = useState<string>('browse');
|
||||||
|
|
||||||
const isMobile = window.innerWidth <= 768;
|
const isMobile = window.innerWidth <= 768;
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
// 判断是否为服务端管理员
|
// 判断是否为服务端管理员
|
||||||
const isServerAdmin = serviceStatus?.mode === 'server' && currentUser?.is_admin;
|
const isServerAdmin = serviceStatus?.mode === 'server' && currentUser?.is_admin;
|
||||||
@@ -431,7 +433,7 @@ export default function PromptWorkshop() {
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
border: '1px solid #f0f0f0',
|
border: `1px solid ${token.colorBorderSecondary}`,
|
||||||
}}
|
}}
|
||||||
bodyStyle={{
|
bodyStyle={{
|
||||||
padding: 16,
|
padding: 16,
|
||||||
@@ -446,7 +448,7 @@ export default function PromptWorkshop() {
|
|||||||
<Tooltip title={item.is_liked ? '取消点赞' : '点赞'} key="like">
|
<Tooltip title={item.is_liked ? '取消点赞' : '点赞'} key="like">
|
||||||
<span onClick={() => handleLike(item)}>
|
<span onClick={() => handleLike(item)}>
|
||||||
{item.is_liked ? (
|
{item.is_liked ? (
|
||||||
<HeartFilled style={{ color: '#ff4d4f' }} />
|
<HeartFilled style={{ color: token.colorError }} />
|
||||||
) : (
|
) : (
|
||||||
<HeartOutlined />
|
<HeartOutlined />
|
||||||
)}
|
)}
|
||||||
@@ -489,7 +491,7 @@ export default function PromptWorkshop() {
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
backgroundColor: '#fafafa',
|
backgroundColor: token.colorFillQuaternary,
|
||||||
padding: 8,
|
padding: 8,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -512,7 +514,7 @@ export default function PromptWorkshop() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: 8, color: '#999', fontSize: 12 }}>
|
<div style={{ marginTop: 8, color: token.colorTextTertiary, fontSize: 12 }}>
|
||||||
<Space>
|
<Space>
|
||||||
<span><UserOutlined /> {item.author_name || '匿名'}</span>
|
<span><UserOutlined /> {item.author_name || '匿名'}</span>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -569,7 +571,7 @@ export default function PromptWorkshop() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
style={{ borderRadius: 12, height: '100%', border: '1px solid #f0f0f0' }}
|
style={{ borderRadius: 12, height: '100%', border: `1px solid ${token.colorBorderSecondary}` }}
|
||||||
bodyStyle={{ padding: 16 }}
|
bodyStyle={{ padding: 16 }}
|
||||||
>
|
>
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
@@ -599,7 +601,7 @@ export default function PromptWorkshop() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ fontSize: 12, color: '#999' }}>
|
<div style={{ fontSize: 12, color: token.colorTextTertiary }}>
|
||||||
提交时间: {sub.created_at ? new Date(sub.created_at).toLocaleDateString() : '-'}
|
提交时间: {sub.created_at ? new Date(sub.created_at).toLocaleDateString() : '-'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -802,7 +804,7 @@ export default function PromptWorkshop() {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col span={4}>
|
<Col span={4}>
|
||||||
<Card size="small">
|
<Card size="small">
|
||||||
<Statistic title="待审核" value={adminStats.total_pending} valueStyle={{ color: '#faad14' }} />
|
<Statistic title="待审核" value={adminStats.total_pending} valueStyle={{ color: token.colorWarning }} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={4}>
|
<Col span={4}>
|
||||||
@@ -853,13 +855,13 @@ export default function PromptWorkshop() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
style={{ borderRadius: 12, border: '1px solid #f0f0f0' }}
|
style={{ borderRadius: 12, border: `1px solid ${token.colorBorderSecondary}` }}
|
||||||
bodyStyle={{ padding: 16 }}
|
bodyStyle={{ padding: 16 }}
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Button
|
||||||
key="approve"
|
key="approve"
|
||||||
type="link"
|
type="link"
|
||||||
style={{ color: '#52c41a' }}
|
style={{ color: token.colorSuccess }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setReviewingSubmission(sub);
|
setReviewingSubmission(sub);
|
||||||
reviewForm.setFieldsValue({
|
reviewForm.setFieldsValue({
|
||||||
@@ -887,7 +889,7 @@ export default function PromptWorkshop() {
|
|||||||
{sub.prompt_content}
|
{sub.prompt_content}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
||||||
<div style={{ fontSize: 11, color: '#999' }}>
|
<div style={{ fontSize: 11, color: token.colorTextTertiary }}>
|
||||||
<div>提交者: {sub.submitter_name || '未知'}</div>
|
<div>提交者: {sub.submitter_name || '未知'}</div>
|
||||||
<div>来源: {sub.source_instance}</div>
|
<div>来源: {sub.source_instance}</div>
|
||||||
<div>时间: {sub.created_at ? new Date(sub.created_at).toLocaleDateString() : '-'}</div>
|
<div>时间: {sub.created_at ? new Date(sub.created_at).toLocaleDateString() : '-'}</div>
|
||||||
@@ -928,7 +930,7 @@ export default function PromptWorkshop() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
style={{ borderRadius: 12, border: '1px solid #f0f0f0' }}
|
style={{ borderRadius: 12, border: `1px solid ${token.colorBorderSecondary}` }}
|
||||||
bodyStyle={{ padding: 16 }}
|
bodyStyle={{ padding: 16 }}
|
||||||
actions={[
|
actions={[
|
||||||
<Tooltip title="编辑" key="edit">
|
<Tooltip title="编辑" key="edit">
|
||||||
@@ -965,7 +967,7 @@ export default function PromptWorkshop() {
|
|||||||
{item.prompt_content}
|
{item.prompt_content}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
||||||
<div style={{ fontSize: 11, color: '#999' }}>
|
<div style={{ fontSize: 11, color: token.colorTextTertiary }}>
|
||||||
<Space>
|
<Space>
|
||||||
<span><HeartOutlined /> {item.like_count || 0}</span>
|
<span><HeartOutlined /> {item.like_count || 0}</span>
|
||||||
<span><DownloadOutlined /> {item.download_count || 0}</span>
|
<span><DownloadOutlined /> {item.download_count || 0}</span>
|
||||||
@@ -992,7 +994,7 @@ export default function PromptWorkshop() {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: isMobile ? '12px 0' : '16px 0',
|
padding: isMobile ? '12px 0' : '16px 0',
|
||||||
marginBottom: isMobile ? 12 : 16,
|
marginBottom: isMobile ? 12 : 16,
|
||||||
borderBottom: '1px solid #f0f0f0',
|
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: 12,
|
gap: 12,
|
||||||
}}>
|
}}>
|
||||||
@@ -1185,7 +1187,7 @@ export default function PromptWorkshop() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: token.colorFillSecondary,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
@@ -1236,7 +1238,7 @@ export default function PromptWorkshop() {
|
|||||||
{reviewingSubmission && (
|
{reviewingSubmission && (
|
||||||
<div>
|
<div>
|
||||||
<div style={{
|
<div style={{
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: token.colorFillSecondary,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo, type CSSProperties } from 'react';
|
import { useState, useEffect, useCallback, useMemo, type CSSProperties } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Card, Tag, Button, Space, message, Typography } from 'antd';
|
import { Card, Tag, Button, Space, message, Typography, theme } from 'antd';
|
||||||
import {
|
import {
|
||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined,
|
||||||
ApartmentOutlined,
|
ApartmentOutlined,
|
||||||
@@ -100,16 +100,18 @@ interface CharacterListResponse {
|
|||||||
const GROUP_MAIN_CAREER_NODE_ID = '__career_group_main__';
|
const GROUP_MAIN_CAREER_NODE_ID = '__career_group_main__';
|
||||||
const GROUP_SUB_CAREER_NODE_ID = '__career_group_sub__';
|
const GROUP_SUB_CAREER_NODE_ID = '__career_group_sub__';
|
||||||
|
|
||||||
const EDGE_CATEGORY_META: Record<string, { label: string; color: string; order: number }> = {
|
type EdgeColorPreset = 'primary' | 'warning' | 'info' | 'textTertiary' | 'error' | 'success';
|
||||||
organization: { label: '组织成员', color: '#722ed1', order: 1 },
|
|
||||||
career_main: { label: '主职业关联', color: '#faad14', order: 2 },
|
const EDGE_CATEGORY_META: Record<string, { label: string; colorPreset: EdgeColorPreset; order: number }> = {
|
||||||
career_sub: { label: '副职业关联', color: '#13c2c2', order: 3 },
|
organization: { label: '组织成员', colorPreset: 'primary', order: 1 },
|
||||||
career_group: { label: '职业分类', color: '#8c8c8c', order: 4 },
|
career_main: { label: '主职业关联', colorPreset: 'warning', order: 2 },
|
||||||
family: { label: '亲属关系', color: '#f39c12', order: 5 },
|
career_sub: { label: '副职业关联', colorPreset: 'info', order: 3 },
|
||||||
hostile: { label: '敌对关系', color: '#e74c3c', order: 6 },
|
career_group: { label: '职业分类', colorPreset: 'textTertiary', order: 4 },
|
||||||
professional: { label: '职业关系', color: '#3498db', order: 7 },
|
family: { label: '亲属关系', colorPreset: 'warning', order: 5 },
|
||||||
social: { label: '社交关系', color: '#27ae60', order: 8 },
|
hostile: { label: '敌对关系', colorPreset: 'error', order: 6 },
|
||||||
default: { label: '其他关系', color: '#95a5a6', order: 99 },
|
professional: { label: '职业关系', colorPreset: 'info', order: 7 },
|
||||||
|
social: { label: '社交关系', colorPreset: 'success', order: 8 },
|
||||||
|
default: { label: '其他关系', colorPreset: 'textTertiary', order: 99 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEdgeCategory = (edge: Edge) =>
|
const getEdgeCategory = (edge: Edge) =>
|
||||||
@@ -118,13 +120,36 @@ const getEdgeCategory = (edge: Edge) =>
|
|||||||
const getEdgeCategoryMeta = (category: string) =>
|
const getEdgeCategoryMeta = (category: string) =>
|
||||||
EDGE_CATEGORY_META[category] || {
|
EDGE_CATEGORY_META[category] || {
|
||||||
label: `${category}关系`,
|
label: `${category}关系`,
|
||||||
color: '#95a5a6',
|
colorPreset: 'textTertiary' as const,
|
||||||
order: 999,
|
order: 999,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resolveEdgePresetColor = (
|
||||||
|
colorPreset: EdgeColorPreset,
|
||||||
|
token: {
|
||||||
|
colorPrimary: string;
|
||||||
|
colorWarning: string;
|
||||||
|
colorInfo: string;
|
||||||
|
colorTextTertiary: string;
|
||||||
|
colorError: string;
|
||||||
|
colorSuccess: string;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const colorMap: Record<EdgeColorPreset, string> = {
|
||||||
|
primary: token.colorPrimary,
|
||||||
|
warning: token.colorWarning,
|
||||||
|
info: token.colorInfo,
|
||||||
|
textTertiary: token.colorTextTertiary,
|
||||||
|
error: token.colorError,
|
||||||
|
success: token.colorSuccess,
|
||||||
|
};
|
||||||
|
|
||||||
|
return colorMap[colorPreset];
|
||||||
|
};
|
||||||
|
|
||||||
const clampTextStyle = (rows: number): CSSProperties => ({
|
const clampTextStyle = (rows: number): CSSProperties => ({
|
||||||
margin: '4px 0 0',
|
margin: '4px 0 0',
|
||||||
color: '#555',
|
color: 'var(--ant-color-text-secondary)',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
lineHeight: '22px',
|
lineHeight: '22px',
|
||||||
display: '-webkit-box',
|
display: '-webkit-box',
|
||||||
@@ -386,54 +411,65 @@ const getCategoryColor = (
|
|||||||
relationshipName: string,
|
relationshipName: string,
|
||||||
isActive: boolean,
|
isActive: boolean,
|
||||||
relationshipTypes: RelationshipType[],
|
relationshipTypes: RelationshipType[],
|
||||||
|
token: {
|
||||||
|
colorPrimary: string;
|
||||||
|
colorWarning: string;
|
||||||
|
colorInfo: string;
|
||||||
|
colorTextTertiary: string;
|
||||||
|
colorError: string;
|
||||||
|
colorSuccess: string;
|
||||||
|
colorBorder: string;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
|
const inactiveColor = token.colorBorder;
|
||||||
|
|
||||||
if (relationshipName.startsWith('组织成员·')) {
|
if (relationshipName.startsWith('组织成员·')) {
|
||||||
return isActive ? '#722ed1' : '#cdb7f6';
|
return isActive ? token.colorPrimary : inactiveColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relationshipName.startsWith('主职业·')) {
|
if (relationshipName.startsWith('主职业·')) {
|
||||||
return isActive ? '#faad14' : '#ffe7ba';
|
return isActive ? token.colorWarning : inactiveColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relationshipName.startsWith('副职业·')) {
|
if (relationshipName.startsWith('副职业·')) {
|
||||||
return isActive ? '#13c2c2' : '#b5f5ec';
|
return isActive ? token.colorInfo : inactiveColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relationshipName.startsWith('职业分类·')) {
|
if (relationshipName.startsWith('职业分类·')) {
|
||||||
return isActive ? '#8c8c8c' : '#d9d9d9';
|
return isActive ? token.colorTextTertiary : inactiveColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
const relType = relationshipTypes.find((rt) => rt.name === relationshipName);
|
const relType = relationshipTypes.find((rt) => rt.name === relationshipName);
|
||||||
const category = relType?.category || 'default';
|
const category = relType?.category || 'default';
|
||||||
|
|
||||||
const categoryColors: Record<string, { active: string; inactive: string }> = {
|
const categoryColors: Record<string, string> = {
|
||||||
family: { active: '#f39c12', inactive: '#fcd59e' },
|
family: token.colorWarning,
|
||||||
hostile: { active: '#e74c3c', inactive: '#f5a49a' },
|
hostile: token.colorError,
|
||||||
professional: { active: '#3498db', inactive: '#a9d4ed' },
|
professional: token.colorInfo,
|
||||||
social: { active: '#27ae60', inactive: '#a3d9b5' },
|
social: token.colorSuccess,
|
||||||
default: { active: '#95a5a6', inactive: '#c8d0d2' },
|
default: token.colorTextTertiary,
|
||||||
};
|
};
|
||||||
|
|
||||||
const colors = categoryColors[category] || categoryColors.default;
|
const activeColor = categoryColors[category] || categoryColors.default;
|
||||||
return isActive ? colors.active : colors.inactive;
|
return isActive ? activeColor : inactiveColor;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCharacterNodeStyle = (roleType: string): CSSProperties => {
|
const getCharacterNodeStyle = (roleType: string): CSSProperties => {
|
||||||
const roleColorMap: Record<string, string> = {
|
const roleColorMap: Record<string, string> = {
|
||||||
protagonist: '#e74c3c',
|
protagonist: 'var(--ant-color-error)',
|
||||||
antagonist: '#9b59b6',
|
antagonist: 'var(--ant-color-primary)',
|
||||||
supporting: '#3498db',
|
supporting: 'var(--ant-color-info)',
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseColor = roleColorMap[roleType] || '#3498db';
|
const baseColor = roleColorMap[roleType] || 'var(--ant-color-info)';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width: 130,
|
width: 130,
|
||||||
height: 130,
|
height: 130,
|
||||||
border: `2px solid ${baseColor}`,
|
border: `2px solid ${baseColor}`,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: `linear-gradient(135deg, #ffffff, ${baseColor}15)`,
|
background: `linear-gradient(135deg, var(--ant-color-bg-container), color-mix(in srgb, ${baseColor} 12%, var(--ant-color-bg-container)))`,
|
||||||
boxShadow: `0 4px 16px ${baseColor}25`,
|
boxShadow: `0 4px 16px color-mix(in srgb, ${baseColor} 25%, transparent)`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -445,10 +481,10 @@ const getCharacterNodeStyle = (roleType: string): CSSProperties => {
|
|||||||
const getOrganizationNodeStyle = (): CSSProperties => ({
|
const getOrganizationNodeStyle = (): CSSProperties => ({
|
||||||
width: 160,
|
width: 160,
|
||||||
height: 90,
|
height: 90,
|
||||||
border: '2px solid #27ae60',
|
border: '2px solid var(--ant-color-success)',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: 'linear-gradient(135deg, #ffffff, #27ae6015)',
|
background: 'linear-gradient(135deg, var(--ant-color-bg-container), color-mix(in srgb, var(--ant-color-success) 12%, var(--ant-color-bg-container)))',
|
||||||
boxShadow: '0 4px 16px rgba(39, 174, 96, 0.15)',
|
boxShadow: '0 4px 16px color-mix(in srgb, var(--ant-color-success) 15%, transparent)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -457,15 +493,15 @@ const getOrganizationNodeStyle = (): CSSProperties => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getCareerNodeStyle = (type: 'main' | 'sub'): CSSProperties => {
|
const getCareerNodeStyle = (type: 'main' | 'sub'): CSSProperties => {
|
||||||
const color = type === 'main' ? '#faad14' : '#13c2c2';
|
const color = type === 'main' ? 'var(--ant-color-warning)' : 'var(--ant-color-info)';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width: 150,
|
width: 150,
|
||||||
height: 72,
|
height: 72,
|
||||||
border: `2px solid ${color}`,
|
border: `2px solid ${color}`,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: `linear-gradient(135deg, #ffffff, ${color}15)`,
|
background: `linear-gradient(135deg, var(--ant-color-bg-container), color-mix(in srgb, ${color} 12%, var(--ant-color-bg-container)))`,
|
||||||
boxShadow: `0 4px 12px ${color}20`,
|
boxShadow: `0 4px 12px color-mix(in srgb, ${color} 20%, transparent)`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -475,15 +511,15 @@ const getCareerNodeStyle = (type: 'main' | 'sub'): CSSProperties => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getCareerGroupStyle = (type: 'main' | 'sub'): CSSProperties => {
|
const getCareerGroupStyle = (type: 'main' | 'sub'): CSSProperties => {
|
||||||
const color = type === 'main' ? '#d48806' : '#08979c';
|
const color = type === 'main' ? 'var(--ant-color-warning)' : 'var(--ant-color-info)';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width: 130,
|
width: 130,
|
||||||
height: 52,
|
height: 52,
|
||||||
border: `2px dashed ${color}`,
|
border: `2px dashed ${color}`,
|
||||||
borderRadius: 26,
|
borderRadius: 26,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: 'var(--ant-color-bg-container)',
|
||||||
boxShadow: `0 2px 8px ${color}15`,
|
boxShadow: `0 2px 8px color-mix(in srgb, ${color} 15%, transparent)`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -511,12 +547,12 @@ const InfoField = ({
|
|||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
padding: '12px 14px',
|
padding: '12px 14px',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: '#f8f9fa',
|
background: 'var(--ant-color-fill-quaternary)',
|
||||||
border: '1px solid #eef0f2',
|
border: '1px solid var(--ant-color-border-secondary)',
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.02)',
|
boxShadow: '0 2px 4px color-mix(in srgb, var(--ant-color-text) 6%, transparent)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text strong style={{ fontSize: 14, color: '#333' }}>
|
<Text strong style={{ fontSize: 14, color: 'var(--ant-color-text)' }}>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
<div style={clampTextStyle(rows)}>{value}</div>
|
<div style={clampTextStyle(rows)}>{value}</div>
|
||||||
@@ -527,6 +563,9 @@ const InfoField = ({
|
|||||||
export default function RelationshipGraph() {
|
export default function RelationshipGraph() {
|
||||||
const { projectId } = useParams<{ projectId: string }>();
|
const { projectId } = useParams<{ projectId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const alphaColor = (color: string, alpha: number) =>
|
||||||
|
`color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||||
|
|
||||||
const [graphData, setGraphData] = useState<GraphData | null>(null);
|
const [graphData, setGraphData] = useState<GraphData | null>(null);
|
||||||
const [, setLoading] = useState(false);
|
const [, setLoading] = useState(false);
|
||||||
@@ -564,11 +603,13 @@ export default function RelationshipGraph() {
|
|||||||
return {
|
return {
|
||||||
category,
|
category,
|
||||||
count,
|
count,
|
||||||
...meta,
|
label: meta.label,
|
||||||
|
color: resolveEdgePresetColor(meta.colorPreset, token),
|
||||||
|
order: meta.order,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.order - b.order || a.label.localeCompare(b.label, 'zh-CN'));
|
.sort((a, b) => a.order - b.order || a.label.localeCompare(b.label, 'zh-CN'));
|
||||||
}, [edges]);
|
}, [edges, token]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (edgeCategoryOptions.length === 0) {
|
if (edgeCategoryOptions.length === 0) {
|
||||||
@@ -625,7 +666,15 @@ export default function RelationshipGraph() {
|
|||||||
layoutWeight?: number;
|
layoutWeight?: number;
|
||||||
},
|
},
|
||||||
): Edge => {
|
): Edge => {
|
||||||
const edgeColor = getCategoryColor(relationship, status === 'active', relationshipTypes);
|
const edgeColor = getCategoryColor(relationship, status === 'active', relationshipTypes, {
|
||||||
|
colorPrimary: token.colorPrimary,
|
||||||
|
colorWarning: token.colorWarning,
|
||||||
|
colorInfo: token.colorInfo,
|
||||||
|
colorTextTertiary: token.colorTextTertiary,
|
||||||
|
colorError: token.colorError,
|
||||||
|
colorSuccess: token.colorSuccess,
|
||||||
|
colorBorder: token.colorBorder,
|
||||||
|
});
|
||||||
const isOrgMemberLink = relationship.startsWith('组织成员·');
|
const isOrgMemberLink = relationship.startsWith('组织成员·');
|
||||||
const isCareerMainLink = relationship.startsWith('主职业·');
|
const isCareerMainLink = relationship.startsWith('主职业·');
|
||||||
const isCareerSubLink = relationship.startsWith('副职业·');
|
const isCareerSubLink = relationship.startsWith('副职业·');
|
||||||
@@ -645,12 +694,12 @@ export default function RelationshipGraph() {
|
|||||||
opacity: isCareerClassLink ? 0.5 : (isCareerMainLink || isCareerSubLink ? 0.6 : 1),
|
opacity: isCareerClassLink ? 0.5 : (isCareerMainLink || isCareerSubLink ? 0.6 : 1),
|
||||||
},
|
},
|
||||||
labelStyle: {
|
labelStyle: {
|
||||||
fill: '#666',
|
fill: token.colorTextSecondary,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: isCareerMainLink || isCareerSubLink ? 600 : 500,
|
fontWeight: isCareerMainLink || isCareerSubLink ? 600 : 500,
|
||||||
},
|
},
|
||||||
labelBgStyle: {
|
labelBgStyle: {
|
||||||
fill: '#fff',
|
fill: token.colorBgContainer,
|
||||||
fillOpacity: 0.9,
|
fillOpacity: 0.9,
|
||||||
},
|
},
|
||||||
markerEnd: {
|
markerEnd: {
|
||||||
@@ -673,7 +722,18 @@ export default function RelationshipGraph() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[relationshipTypes],
|
[
|
||||||
|
relationshipTypes,
|
||||||
|
token.colorBgContainer,
|
||||||
|
token.colorBorder,
|
||||||
|
token.colorError,
|
||||||
|
token.colorInfo,
|
||||||
|
token.colorPrimary,
|
||||||
|
token.colorSuccess,
|
||||||
|
token.colorTextSecondary,
|
||||||
|
token.colorTextTertiary,
|
||||||
|
token.colorWarning,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadGraphData = useCallback(async () => {
|
const loadGraphData = useCallback(async () => {
|
||||||
@@ -705,28 +765,28 @@ export default function RelationshipGraph() {
|
|||||||
const detail = detailMap[node.id];
|
const detail = detailMap[node.id];
|
||||||
|
|
||||||
const roleColorMap: Record<string, string> = {
|
const roleColorMap: Record<string, string> = {
|
||||||
protagonist: '#e74c3c',
|
protagonist: token.colorError,
|
||||||
antagonist: '#9b59b6',
|
antagonist: token.colorPrimary,
|
||||||
supporting: '#3498db',
|
supporting: token.colorInfo,
|
||||||
};
|
};
|
||||||
const baseColor = roleColorMap[node.role_type] || '#3498db';
|
const baseColor = roleColorMap[node.role_type] || token.colorInfo;
|
||||||
|
|
||||||
const labelContent = node.type === 'organization' ? (
|
const labelContent = node.type === 'organization' ? (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
|
||||||
<ApartmentOutlined style={{ fontSize: 24, color: '#27ae60', marginBottom: 4 }} />
|
<ApartmentOutlined style={{ fontSize: 24, color: token.colorSuccess, marginBottom: 4 }} />
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, color: '#333', maxWidth: '90%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{node.name}</div>
|
<div style={{ fontWeight: 600, fontSize: 14, color: token.colorText, maxWidth: '90%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{node.name}</div>
|
||||||
<div style={{ fontSize: 11, color: '#666', marginTop: 2 }}>{detail?.organization_type || '组织'}</div>
|
<div style={{ fontSize: 11, color: token.colorTextSecondary, marginTop: 2 }}>{detail?.organization_type || '组织'}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
|
||||||
{detail?.avatar_url ? (
|
{detail?.avatar_url ? (
|
||||||
<img src={detail.avatar_url} alt={node.name} style={{ width: 56, height: 56, borderRadius: '50%', objectFit: 'cover', border: '2px solid #fff', boxShadow: '0 2px 6px rgba(0,0,0,0.1)', marginBottom: 6 }} />
|
<img src={detail.avatar_url} alt={node.name} style={{ width: 56, height: 56, borderRadius: '50%', objectFit: 'cover', border: `2px solid ${token.colorBgContainer}`, boxShadow: `0 2px 6px ${alphaColor(token.colorTextBase, 0.18)}`, marginBottom: 6 }} />
|
||||||
) : (
|
) : (
|
||||||
<div style={{ width: 56, height: 56, borderRadius: '50%', backgroundColor: '#f0f2f5', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '2px solid #fff', boxShadow: '0 2px 6px rgba(0,0,0,0.1)', marginBottom: 6 }}>
|
<div style={{ width: 56, height: 56, borderRadius: '50%', backgroundColor: token.colorFillSecondary, display: 'flex', alignItems: 'center', justifyContent: 'center', border: `2px solid ${token.colorBgContainer}`, boxShadow: `0 2px 6px ${alphaColor(token.colorTextBase, 0.18)}`, marginBottom: 6 }}>
|
||||||
<UserOutlined style={{ fontSize: 28, color: baseColor }} />
|
<UserOutlined style={{ fontSize: 28, color: baseColor }} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{ fontWeight: 600, fontSize: 13, color: '#333', maxWidth: '90%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{node.name}</div>
|
<div style={{ fontWeight: 600, fontSize: 13, color: token.colorText, maxWidth: '90%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{node.name}</div>
|
||||||
<div style={{ fontSize: 11, color: baseColor, marginTop: 2, transform: 'scale(0.9)' }}>
|
<div style={{ fontSize: 11, color: baseColor, marginTop: 2, transform: 'scale(0.9)' }}>
|
||||||
{node.role_type === 'protagonist' ? '主角' : node.role_type === 'antagonist' ? '反派' : '配角'}
|
{node.role_type === 'protagonist' ? '主角' : node.role_type === 'antagonist' ? '反派' : '配角'}
|
||||||
</div>
|
</div>
|
||||||
@@ -753,8 +813,8 @@ export default function RelationshipGraph() {
|
|||||||
data: {
|
data: {
|
||||||
label: (
|
label: (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<div style={{ fontSize: 11, color: '#d48806', marginBottom: 2 }}>主职业</div>
|
<div style={{ fontSize: 11, color: token.colorWarning, marginBottom: 2 }}>主职业</div>
|
||||||
<div style={{ fontWeight: 600, fontSize: 13, color: '#333' }}>{career.name}</div>
|
<div style={{ fontWeight: 600, fontSize: 13, color: token.colorText }}>{career.name}</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
type: 'career_main',
|
type: 'career_main',
|
||||||
@@ -769,8 +829,8 @@ export default function RelationshipGraph() {
|
|||||||
data: {
|
data: {
|
||||||
label: (
|
label: (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<div style={{ fontSize: 11, color: '#08979c', marginBottom: 2 }}>副职业</div>
|
<div style={{ fontSize: 11, color: token.colorInfo, marginBottom: 2 }}>副职业</div>
|
||||||
<div style={{ fontWeight: 600, fontSize: 13, color: '#333' }}>{career.name}</div>
|
<div style={{ fontWeight: 600, fontSize: 13, color: token.colorText }}>{career.name}</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
type: 'career_sub',
|
type: 'career_sub',
|
||||||
@@ -922,7 +982,23 @@ export default function RelationshipGraph() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [projectId, relationshipTypes, buildFlowEdge, setNodes, setEdges]);
|
}, [
|
||||||
|
projectId,
|
||||||
|
relationshipTypes,
|
||||||
|
buildFlowEdge,
|
||||||
|
setNodes,
|
||||||
|
setEdges,
|
||||||
|
token.colorBgContainer,
|
||||||
|
token.colorError,
|
||||||
|
token.colorFillSecondary,
|
||||||
|
token.colorInfo,
|
||||||
|
token.colorPrimary,
|
||||||
|
token.colorSuccess,
|
||||||
|
token.colorText,
|
||||||
|
token.colorTextBase,
|
||||||
|
token.colorTextSecondary,
|
||||||
|
token.colorWarning,
|
||||||
|
]);
|
||||||
|
|
||||||
// 当 relationshipTypes 加载完成后再加载图数据
|
// 当 relationshipTypes 加载完成后再加载图数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1000,27 +1076,27 @@ export default function RelationshipGraph() {
|
|||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
padding: '12px 14px',
|
padding: '12px 14px',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: '#f8f9fa',
|
background: token.colorFillQuaternary,
|
||||||
border: '1px solid #eef0f2',
|
border: `1px solid ${token.colorBorderSecondary}`,
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.02)',
|
boxShadow: `0 2px 4px ${alphaColor(token.colorTextBase, 0.06)}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text strong style={{ fontSize: 14, color: '#333' }}>
|
<Text strong style={{ fontSize: 14, color: token.colorText }}>
|
||||||
职业体系
|
职业体系
|
||||||
</Text>
|
</Text>
|
||||||
<div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
<div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
{nodeDetail.main_career_id ? (
|
{nodeDetail.main_career_id ? (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<Tag color="gold" style={{ margin: 0, borderRadius: 12, padding: '0 10px', fontWeight: 500 }}>主职业</Tag>
|
<Tag color="gold" style={{ margin: 0, borderRadius: 12, padding: '0 10px', fontWeight: 500 }}>主职业</Tag>
|
||||||
<span style={{ fontSize: 14, color: '#444' }}>
|
<span style={{ fontSize: 14, color: token.colorText }}>
|
||||||
{careerNameMap[nodeDetail.main_career_id]?.name || nodeDetail.main_career_id}
|
{careerNameMap[nodeDetail.main_career_id]?.name || nodeDetail.main_career_id}
|
||||||
{nodeDetail.main_career_stage ? <span style={{ color: '#888', marginLeft: 4 }}>第{nodeDetail.main_career_stage}阶</span> : ''}
|
{nodeDetail.main_career_stage ? <span style={{ color: token.colorTextTertiary, marginLeft: 4 }}>第{nodeDetail.main_career_stage}阶</span> : ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<Tag style={{ margin: 0, borderRadius: 12, padding: '0 10px' }}>主职业</Tag>
|
<Tag style={{ margin: 0, borderRadius: 12, padding: '0 10px' }}>主职业</Tag>
|
||||||
<span style={{ fontSize: 14, color: '#888' }}>未设置</span>
|
<span style={{ fontSize: 14, color: token.colorTextTertiary }}>未设置</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1029,9 +1105,9 @@ export default function RelationshipGraph() {
|
|||||||
<Tag color="cyan" style={{ margin: 0, borderRadius: 12, padding: '0 10px', fontWeight: 500 }}>副职业</Tag>
|
<Tag color="cyan" style={{ margin: 0, borderRadius: 12, padding: '0 10px', fontWeight: 500 }}>副职业</Tag>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, flex: 1 }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, flex: 1 }}>
|
||||||
{subCareerData.map((sub, index) => (
|
{subCareerData.map((sub, index) => (
|
||||||
<span key={`${sub.career_id}-${index}`} style={{ fontSize: 14, color: '#444', background: '#fff', border: '1px solid #e8e8e8', borderRadius: 4, padding: '0 6px' }}>
|
<span key={`${sub.career_id}-${index}`} style={{ fontSize: 14, color: token.colorText, background: token.colorBgContainer, border: `1px solid ${token.colorBorderSecondary}`, borderRadius: token.borderRadiusSM, padding: '0 6px' }}>
|
||||||
{careerNameMap[sub.career_id]?.name || sub.career_id}
|
{careerNameMap[sub.career_id]?.name || sub.career_id}
|
||||||
{sub.stage ? <span style={{ color: '#888', marginLeft: 4 }}>阶{sub.stage}</span> : ''}
|
{sub.stage ? <span style={{ color: token.colorTextTertiary, marginLeft: 4 }}>阶{sub.stage}</span> : ''}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1039,7 +1115,7 @@ export default function RelationshipGraph() {
|
|||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<Tag style={{ margin: 0, borderRadius: 12, padding: '0 10px' }}>副职业</Tag>
|
<Tag style={{ margin: 0, borderRadius: 12, padding: '0 10px' }}>副职业</Tag>
|
||||||
<span style={{ fontSize: 14, color: '#888' }}>未设置</span>
|
<span style={{ fontSize: 14, color: token.colorTextTertiary }}>未设置</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1057,7 +1133,7 @@ export default function RelationshipGraph() {
|
|||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: token.colorBgLayout,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1093,35 +1169,35 @@ export default function RelationshipGraph() {
|
|||||||
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 14, fontSize: 12, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 14, fontSize: 12, flexWrap: 'wrap' }}>
|
||||||
{/* 节点图例 */}
|
{/* 节点图例 */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
<span style={{ color: '#3498db', fontWeight: 'bold' }}>●</span>
|
<span style={{ color: token.colorInfo, fontWeight: 'bold' }}>●</span>
|
||||||
<span>角色(圆形)</span>
|
<span>角色(圆形)</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
<span style={{ color: '#27ae60', fontWeight: 'bold' }}>■</span>
|
<span style={{ color: token.colorSuccess, fontWeight: 'bold' }}>■</span>
|
||||||
<span>组织(方形)</span>
|
<span>组织(方形)</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
<span style={{ color: '#faad14', fontWeight: 'bold' }}>▭</span>
|
<span style={{ color: token.colorWarning, fontWeight: 'bold' }}>▭</span>
|
||||||
<span>主职业</span>
|
<span>主职业</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
<span style={{ color: '#13c2c2', fontWeight: 'bold' }}>▭</span>
|
<span style={{ color: token.colorInfo, fontWeight: 'bold' }}>▭</span>
|
||||||
<span>副职业</span>
|
<span>副职业</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span style={{ color: '#d9d9d9' }}>|</span>
|
<span style={{ color: token.colorBorder }}>|</span>
|
||||||
|
|
||||||
{/* 连线图例 */}
|
{/* 连线图例 */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
<span style={{ color: '#722ed1', fontWeight: 'bold' }}>- -</span>
|
<span style={{ color: token.colorPrimary, fontWeight: 'bold' }}>- -</span>
|
||||||
<span>组织成员</span>
|
<span>组织成员</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
<span style={{ color: '#faad14', fontWeight: 'bold' }}>—</span>
|
<span style={{ color: token.colorWarning, fontWeight: 'bold' }}>—</span>
|
||||||
<span>主职业关联</span>
|
<span>主职业关联</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
<span style={{ color: '#13c2c2', fontWeight: 'bold' }}>- -</span>
|
<span style={{ color: token.colorInfo, fontWeight: 'bold' }}>- -</span>
|
||||||
<span>副职业关联</span>
|
<span>副职业关联</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1141,8 +1217,8 @@ export default function RelationshipGraph() {
|
|||||||
onClick={() => toggleEdgeCategoryVisibility(option.category)}
|
onClick={() => toggleEdgeCategoryVisibility(option.category)}
|
||||||
style={
|
style={
|
||||||
isVisible
|
isVisible
|
||||||
? { backgroundColor: option.color, borderColor: option.color, color: '#fff' }
|
? { backgroundColor: option.color, borderColor: option.color, color: token.colorWhite }
|
||||||
: { color: '#666' }
|
: { color: token.colorTextSecondary }
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{option.label}({option.count})
|
{option.label}({option.count})
|
||||||
@@ -1154,7 +1230,68 @@ export default function RelationshipGraph() {
|
|||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div style={{ flex: 1, minHeight: 0 }}>
|
<div style={{ flex: 1, minHeight: 0 }} className="relationship-graph-flow">
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
.relationship-graph-flow .react-flow__handle {
|
||||||
|
opacity: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-graph-flow .react-flow__node {
|
||||||
|
outline: 1px solid var(--ant-color-border-secondary);
|
||||||
|
outline-offset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-graph-flow .react-flow__controls {
|
||||||
|
border: 1px solid var(--ant-color-border-secondary);
|
||||||
|
border-radius: var(--ant-border-radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--ant-color-bg-elevated);
|
||||||
|
box-shadow: 0 6px 16px color-mix(in srgb, var(--ant-color-text) 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-graph-flow .react-flow__controls-button {
|
||||||
|
background: var(--ant-color-bg-elevated);
|
||||||
|
border-bottom: 1px solid var(--ant-color-border-secondary);
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-graph-flow .react-flow__controls-button:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-graph-flow .react-flow__controls-button:hover {
|
||||||
|
background: var(--ant-color-fill-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-graph-flow .react-flow__controls-button:disabled {
|
||||||
|
background: var(--ant-color-fill-quaternary);
|
||||||
|
color: var(--ant-color-text-quaternary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-graph-flow .react-flow__controls-button svg {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-graph-flow .react-flow__attribution {
|
||||||
|
background: var(--ant-color-bg-elevated);
|
||||||
|
border: 1px solid var(--ant-color-border-secondary);
|
||||||
|
border-radius: var(--ant-border-radius-sm);
|
||||||
|
box-shadow: 0 2px 8px color-mix(in srgb, var(--ant-color-text) 10%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-graph-flow .react-flow__attribution a {
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-graph-flow .react-flow__attribution a:hover {
|
||||||
|
color: var(--ant-color-primary);
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={visibleEdges}
|
edges={visibleEdges}
|
||||||
@@ -1192,7 +1329,7 @@ export default function RelationshipGraph() {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
boxShadow: '0 12px 32px rgba(0,0,0,0.12)',
|
boxShadow: `0 12px 32px ${alphaColor(token.colorTextBase, 0.22)}`,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -1238,8 +1375,8 @@ export default function RelationshipGraph() {
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
objectFit: 'cover',
|
objectFit: 'cover',
|
||||||
border: '3px solid #fff',
|
border: `3px solid ${token.colorBgContainer}`,
|
||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
boxShadow: `0 4px 12px ${alphaColor(token.colorTextBase, 0.18)}`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -1248,14 +1385,14 @@ export default function RelationshipGraph() {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
backgroundColor: nodeDetail.color || (nodeDetail.is_organization ? '#27ae60' : '#1890ff'),
|
backgroundColor: nodeDetail.color || (nodeDetail.is_organization ? token.colorSuccess : token.colorPrimary),
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
color: '#fff',
|
color: token.colorWhite,
|
||||||
border: '3px solid #fff',
|
border: `3px solid ${token.colorBgContainer}`,
|
||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
boxShadow: `0 4px 12px ${alphaColor(token.colorTextBase, 0.18)}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{nodeDetail.is_organization ? <TeamOutlined /> : <UserOutlined />}
|
{nodeDetail.is_organization ? <TeamOutlined /> : <UserOutlined />}
|
||||||
@@ -1266,23 +1403,23 @@ export default function RelationshipGraph() {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: -4,
|
bottom: -4,
|
||||||
right: -4,
|
right: -4,
|
||||||
background: nodeDetail.is_organization ? '#27ae60' : (nodeDetail.role_type === 'protagonist' ? '#e74c3c' : nodeDetail.role_type === 'antagonist' ? '#9b59b6' : '#3498db'),
|
background: nodeDetail.is_organization ? token.colorSuccess : (nodeDetail.role_type === 'protagonist' ? token.colorError : nodeDetail.role_type === 'antagonist' ? token.colorPrimary : token.colorInfo),
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
width: 28,
|
width: 28,
|
||||||
height: 28,
|
height: 28,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
border: '2px solid #fff',
|
border: `2px solid ${token.colorBgContainer}`,
|
||||||
color: '#fff',
|
color: token.colorWhite,
|
||||||
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
boxShadow: `0 2px 6px ${alphaColor(token.colorTextBase, 0.22)}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{nodeDetail.is_organization ? <ApartmentOutlined style={{ fontSize: 14 }} /> : <UserOutlined style={{ fontSize: 14 }} />}
|
{nodeDetail.is_organization ? <ApartmentOutlined style={{ fontSize: 14 }} /> : <UserOutlined style={{ fontSize: 14 }} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ fontSize: 20, fontWeight: 600, color: '#333', marginBottom: 8 }}>{nodeDetail.name}</div>
|
<div style={{ fontSize: 20, fontWeight: 600, color: token.colorText, marginBottom: 8 }}>{nodeDetail.name}</div>
|
||||||
<Space size={6} wrap style={{ justifyContent: 'center' }}>
|
<Space size={6} wrap style={{ justifyContent: 'center' }}>
|
||||||
{!nodeDetail.is_organization && (
|
{!nodeDetail.is_organization && (
|
||||||
<Tag
|
<Tag
|
||||||
@@ -1321,12 +1458,12 @@ export default function RelationshipGraph() {
|
|||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
padding: '12px 14px',
|
padding: '12px 14px',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: '#f8f9fa',
|
background: token.colorFillQuaternary,
|
||||||
border: '1px solid #eef0f2',
|
border: `1px solid ${token.colorBorderSecondary}`,
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.02)',
|
boxShadow: `0 2px 4px ${alphaColor(token.colorTextBase, 0.06)}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text strong style={{ fontSize: 14, color: '#333' }}>
|
<Text strong style={{ fontSize: 14, color: token.colorText }}>
|
||||||
特征标签
|
特征标签
|
||||||
</Text>
|
</Text>
|
||||||
<Space size={[6, 8]} wrap style={{ marginTop: 10 }}>
|
<Space size={[6, 8]} wrap style={{ marginTop: 10 }}>
|
||||||
@@ -1352,16 +1489,16 @@ export default function RelationshipGraph() {
|
|||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
padding: '12px 14px',
|
padding: '12px 14px',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: '#f8f9fa',
|
background: token.colorFillQuaternary,
|
||||||
border: '1px solid #eef0f2',
|
border: `1px solid ${token.colorBorderSecondary}`,
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.02)',
|
boxShadow: `0 2px 4px ${alphaColor(token.colorTextBase, 0.06)}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text strong style={{ fontSize: 14, color: '#333' }}>
|
<Text strong style={{ fontSize: 14, color: token.colorText }}>
|
||||||
势力等级
|
势力等级
|
||||||
</Text>
|
</Text>
|
||||||
<div style={{ ...clampTextStyle(1), fontSize: 18, color: '#f39c12', fontWeight: 'bold' }}>
|
<div style={{ ...clampTextStyle(1), fontSize: 18, color: token.colorWarning, fontWeight: 'bold' }}>
|
||||||
{nodeDetail.power_level}<span style={{ fontSize: 14, color: '#888', fontWeight: 'normal' }}>/100</span>
|
{nodeDetail.power_level}<span style={{ fontSize: 14, color: token.colorTextTertiary, fontWeight: 'normal' }}>/100</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1372,12 +1509,12 @@ export default function RelationshipGraph() {
|
|||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
padding: '12px 14px',
|
padding: '12px 14px',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: '#f8f9fa',
|
background: token.colorFillQuaternary,
|
||||||
border: '1px solid #eef0f2',
|
border: `1px solid ${token.colorBorderSecondary}`,
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.02)',
|
boxShadow: `0 2px 4px ${alphaColor(token.colorTextBase, 0.06)}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text strong style={{ fontSize: 14, color: '#333' }}>
|
<Text strong style={{ fontSize: 14, color: token.colorText }}>
|
||||||
组织成员
|
组织成员
|
||||||
</Text>
|
</Text>
|
||||||
<Space size={[6, 8]} wrap style={{ marginTop: 10 }}>
|
<Space size={[6, 8]} wrap style={{ marginTop: 10 }}>
|
||||||
@@ -1407,9 +1544,9 @@ export default function RelationshipGraph() {
|
|||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card size="small" style={{ width: 300, borderRadius: 10, boxShadow: '0 6px 18px rgba(0,0,0,0.12)' }}>
|
<Card size="small" style={{ width: 300, borderRadius: 10, boxShadow: `0 6px 18px ${alphaColor(token.colorTextBase, 0.2)}` }}>
|
||||||
<Space align="start">
|
<Space align="start">
|
||||||
<TrophyOutlined style={{ color: '#faad14', marginTop: 4 }} />
|
<TrophyOutlined style={{ color: token.colorWarning, marginTop: 4 }} />
|
||||||
<div>
|
<div>
|
||||||
<Text strong>职业节点</Text>
|
<Text strong>职业节点</Text>
|
||||||
<p style={{ ...clampTextStyle(2), marginTop: 2 }}>
|
<p style={{ ...clampTextStyle(2), marginTop: 2 }}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, Slider, Input, Tabs, AutoComplete } from 'antd';
|
import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, Slider, Input, Tabs, AutoComplete, theme } from 'antd';
|
||||||
import { PlusOutlined, ApartmentOutlined, UserOutlined, EditOutlined } from '@ant-design/icons';
|
import { PlusOutlined, ApartmentOutlined, UserOutlined, EditOutlined } from '@ant-design/icons';
|
||||||
import { useStore } from '../store';
|
import { useStore } from '../store';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
@@ -45,6 +45,7 @@ export default function Relationships() {
|
|||||||
const [editingRelationship, setEditingRelationship] = useState<Relationship | null>(null);
|
const [editingRelationship, setEditingRelationship] = useState<Relationship | null>(null);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
|
const { token } = theme.useToken();
|
||||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
@@ -393,7 +394,7 @@ export default function Relationships() {
|
|||||||
key={category}
|
key={category}
|
||||||
size="small"
|
size="small"
|
||||||
title={categoryLabels[category] || category}
|
title={categoryLabels[category] || category}
|
||||||
headStyle={{ backgroundColor: '#f5f5f5' }}
|
headStyle={{ backgroundColor: token.colorFillAlter }}
|
||||||
>
|
>
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
{types.map(type => (
|
{types.map(type => (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Card, Form, Input, Button, Select, Slider, InputNumber, message, Space, Typography, Spin, Modal, Alert, Grid, Tabs, List, Tag, Popconfirm, Empty, Row, Col } from 'antd';
|
import { Card, Form, Input, Button, Select, Slider, InputNumber, message, Space, Typography, Spin, Modal, Alert, Grid, Tabs, List, Tag, Popconfirm, Empty, Row, Col, theme } from 'antd';
|
||||||
import { SaveOutlined, DeleteOutlined, ReloadOutlined, InfoCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined, PlusOutlined, EditOutlined, CopyOutlined, WarningOutlined } from '@ant-design/icons';
|
import { SaveOutlined, DeleteOutlined, ReloadOutlined, InfoCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined, PlusOutlined, EditOutlined, CopyOutlined, WarningOutlined } from '@ant-design/icons';
|
||||||
import { settingsApi, mcpPluginApi } from '../services/api';
|
import { settingsApi, mcpPluginApi } from '../services/api';
|
||||||
import type { SettingsUpdate, APIKeyPreset, PresetCreateRequest, APIKeyPresetConfig } from '../types';
|
import type { SettingsUpdate, APIKeyPreset, PresetCreateRequest, APIKeyPresetConfig } from '../types';
|
||||||
@@ -11,6 +11,7 @@ const { useBreakpoint } = Grid;
|
|||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
|
const { token } = theme.useToken();
|
||||||
const screens = useBreakpoint();
|
const screens = useBreakpoint();
|
||||||
const isMobile = !screens.md; // md断点是768px
|
const isMobile = !screens.md; // md断点是768px
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
@@ -51,6 +52,9 @@ export default function SettingsPage() {
|
|||||||
const [presetModelsFetched, setPresetModelsFetched] = useState(false);
|
const [presetModelsFetched, setPresetModelsFetched] = useState(false);
|
||||||
const [presetModelSearchText, setPresetModelSearchText] = useState('');
|
const [presetModelSearchText, setPresetModelSearchText] = useState('');
|
||||||
|
|
||||||
|
const pageBackground = `linear-gradient(180deg, ${token.colorBgLayout} 0%, ${token.colorFillSecondary} 100%)`;
|
||||||
|
const headerBackground = `linear-gradient(135deg, ${token.colorPrimary} 0%, ${token.colorPrimaryHover} 100%)`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
if (activeTab === 'presets') {
|
if (activeTab === 'presets') {
|
||||||
@@ -181,7 +185,7 @@ export default function SettingsPage() {
|
|||||||
modal.warning({
|
modal.warning({
|
||||||
title: (
|
title: (
|
||||||
<Space>
|
<Space>
|
||||||
<WarningOutlined style={{ color: '#faad14' }} />
|
<WarningOutlined style={{ color: token.colorWarning }} />
|
||||||
<span>API 配置已更改</span>
|
<span>API 配置已更改</span>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
@@ -196,8 +200,8 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: 12,
|
padding: 12,
|
||||||
background: 'var(--color-info-bg)',
|
background: token.colorInfoBg,
|
||||||
border: '1px solid var(--color-info-border)',
|
border: `1px solid ${token.colorInfoBorder}`,
|
||||||
borderRadius: 8
|
borderRadius: 8
|
||||||
}}>
|
}}>
|
||||||
<Text strong style={{ display: 'block', marginBottom: 8 }}>请完成以下步骤:</Text>
|
<Text strong style={{ display: 'block', marginBottom: 8 }}>请完成以下步骤:</Text>
|
||||||
@@ -600,7 +604,7 @@ export default function SettingsPage() {
|
|||||||
modal.warning({
|
modal.warning({
|
||||||
title: (
|
title: (
|
||||||
<Space>
|
<Space>
|
||||||
<WarningOutlined style={{ color: '#faad14' }} />
|
<WarningOutlined style={{ color: token.colorWarning }} />
|
||||||
<span>API 配置已更改</span>
|
<span>API 配置已更改</span>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
@@ -615,8 +619,8 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: 12,
|
padding: 12,
|
||||||
background: 'var(--color-info-bg)',
|
background: token.colorInfoBg,
|
||||||
border: '1px solid var(--color-info-border)',
|
border: `1px solid ${token.colorInfoBorder}`,
|
||||||
borderRadius: 8
|
borderRadius: 8
|
||||||
}}>
|
}}>
|
||||||
<Text strong style={{ display: 'block', marginBottom: 8 }}>请完成以下步骤:</Text>
|
<Text strong style={{ display: 'block', marginBottom: 8 }}>请完成以下步骤:</Text>
|
||||||
@@ -657,15 +661,15 @@ export default function SettingsPage() {
|
|||||||
width: isMobile ? '90%' : 600,
|
width: isMobile ? '90%' : 600,
|
||||||
content: (
|
content: (
|
||||||
<div style={{ padding: '8px 0' }}>
|
<div style={{ padding: '8px 0' }}>
|
||||||
<div style={{ marginBottom: 24, padding: 16, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
|
<div style={{ marginBottom: 24, padding: 16, background: token.colorSuccessBg, border: `1px solid ${token.colorSuccessBorder}`, borderRadius: 8 }}>
|
||||||
<Typography.Text strong style={{ color: 'var(--color-success)' }}>
|
<Typography.Text strong style={{ color: token.colorSuccess }}>
|
||||||
✓ API 连接正常
|
✓ API 连接正常
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: 16,
|
padding: 16,
|
||||||
background: 'var(--color-bg-layout)',
|
background: token.colorBgLayout,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: 16
|
marginBottom: 16
|
||||||
}}>
|
}}>
|
||||||
@@ -711,13 +715,13 @@ export default function SettingsPage() {
|
|||||||
{result.error && (
|
{result.error && (
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: 16,
|
padding: 16,
|
||||||
background: 'var(--color-error-bg)',
|
background: token.colorErrorBg,
|
||||||
border: '1px solid var(--color-error-border)',
|
border: `1px solid ${token.colorErrorBorder}`,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: 16
|
marginBottom: 16
|
||||||
}}>
|
}}>
|
||||||
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>错误信息:</Text>
|
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>错误信息:</Text>
|
||||||
<Text style={{ fontSize: 13, color: 'var(--color-error)', fontFamily: 'monospace', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
<Text style={{ fontSize: 13, color: token.colorError, fontFamily: 'monospace', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||||
{result.error}
|
{result.error}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -726,8 +730,8 @@ export default function SettingsPage() {
|
|||||||
{result.suggestions && result.suggestions.length > 0 && (
|
{result.suggestions && result.suggestions.length > 0 && (
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: 16,
|
padding: 16,
|
||||||
background: 'var(--color-warning-bg)',
|
background: token.colorWarningBg,
|
||||||
border: '1px solid var(--color-warning-border)',
|
border: `1px solid ${token.colorWarningBorder}`,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: 16
|
marginBottom: 16
|
||||||
}}>
|
}}>
|
||||||
@@ -817,10 +821,10 @@ export default function SettingsPage() {
|
|||||||
<List.Item
|
<List.Item
|
||||||
key={preset.id}
|
key={preset.id}
|
||||||
style={{
|
style={{
|
||||||
background: isActive ? '#f0f5ff' : 'transparent',
|
background: isActive ? token.colorInfoBg : 'transparent',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
marginBottom: '8px',
|
marginBottom: '8px',
|
||||||
border: isActive ? '2px solid #1890ff' : '1px solid #f0f0f0',
|
border: isActive ? `2px solid ${token.colorPrimary}` : `1px solid ${token.colorBorderSecondary}`,
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
}}
|
}}
|
||||||
actions={[
|
actions={[
|
||||||
@@ -870,7 +874,7 @@ export default function SettingsPage() {
|
|||||||
avatar={
|
avatar={
|
||||||
isActive && (
|
isActive && (
|
||||||
<CheckCircleOutlined
|
<CheckCircleOutlined
|
||||||
style={{ fontSize: '24px', color: '#52c41a' }}
|
style={{ fontSize: '24px', color: token.colorSuccess }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -883,7 +887,7 @@ export default function SettingsPage() {
|
|||||||
description={
|
description={
|
||||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||||
{preset.description && (
|
{preset.description && (
|
||||||
<div style={{ color: '#666' }}>{preset.description}</div>
|
<div style={{ color: token.colorTextSecondary }}>{preset.description}</div>
|
||||||
)}
|
)}
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Tag color={getProviderColor(preset.config.api_provider)}>
|
<Tag color={getProviderColor(preset.config.api_provider)}>
|
||||||
@@ -893,7 +897,7 @@ export default function SettingsPage() {
|
|||||||
<Tag>温度: {preset.config.temperature}</Tag>
|
<Tag>温度: {preset.config.temperature}</Tag>
|
||||||
<Tag>Tokens: {preset.config.max_tokens}</Tag>
|
<Tag>Tokens: {preset.config.max_tokens}</Tag>
|
||||||
</Space>
|
</Space>
|
||||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
<div style={{ fontSize: '12px', color: token.colorTextTertiary }}>
|
||||||
创建于: {new Date(preset.created_at).toLocaleString()}
|
创建于: {new Date(preset.created_at).toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -913,7 +917,7 @@ export default function SettingsPage() {
|
|||||||
{contextHolder}
|
{contextHolder}
|
||||||
<div style={{
|
<div style={{
|
||||||
minHeight: '90vh',
|
minHeight: '90vh',
|
||||||
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)',
|
background: pageBackground,
|
||||||
padding: isMobile ? '20px 16px 70px' : '24px 24px 70px',
|
padding: isMobile ? '20px 16px 70px' : '24px 24px 70px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -930,9 +934,9 @@ export default function SettingsPage() {
|
|||||||
<Card
|
<Card
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, #5A9BA5 50%, var(--color-primary-hover) 100%)',
|
background: headerBackground,
|
||||||
borderRadius: isMobile ? 16 : 24,
|
borderRadius: isMobile ? 16 : 24,
|
||||||
boxShadow: '0 12px 40px rgba(77, 128, 136, 0.25), 0 4px 12px rgba(0, 0, 0, 0.06)',
|
boxShadow: token.boxShadowSecondary,
|
||||||
marginBottom: isMobile ? 20 : 24,
|
marginBottom: isMobile ? 20 : 24,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -940,17 +944,17 @@ export default function SettingsPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 装饰性背景元素 */}
|
{/* 装饰性背景元素 */}
|
||||||
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.08)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: token.colorWhite, opacity: 0.08, pointerEvents: 'none' }} />
|
||||||
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.05)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: token.colorWhite, opacity: 0.05, pointerEvents: 'none' }} />
|
||||||
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.06)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: token.colorWhite, opacity: 0.06, pointerEvents: 'none' }} />
|
||||||
|
|
||||||
<Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
|
<Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
|
||||||
<Col xs={24} sm={12}>
|
<Col xs={24} sm={12}>
|
||||||
<Space direction="vertical" size={4}>
|
<Space direction="vertical" size={4}>
|
||||||
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: '#fff', textShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
|
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: token.colorWhite, textShadow: `0 2px 4px ${token.colorBgMask}` }}>
|
||||||
AI API 设置
|
AI API 设置
|
||||||
</Title>
|
</Title>
|
||||||
<Text style={{ fontSize: isMobile ? 12 : 14, color: 'rgba(255,255,255,0.85)', marginLeft: isMobile ? 40 : 48 }}>
|
<Text style={{ fontSize: isMobile ? 12 : 14, color: token.colorTextLightSolid, marginLeft: isMobile ? 40 : 48, opacity: 0.85 }}>
|
||||||
配置AI接口参数,管理多个API配置预设
|
配置AI接口参数,管理多个API配置预设
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -965,9 +969,9 @@ export default function SettingsPage() {
|
|||||||
<Card
|
<Card
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(255, 255, 255, 0.95)',
|
background: token.colorBgContainer,
|
||||||
borderRadius: isMobile ? 12 : 16,
|
borderRadius: isMobile ? 12 : 16,
|
||||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
boxShadow: token.boxShadowSecondary,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
styles={{
|
styles={{
|
||||||
@@ -1030,7 +1034,7 @@ export default function SettingsPage() {
|
|||||||
<span>API 提供商</span>
|
<span>API 提供商</span>
|
||||||
<InfoCircleOutlined
|
<InfoCircleOutlined
|
||||||
title="选择你的AI服务提供商"
|
title="选择你的AI服务提供商"
|
||||||
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
|
style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -1052,7 +1056,7 @@ export default function SettingsPage() {
|
|||||||
<span>API 密钥</span>
|
<span>API 密钥</span>
|
||||||
<InfoCircleOutlined
|
<InfoCircleOutlined
|
||||||
title="你的API密钥,将加密存储"
|
title="你的API密钥,将加密存储"
|
||||||
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
|
style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -1072,7 +1076,7 @@ export default function SettingsPage() {
|
|||||||
<span>API 地址</span>
|
<span>API 地址</span>
|
||||||
<InfoCircleOutlined
|
<InfoCircleOutlined
|
||||||
title="API的基础URL地址"
|
title="API的基础URL地址"
|
||||||
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
|
style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -1094,7 +1098,7 @@ export default function SettingsPage() {
|
|||||||
<span>模型名称</span>
|
<span>模型名称</span>
|
||||||
<InfoCircleOutlined
|
<InfoCircleOutlined
|
||||||
title="AI模型的名称,如 gpt-4, gpt-3.5-turbo"
|
title="AI模型的名称,如 gpt-4, gpt-3.5-turbo"
|
||||||
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
|
style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -1121,17 +1125,17 @@ export default function SettingsPage() {
|
|||||||
<>
|
<>
|
||||||
{menu}
|
{menu}
|
||||||
{fetchingModels && (
|
{fetchingModels && (
|
||||||
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
<div style={{ padding: '8px 12px', color: token.colorTextSecondary, textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||||
<Spin size="small" /> 正在获取模型列表...
|
<Spin size="small" /> 正在获取模型列表...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!fetchingModels && modelOptions.length === 0 && modelsFetched && !modelSearchText && (
|
{!fetchingModels && modelOptions.length === 0 && modelsFetched && !modelSearchText && (
|
||||||
<div style={{ padding: '8px 12px', color: '#ff4d4f', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
<div style={{ padding: '8px 12px', color: token.colorError, textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||||
未能获取到模型列表,可直接输入模型名称
|
未能获取到模型列表,可直接输入模型名称
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!fetchingModels && modelOptions.length === 0 && !modelsFetched && !modelSearchText && (
|
{!fetchingModels && modelOptions.length === 0 && !modelsFetched && !modelSearchText && (
|
||||||
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
<div style={{ padding: '8px 12px', color: token.colorTextSecondary, textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||||
点击输入框自动获取,或直接输入模型名称
|
点击输入框自动获取,或直接输入模型名称
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1200,13 +1204,13 @@ export default function SettingsPage() {
|
|||||||
<div style={{ fontWeight: 500, fontSize: isMobile ? '13px' : '14px' }}>
|
<div style={{ fontWeight: 500, fontSize: isMobile ? '13px' : '14px' }}>
|
||||||
{option.data.description === '手动输入的模型名称' ? (
|
{option.data.description === '手动输入的模型名称' ? (
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
<EditOutlined style={{ color: 'var(--color-primary)' }} />
|
<EditOutlined style={{ color: token.colorPrimary }} />
|
||||||
<span>使用 "{option.data.label}"</span>
|
<span>使用 "{option.data.label}"</span>
|
||||||
</Space>
|
</Space>
|
||||||
) : option.data.label}
|
) : option.data.label}
|
||||||
</div>
|
</div>
|
||||||
{option.data.description && option.data.description !== '手动输入的模型名称' && (
|
{option.data.description && option.data.description !== '手动输入的模型名称' && (
|
||||||
<div style={{ fontSize: isMobile ? '11px' : '12px', color: '#8c8c8c', marginTop: '2px' }}>
|
<div style={{ fontSize: isMobile ? '11px' : '12px', color: token.colorTextTertiary, marginTop: '2px' }}>
|
||||||
{option.data.description}
|
{option.data.description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1221,7 +1225,7 @@ export default function SettingsPage() {
|
|||||||
<span>温度参数</span>
|
<span>温度参数</span>
|
||||||
<InfoCircleOutlined
|
<InfoCircleOutlined
|
||||||
title="控制输出的随机性,值越高越随机(0.0-2.0)"
|
title="控制输出的随机性,值越高越随机(0.0-2.0)"
|
||||||
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
|
style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -1246,7 +1250,7 @@ export default function SettingsPage() {
|
|||||||
<span>最大 Token 数</span>
|
<span>最大 Token 数</span>
|
||||||
<InfoCircleOutlined
|
<InfoCircleOutlined
|
||||||
title="单次请求的最大token数量"
|
title="单次请求的最大token数量"
|
||||||
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
|
style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -1270,7 +1274,7 @@ export default function SettingsPage() {
|
|||||||
<span>系统提示词</span>
|
<span>系统提示词</span>
|
||||||
<InfoCircleOutlined
|
<InfoCircleOutlined
|
||||||
title="设置全局系统提示词,每次AI调用时都会自动使用。可用于设定AI的角色、语言风格等"
|
title="设置全局系统提示词,每次AI调用时都会自动使用。可用于设定AI的角色、语言风格等"
|
||||||
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
|
style={{ color: token.colorTextSecondary, fontSize: isMobile ? '12px' : '14px' }}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -1291,9 +1295,9 @@ export default function SettingsPage() {
|
|||||||
message={
|
message={
|
||||||
<Space>
|
<Space>
|
||||||
{testResult.success ? (
|
{testResult.success ? (
|
||||||
<CheckCircleOutlined style={{ color: 'var(--color-success)', fontSize: isMobile ? '16px' : '18px' }} />
|
<CheckCircleOutlined style={{ color: token.colorSuccess, fontSize: isMobile ? '16px' : '18px' }} />
|
||||||
) : (
|
) : (
|
||||||
<CloseCircleOutlined style={{ color: 'var(--color-error)', fontSize: isMobile ? '16px' : '18px' }} />
|
<CloseCircleOutlined style={{ color: token.colorError, fontSize: isMobile ? '16px' : '18px' }} />
|
||||||
)}
|
)}
|
||||||
<span style={{ fontSize: isMobile ? '14px' : '16px', fontWeight: 500 }}>
|
<span style={{ fontSize: isMobile ? '14px' : '16px', fontWeight: 500 }}>
|
||||||
{testResult.message}
|
{testResult.message}
|
||||||
@@ -1313,16 +1317,16 @@ export default function SettingsPage() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
fontSize: isMobile ? '12px' : '13px',
|
fontSize: isMobile ? '12px' : '13px',
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
background: '#f6ffed',
|
background: token.colorSuccessBg,
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
border: '1px solid #b7eb8f',
|
border: `1px solid ${token.colorSuccessBorder}`,
|
||||||
marginTop: '8px'
|
marginTop: '8px'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ marginBottom: '4px', fontWeight: 500 }}>AI 响应预览:</div>
|
<div style={{ marginBottom: '4px', fontWeight: 500 }}>AI 响应预览:</div>
|
||||||
<div style={{ color: '#595959' }}>{testResult.response_preview}</div>
|
<div style={{ color: token.colorTextSecondary }}>{testResult.response_preview}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{ color: 'var(--color-success)', fontSize: isMobile ? '12px' : '13px', marginTop: '4px' }}>
|
<div style={{ color: token.colorSuccess, fontSize: isMobile ? '12px' : '13px', marginTop: '4px' }}>
|
||||||
✓ API 配置正确,可以正常使用
|
✓ API 配置正确,可以正常使用
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -1332,16 +1336,16 @@ export default function SettingsPage() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
fontSize: isMobile ? '12px' : '13px',
|
fontSize: isMobile ? '12px' : '13px',
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
background: '#fff2e8',
|
background: token.colorErrorBg,
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
border: '1px solid #ffbb96',
|
border: `1px solid ${token.colorErrorBorder}`,
|
||||||
color: '#d4380d'
|
color: token.colorError
|
||||||
}}>
|
}}>
|
||||||
<strong>错误信息:</strong> {testResult.error}
|
<strong>错误信息:</strong> {testResult.error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{testResult.error_type && (
|
{testResult.error_type && (
|
||||||
<div style={{ fontSize: isMobile ? '11px' : '12px', color: 'var(--color-text-secondary)' }}>
|
<div style={{ fontSize: isMobile ? '11px' : '12px', color: token.colorTextSecondary }}>
|
||||||
错误类型: {testResult.error_type}
|
错误类型: {testResult.error_type}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1354,7 +1358,7 @@ export default function SettingsPage() {
|
|||||||
margin: 0,
|
margin: 0,
|
||||||
paddingLeft: isMobile ? '16px' : '20px',
|
paddingLeft: isMobile ? '16px' : '20px',
|
||||||
fontSize: isMobile ? '12px' : '13px',
|
fontSize: isMobile ? '12px' : '13px',
|
||||||
color: '#595959'
|
color: token.colorTextSecondary
|
||||||
}}>
|
}}>
|
||||||
{testResult.suggestions.map((suggestion, index) => (
|
{testResult.suggestions.map((suggestion, index) => (
|
||||||
<li key={index} style={{ marginBottom: '4px' }}>{suggestion}</li>
|
<li key={index} style={{ marginBottom: '4px' }}>{suggestion}</li>
|
||||||
@@ -1386,7 +1390,7 @@ export default function SettingsPage() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
block
|
block
|
||||||
style={{
|
style={{
|
||||||
background: 'var(--color-primary)',
|
background: token.colorPrimary,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
height: '44px'
|
height: '44px'
|
||||||
}}
|
}}
|
||||||
@@ -1400,8 +1404,8 @@ export default function SettingsPage() {
|
|||||||
loading={testingApi}
|
loading={testingApi}
|
||||||
block
|
block
|
||||||
style={{
|
style={{
|
||||||
borderColor: 'var(--color-success)',
|
borderColor: token.colorSuccess,
|
||||||
color: 'var(--color-success)',
|
color: token.colorSuccess,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
height: '44px'
|
height: '44px'
|
||||||
}}
|
}}
|
||||||
@@ -1466,8 +1470,8 @@ export default function SettingsPage() {
|
|||||||
onClick={handleTestConnection}
|
onClick={handleTestConnection}
|
||||||
loading={testingApi}
|
loading={testingApi}
|
||||||
style={{
|
style={{
|
||||||
borderColor: 'var(--color-success)',
|
borderColor: token.colorSuccess,
|
||||||
color: 'var(--color-success)',
|
color: token.colorSuccess,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
minWidth: '100px'
|
minWidth: '100px'
|
||||||
}}
|
}}
|
||||||
@@ -1491,7 +1495,7 @@ export default function SettingsPage() {
|
|||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
style={{
|
style={{
|
||||||
background: 'var(--color-primary)',
|
background: token.colorPrimary,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
minWidth: '120px',
|
minWidth: '120px',
|
||||||
fontWeight: 500
|
fontWeight: 500
|
||||||
@@ -1611,7 +1615,7 @@ export default function SettingsPage() {
|
|||||||
<span>模型名称</span>
|
<span>模型名称</span>
|
||||||
<InfoCircleOutlined
|
<InfoCircleOutlined
|
||||||
title="AI模型的名称,点击下拉框自动获取可用模型"
|
title="AI模型的名称,点击下拉框自动获取可用模型"
|
||||||
style={{ color: 'var(--color-text-secondary)', fontSize: '12px' }}
|
style={{ color: token.colorTextSecondary, fontSize: '12px' }}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -1637,17 +1641,17 @@ export default function SettingsPage() {
|
|||||||
<>
|
<>
|
||||||
{menu}
|
{menu}
|
||||||
{fetchingPresetModels && (
|
{fetchingPresetModels && (
|
||||||
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: '12px' }}>
|
<div style={{ padding: '8px 12px', color: token.colorTextSecondary, textAlign: 'center', fontSize: '12px' }}>
|
||||||
<Spin size="small" /> 正在获取模型列表...
|
<Spin size="small" /> 正在获取模型列表...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!fetchingPresetModels && presetModelOptions.length === 0 && presetModelsFetched && !presetModelSearchText && (
|
{!fetchingPresetModels && presetModelOptions.length === 0 && presetModelsFetched && !presetModelSearchText && (
|
||||||
<div style={{ padding: '8px 12px', color: '#ff4d4f', textAlign: 'center', fontSize: '12px' }}>
|
<div style={{ padding: '8px 12px', color: token.colorError, textAlign: 'center', fontSize: '12px' }}>
|
||||||
未能获取到模型列表,可直接输入模型名称
|
未能获取到模型列表,可直接输入模型名称
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!fetchingPresetModels && presetModelOptions.length === 0 && !presetModelsFetched && !presetModelSearchText && (
|
{!fetchingPresetModels && presetModelOptions.length === 0 && !presetModelsFetched && !presetModelSearchText && (
|
||||||
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: '12px' }}>
|
<div style={{ padding: '8px 12px', color: token.colorTextSecondary, textAlign: 'center', fontSize: '12px' }}>
|
||||||
点击输入框自动获取,或直接输入模型名称
|
点击输入框自动获取,或直接输入模型名称
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1714,13 +1718,13 @@ export default function SettingsPage() {
|
|||||||
<div style={{ fontWeight: 500, fontSize: '13px' }}>
|
<div style={{ fontWeight: 500, fontSize: '13px' }}>
|
||||||
{option.data.description === '手动输入的模型名称' ? (
|
{option.data.description === '手动输入的模型名称' ? (
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
<EditOutlined style={{ color: 'var(--color-primary)' }} />
|
<EditOutlined style={{ color: token.colorPrimary }} />
|
||||||
<span>使用 "{option.data.label}"</span>
|
<span>使用 "{option.data.label}"</span>
|
||||||
</Space>
|
</Space>
|
||||||
) : option.data.label}
|
) : option.data.label}
|
||||||
</div>
|
</div>
|
||||||
{option.data.description && option.data.description !== '手动输入的模型名称' && (
|
{option.data.description && option.data.description !== '手动输入的模型名称' && (
|
||||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginTop: '2px' }}>
|
<div style={{ fontSize: '11px', color: token.colorTextTertiary, marginTop: '2px' }}>
|
||||||
{option.data.description}
|
{option.data.description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useState } from 'react';
|
import { useState, type ReactNode } from 'react';
|
||||||
import { Card, Row, Col, Typography, Image, Divider, Modal, Button } from 'antd';
|
import { Card, Row, Col, Typography, Image, Divider, Modal, Button, theme } from 'antd';
|
||||||
import {
|
import {
|
||||||
HeartOutlined,
|
HeartOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
RocketOutlined,
|
RocketOutlined,
|
||||||
MessageOutlined,
|
MessageOutlined,
|
||||||
StarOutlined
|
// StarOutlined,
|
||||||
|
WechatOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
const { Title, Paragraph, Text } = Typography;
|
const { Title, Paragraph, Text } = Typography;
|
||||||
@@ -18,6 +19,13 @@ interface SponsorOption {
|
|||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SponsorBenefit {
|
||||||
|
icon: ReactNode;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
price?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const sponsorOptions: SponsorOption[] = [
|
const sponsorOptions: SponsorOption[] = [
|
||||||
{ amount: 5, label: '🌶️ 一包辣条', image: '/5.png', description: '¥5' },
|
{ amount: 5, label: '🌶️ 一包辣条', image: '/5.png', description: '¥5' },
|
||||||
{ amount: 10, label: '🍱 一顿拼好饭', image: '/10.png', description: '¥10' },
|
{ amount: 10, label: '🍱 一顿拼好饭', image: '/10.png', description: '¥10' },
|
||||||
@@ -26,27 +34,39 @@ const sponsorOptions: SponsorOption[] = [
|
|||||||
{ amount: 99, label: '🍲 一顿海底捞', image: '/99.png', description: '¥99' },
|
{ amount: 99, label: '🍲 一顿海底捞', image: '/99.png', description: '¥99' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const benefits = [
|
const benefits: SponsorBenefit[] = [
|
||||||
{
|
{
|
||||||
icon: <FileTextOutlined style={{ fontSize: '32px', color: 'var(--color-primary)' }} />,
|
icon: <WechatOutlined style={{ fontSize: '32px', color: 'var(--ant-color-primary)' }} />,
|
||||||
|
title: '加入赞助群',
|
||||||
|
description: '进入内部群,获取项目第一手更新消息',
|
||||||
|
price: '(🌶️ 一包辣条)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FileTextOutlined style={{ fontSize: '32px', color: 'var(--ant-color-primary)' }} />,
|
||||||
title: '优先需求响应',
|
title: '优先需求响应',
|
||||||
description: '您的功能需求和问题反馈将获得优先处理'
|
description: '您的功能需求和问题反馈将获得优先处理',
|
||||||
|
price: '(🌶️ 一包辣条)'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <RocketOutlined style={{ fontSize: '32px', color: 'var(--color-success)' }} />,
|
icon: <RocketOutlined style={{ fontSize: '32px', color: 'var(--ant-color-success)' }} />,
|
||||||
title: 'Windows一键启动',
|
title: 'Windows一键启动',
|
||||||
description: '获取免安装一键启动包,开箱即可使用'
|
description: '获取免安装一键启动包,开箱即可使用',
|
||||||
|
price: '(🌶️ 一包辣条)'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <MessageOutlined style={{ fontSize: '32px', color: 'var(--color-warning)' }} />,
|
icon: <MessageOutlined style={{ fontSize: '32px', color: 'var(--ant-color-warning)' }} />,
|
||||||
title: '专属技术支持',
|
title: '专属技术支持',
|
||||||
description: '加入赞助者群,获得远程协助和配置指导(☕ 一杯咖啡)'
|
description: '获得远程协助和配置指导',
|
||||||
|
price: '(☕ 一杯咖啡)'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Sponsor() {
|
export default function Sponsor() {
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [selectedOption, setSelectedOption] = useState<SponsorOption | null>(null);
|
const [selectedOption, setSelectedOption] = useState<SponsorOption | null>(null);
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const alphaColor = (color: string, alpha: number) =>
|
||||||
|
`color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||||
|
|
||||||
const handleCardClick = (option: SponsorOption) => {
|
const handleCardClick = (option: SponsorOption) => {
|
||||||
setSelectedOption(option);
|
setSelectedOption(option);
|
||||||
@@ -64,10 +84,11 @@ export default function Sponsor() {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
overflowX: 'hidden',
|
overflowX: 'hidden',
|
||||||
padding: 'clamp(16px, 3vh, 24px) clamp(12px, 2vw, 16px)'
|
// padding: 'clamp(16px, 3vh, 24px) clamp(12px, 2vw, 16px)'
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
maxWidth: '1200px',
|
// maxWidth: '1200px',
|
||||||
|
height: '100%',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -76,46 +97,45 @@ export default function Sponsor() {
|
|||||||
}}>
|
}}>
|
||||||
{/* 头部标题区域 */}
|
{/* 头部标题区域 */}
|
||||||
<div style={{ textAlign: 'center', marginBottom: 'clamp(20px, 4vh, 32px)' }}>
|
<div style={{ textAlign: 'center', marginBottom: 'clamp(20px, 4vh, 32px)' }}>
|
||||||
<Title level={1} style={{ marginBottom: '8px', fontSize: 'clamp(24px, 5vw, 32px)', fontWeight: 'bold' }}>
|
|
||||||
赞助 MuMuAINovel
|
|
||||||
</Title>
|
|
||||||
<Text type="secondary" style={{ fontSize: 'clamp(11px, 2vw, 13px)', letterSpacing: '2px' }}>
|
|
||||||
SUPPORT AI NOVEL CREATION
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
marginTop: 'clamp(12px, 2vh, 16px)',
|
|
||||||
padding: 'clamp(12px, 2vh, 16px)',
|
padding: 'clamp(12px, 2vh, 16px)',
|
||||||
background: 'var(--color-primary)',
|
background: token.colorPrimary,
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
color: '#fff'
|
color: token.colorWhite
|
||||||
}}>
|
}}>
|
||||||
<Title level={4} style={{ color: '#fff', marginBottom: '8px' }}>
|
<Title level={1} style={{ color: token.colorWhite, marginBottom: '8px', fontSize: 'clamp(24px, 5vw, 32px)', fontWeight: 'bold' }}>
|
||||||
|
赞助 MuMuAINovel
|
||||||
|
</Title>
|
||||||
|
<Text type="secondary" style={{ color: token.colorWhite, fontSize: 'clamp(11px, 2vw, 13px)', letterSpacing: '2px' }}>
|
||||||
|
SUPPORT MuMuAINovel
|
||||||
|
</Text>
|
||||||
|
<Title level={4} style={{ color: token.colorWhite, marginTop: '8px', marginBottom: '8px' }}>
|
||||||
📚 MuMuAINovel - 基于 AI 的智能小说创作助手
|
📚 MuMuAINovel - 基于 AI 的智能小说创作助手
|
||||||
</Title>
|
</Title>
|
||||||
<Paragraph style={{ color: '#fff', fontSize: '14px', margin: 0 }}>
|
|
||||||
支持多AI模型、智能向导、角色管理、章节编辑等强大功能
|
|
||||||
</Paragraph>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 赞助专属权益 */}
|
{/* 赞助专属权益 */}
|
||||||
<div style={{ marginBottom: 'clamp(24px, 4vh, 32px)' }}>
|
<div style={{ marginBottom: 'clamp(24px, 4vh, 32px)' }}>
|
||||||
<Title level={3} style={{ textAlign: 'center', marginBottom: 'clamp(16px, 3vh, 20px)', fontSize: 'clamp(18px, 3vw, 24px)' }}>
|
<Title level={3} style={{ textAlign: 'center', marginBottom: 'clamp(16px, 3vh, 20px)', fontSize: 'clamp(18px, 3vw, 24px)' }}>
|
||||||
<CheckCircleOutlined style={{ color: 'var(--color-success)', marginRight: '8px' }} />
|
<CheckCircleOutlined style={{ color: token.colorSuccess, marginRight: '8px' }} />
|
||||||
赞助专属权益
|
赞助专属权益
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Row gutter={[{ xs: 8, sm: 12, md: 16 }, { xs: 8, sm: 12, md: 16 }]}>
|
<Row
|
||||||
|
gutter={[{ xs: 8, sm: 12, md: 16 }, { xs: 8, sm: 12, md: 16 }]}
|
||||||
|
wrap={false}
|
||||||
|
style={{ overflowX: 'auto', paddingBottom: '4px' }}
|
||||||
|
>
|
||||||
{benefits.map((benefit, index) => (
|
{benefits.map((benefit, index) => (
|
||||||
<Col xs={24} md={8} key={index}>
|
<Col key={index} flex="1" style={{ minWidth: '200px' }}>
|
||||||
<Card
|
<Card
|
||||||
hoverable
|
hoverable
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)'
|
boxShadow: `0 2px 8px ${alphaColor(token.colorTextBase, 0.12)}`
|
||||||
}}
|
}}
|
||||||
styles={{
|
styles={{
|
||||||
body: { padding: 'clamp(16px, 3vh, 20px) clamp(12px, 2vw, 16px)' }
|
body: { padding: 'clamp(16px, 3vh, 20px) clamp(12px, 2vw, 16px)' }
|
||||||
@@ -125,9 +145,14 @@ export default function Sponsor() {
|
|||||||
{benefit.icon}
|
{benefit.icon}
|
||||||
</div>
|
</div>
|
||||||
<Title level={5} style={{ marginBottom: '8px', fontSize: 'clamp(14px, 2.5vw, 16px)' }}>{benefit.title}</Title>
|
<Title level={5} style={{ marginBottom: '8px', fontSize: 'clamp(14px, 2.5vw, 16px)' }}>{benefit.title}</Title>
|
||||||
<Paragraph style={{ color: '#666', marginBottom: 0, fontSize: 'clamp(12px, 2vw, 13px)' }}>
|
<Paragraph style={{ color: token.colorTextSecondary, marginBottom: 0, fontSize: 'clamp(12px, 2vw, 13px)' }}>
|
||||||
{benefit.description}
|
{benefit.description}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
{benefit.price && (
|
||||||
|
<Paragraph style={{ color: token.colorWarning, margin: '4px 0 0', fontSize: 'clamp(12px, 2vw, 13px)', fontWeight: 600 }}>
|
||||||
|
{benefit.price}
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
))}
|
||||||
@@ -135,9 +160,9 @@ export default function Sponsor() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 选择金额 */}
|
{/* 选择金额 */}
|
||||||
<div style={{ marginBottom: 'clamp(24px, 4vh, 32px)' }}>
|
<div>
|
||||||
<Title level={3} style={{ textAlign: 'center', marginBottom: 'clamp(16px, 3vh, 20px)', fontSize: 'clamp(18px, 3vw, 24px)' }}>
|
<Title level={3} style={{ textAlign: 'center', marginBottom: 'clamp(16px, 3vh, 20px)', fontSize: 'clamp(18px, 3vw, 24px)' }}>
|
||||||
<HeartOutlined style={{ color: '#f5222d', marginRight: '8px' }} />
|
<HeartOutlined style={{ color: token.colorError, marginRight: '8px' }} />
|
||||||
选择金额
|
选择金额
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
@@ -150,34 +175,34 @@ export default function Sponsor() {
|
|||||||
style={{
|
style={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
boxShadow: 'var(--shadow-card)',
|
boxShadow: `0 2px 8px ${alphaColor(token.colorTextBase, 0.12)}`,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.3s',
|
transition: 'all 0.3s',
|
||||||
border: '2px solid var(--color-border)'
|
border: `2px solid ${token.colorBorder}`
|
||||||
}}
|
}}
|
||||||
styles={{
|
styles={{
|
||||||
body: { padding: 'clamp(16px, 3vh, 20px) clamp(10px, 2vw, 12px)' }
|
body: { padding: 'clamp(16px, 3vh, 20px) clamp(10px, 2vw, 12px)' }
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.transform = 'translateY(-8px)';
|
e.currentTarget.style.transform = 'translateY(-8px)';
|
||||||
e.currentTarget.style.boxShadow = 'var(--shadow-elevated)';
|
e.currentTarget.style.boxShadow = `0 8px 24px ${alphaColor(token.colorPrimary, 0.3)}`;
|
||||||
e.currentTarget.style.borderColor = 'var(--color-primary)';
|
e.currentTarget.style.borderColor = token.colorPrimary;
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.transform = 'translateY(0)';
|
e.currentTarget.style.transform = 'translateY(0)';
|
||||||
e.currentTarget.style.boxShadow = 'var(--shadow-card)';
|
e.currentTarget.style.boxShadow = `0 2px 8px ${alphaColor(token.colorTextBase, 0.12)}`;
|
||||||
e.currentTarget.style.borderColor = 'var(--color-border)';
|
e.currentTarget.style.borderColor = token.colorBorder;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Title level={3} style={{
|
<Title level={3} style={{
|
||||||
color: 'var(--color-primary)',
|
color: token.colorPrimary,
|
||||||
marginBottom: '4px',
|
marginBottom: '4px',
|
||||||
fontSize: 'clamp(20px, 4vw, 28px)',
|
fontSize: 'clamp(20px, 4vw, 28px)',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}}>
|
}}>
|
||||||
{option.description}
|
{option.description}
|
||||||
</Title>
|
</Title>
|
||||||
<Text style={{ fontSize: 'clamp(12px, 2vw, 14px)', color: '#666' }}>
|
<Text style={{ fontSize: 'clamp(12px, 2vw, 14px)', color: token.colorTextSecondary }}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</Text>
|
</Text>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -186,29 +211,29 @@ export default function Sponsor() {
|
|||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider style={{ margin: 'clamp(16px, 3vh, 24px) 0' }} />
|
<Divider style={{ margin: 'clamp(16px, 3vh, 18px) 0' }} />
|
||||||
|
|
||||||
{/* 感谢文案 */}
|
{/* 感谢文案 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
padding: 'clamp(16px, 3vh, 24px) clamp(16px, 3vw, 20px)',
|
padding: 'clamp(16px, 3vw, 20px)',
|
||||||
background: '#f9f9f9',
|
background: token.colorFillQuaternary,
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
marginTop: 'auto'
|
marginTop: 'auto'
|
||||||
}}>
|
}}>
|
||||||
<Title level={4} style={{ marginBottom: '12px', fontSize: 'clamp(16px, 3vw, 20px)' }}>
|
<Title level={4} style={{ marginBottom: '12px', fontSize: 'clamp(16px, 3vw, 20px)' }}>
|
||||||
💖 感谢您对 MuMuAINovel 项目的支持
|
💖 感谢您对 MuMuAINovel 项目的支持
|
||||||
</Title>
|
</Title>
|
||||||
<Paragraph style={{ fontSize: 'clamp(12px, 2vw, 14px)', color: '#666', marginBottom: '12px' }}>
|
<Paragraph style={{ fontSize: 'clamp(12px, 2vw, 14px)', color: token.colorTextSecondary, marginBottom: '12px' }}>
|
||||||
您的赞助将是我持续更新项目的动力,为大家提供更好的AI小说创作体验
|
您的赞助将是我持续更新项目的动力,为大家提供更好的AI小说创作体验!
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<div style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>
|
{/* <div style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>
|
||||||
<StarOutlined style={{ color: '#faad14', margin: '0 4px' }} />
|
<StarOutlined style={{ color: token.colorWarning, margin: '0 4px' }} />
|
||||||
<StarOutlined style={{ color: '#faad14', margin: '0 4px' }} />
|
<StarOutlined style={{ color: token.colorWarning, margin: '0 4px' }} />
|
||||||
<StarOutlined style={{ color: '#faad14', margin: '0 4px' }} />
|
<StarOutlined style={{ color: token.colorWarning, margin: '0 4px' }} />
|
||||||
<StarOutlined style={{ color: '#faad14', margin: '0 4px' }} />
|
<StarOutlined style={{ color: token.colorWarning, margin: '0 4px' }} />
|
||||||
<StarOutlined style={{ color: '#faad14', margin: '0 4px' }} />
|
<StarOutlined style={{ color: token.colorWarning, margin: '0 4px' }} />
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -240,14 +265,14 @@ export default function Sponsor() {
|
|||||||
style={{
|
style={{
|
||||||
maxWidth: '280px',
|
maxWidth: '280px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
border: '1px solid #f0f0f0'
|
border: `1px solid ${token.colorBorderSecondary}`
|
||||||
}}
|
}}
|
||||||
preview={false}
|
preview={false}
|
||||||
/>
|
/>
|
||||||
<Paragraph style={{ marginTop: '20px', color: '#666' }}>
|
<Paragraph style={{ marginTop: '20px', color: token.colorTextSecondary }}>
|
||||||
扫描二维码完成支付
|
扫描二维码完成支付
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph style={{ color: '#999', fontSize: '12px' }}>
|
<Paragraph style={{ color: token.colorTextTertiary, fontSize: '12px' }}>
|
||||||
支付后可添加微信/QQ联系我们获取权益
|
支付后可添加微信/QQ联系我们获取权益
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Col,
|
Col,
|
||||||
Pagination,
|
Pagination,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
|
theme,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
@@ -72,6 +73,8 @@ export default function UserManagement() {
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [editForm] = Form.useForm();
|
const [editForm] = Form.useForm();
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
|
||||||
|
|
||||||
// 过滤用户列表
|
// 过滤用户列表
|
||||||
const filteredUsers = users.filter(user => {
|
const filteredUsers = users.filter(user => {
|
||||||
@@ -180,7 +183,7 @@ export default function UserManagement() {
|
|||||||
<div>
|
<div>
|
||||||
<p>用户名:<Text strong>{values.username}</Text></p>
|
<p>用户名:<Text strong>{values.username}</Text></p>
|
||||||
<p>初始密码:<Text strong copyable>{res.default_password}</Text></p>
|
<p>初始密码:<Text strong copyable>{res.default_password}</Text></p>
|
||||||
<p style={{ color: '#ff4d4f', marginTop: 16 }}>
|
<p style={{ color: token.colorError, marginTop: 16 }}>
|
||||||
⚠️ 请复制密码并告知用户,此密码仅显示一次!
|
⚠️ 请复制密码并告知用户,此密码仅显示一次!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -270,7 +273,7 @@ export default function UserManagement() {
|
|||||||
<div>
|
<div>
|
||||||
<p>用户:<Text strong>{currentUser.username}</Text></p>
|
<p>用户:<Text strong>{currentUser.username}</Text></p>
|
||||||
<p>新密码:<Text strong copyable>{res.new_password}</Text></p>
|
<p>新密码:<Text strong copyable>{res.new_password}</Text></p>
|
||||||
<p style={{ color: '#ff4d4f', marginTop: 16 }}>
|
<p style={{ color: token.colorError, marginTop: 16 }}>
|
||||||
⚠️ 请复制密码并告知用户!
|
⚠️ 请复制密码并告知用户!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -312,7 +315,7 @@ export default function UserManagement() {
|
|||||||
sortOrder: sortField === 'username' ? sortOrder : null,
|
sortOrder: sortField === 'username' ? sortOrder : null,
|
||||||
render: (text: string) => (
|
render: (text: string) => (
|
||||||
<Space>
|
<Space>
|
||||||
<UserOutlined style={{ color: 'var(--color-primary)' }} />
|
<UserOutlined style={{ color: token.colorPrimary }} />
|
||||||
<Text strong>{text}</Text>
|
<Text strong>{text}</Text>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
@@ -508,7 +511,7 @@ export default function UserManagement() {
|
|||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)',
|
background: `linear-gradient(180deg, ${token.colorBgLayout} 0%, ${alphaColor(token.colorPrimary, 0.08)} 100%)`,
|
||||||
padding: isMobile ? '20px 16px' : '40px 24px',
|
padding: isMobile ? '20px 16px' : '40px 24px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -528,9 +531,9 @@ export default function UserManagement() {
|
|||||||
<Card
|
<Card
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, #5A9BA5 50%, var(--color-primary-hover) 100%)',
|
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${alphaColor(token.colorPrimary, 0.8)} 50%, ${token.colorPrimaryHover} 100%)`,
|
||||||
borderRadius: isMobile ? 16 : 24,
|
borderRadius: isMobile ? 16 : 24,
|
||||||
boxShadow: '0 12px 40px rgba(77, 128, 136, 0.25), 0 4px 12px rgba(0, 0, 0, 0.06)',
|
boxShadow: `0 12px 40px ${alphaColor(token.colorPrimary, 0.25)}, 0 4px 12px ${alphaColor(token.colorText, 0.08)}`,
|
||||||
marginBottom: isMobile ? 20 : 24,
|
marginBottom: isMobile ? 20 : 24,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -538,18 +541,18 @@ export default function UserManagement() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 装饰性背景元素 */}
|
{/* 装饰性背景元素 */}
|
||||||
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.08)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: alphaColor(token.colorWhite, 0.08), pointerEvents: 'none' }} />
|
||||||
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.05)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: alphaColor(token.colorWhite, 0.05), pointerEvents: 'none' }} />
|
||||||
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.06)', pointerEvents: 'none' }} />
|
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: alphaColor(token.colorWhite, 0.06), pointerEvents: 'none' }} />
|
||||||
|
|
||||||
<Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
|
<Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
|
||||||
<Col xs={24} sm={12}>
|
<Col xs={24} sm={12}>
|
||||||
<Space direction="vertical" size={4}>
|
<Space direction="vertical" size={4}>
|
||||||
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: '#fff', textShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
|
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: token.colorWhite, textShadow: `0 2px 4px ${alphaColor(token.colorText, 0.2)}` }}>
|
||||||
<TeamOutlined style={{ color: 'rgba(255,255,255,0.9)', marginRight: 12 }} />
|
<TeamOutlined style={{ color: alphaColor(token.colorWhite, 0.9), marginRight: 12 }} />
|
||||||
用户管理
|
用户管理
|
||||||
</Title>
|
</Title>
|
||||||
<Text style={{ fontSize: isMobile ? 12 : 14, color: 'rgba(255,255,255,0.85)' }}>
|
<Text style={{ fontSize: isMobile ? 12 : 14, color: alphaColor(token.colorWhite, 0.85) }}>
|
||||||
管理系统用户和权限
|
管理系统用户和权限
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -561,19 +564,19 @@ export default function UserManagement() {
|
|||||||
onClick={() => navigate('/')}
|
onClick={() => navigate('/')}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: 'rgba(255, 255, 255, 0.15)',
|
background: alphaColor(token.colorWhite, 0.15),
|
||||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
border: `1px solid ${alphaColor(token.colorWhite, 0.3)}`,
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
boxShadow: `0 2px 8px ${alphaColor(token.colorText, 0.15)}`,
|
||||||
color: '#fff',
|
color: token.colorWhite,
|
||||||
backdropFilter: 'blur(10px)',
|
backdropFilter: 'blur(10px)',
|
||||||
transition: 'all 0.3s ease'
|
transition: 'all 0.3s ease'
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
|
e.currentTarget.style.background = alphaColor(token.colorWhite, 0.25);
|
||||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
|
e.currentTarget.style.background = alphaColor(token.colorWhite, 0.15);
|
||||||
e.currentTarget.style.transform = 'none';
|
e.currentTarget.style.transform = 'none';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -585,10 +588,10 @@ export default function UserManagement() {
|
|||||||
onClick={() => setModalVisible(true)}
|
onClick={() => setModalVisible(true)}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: 'rgba(255, 193, 7, 0.95)',
|
background: alphaColor(token.colorWarning, 0.95),
|
||||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
border: `1px solid ${alphaColor(token.colorWhite, 0.3)}`,
|
||||||
boxShadow: '0 4px 16px rgba(255, 193, 7, 0.4)',
|
boxShadow: `0 4px 16px ${alphaColor(token.colorWarning, 0.4)}`,
|
||||||
color: '#fff',
|
color: token.colorWhite,
|
||||||
fontWeight: 600
|
fontWeight: 600
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -604,11 +607,11 @@ export default function UserManagement() {
|
|||||||
<Card
|
<Card
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(255, 255, 255, 0.7)',
|
background: alphaColor(token.colorBgContainer, 0.72),
|
||||||
borderRadius: isMobile ? 16 : 24,
|
borderRadius: isMobile ? 16 : 24,
|
||||||
border: '1px solid rgba(255, 255, 255, 0.4)',
|
border: `1px solid ${alphaColor(token.colorWhite, 0.45)}`,
|
||||||
backdropFilter: 'blur(20px)',
|
backdropFilter: 'blur(20px)',
|
||||||
boxShadow: '0 4px 24px rgba(0, 0, 0, 0.04)',
|
boxShadow: `0 4px 24px ${alphaColor(token.colorText, 0.06)}`,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -625,11 +628,11 @@ export default function UserManagement() {
|
|||||||
{/* 搜索栏 */}
|
{/* 搜索栏 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '16px 24px 0 24px',
|
padding: '16px 24px 0 24px',
|
||||||
borderBottom: '1px solid rgba(0, 0, 0, 0.03)',
|
borderBottom: `1px solid ${alphaColor(token.colorText, 0.06)}`,
|
||||||
}}>
|
}}>
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索用户名、显示名称或用户ID"
|
placeholder="搜索用户名、显示名称或用户ID"
|
||||||
prefix={<SearchOutlined style={{ color: '#999' }} />}
|
prefix={<SearchOutlined style={{ color: token.colorTextTertiary }} />}
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearchText(e.target.value);
|
setSearchText(e.target.value);
|
||||||
@@ -676,7 +679,7 @@ export default function UserManagement() {
|
|||||||
{/* 固定分页控件 */}
|
{/* 固定分页控件 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '16px 24px 24px 24px',
|
padding: '16px 24px 24px 24px',
|
||||||
borderTop: '1px solid rgba(0, 0, 0, 0.03)',
|
borderTop: `1px solid ${alphaColor(token.colorText, 0.06)}`,
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Card, Descriptions, Empty, Typography, Button, Modal, Form, Input, message, Flex, InputNumber, Select } from 'antd';
|
import { Card, Descriptions, Empty, Typography, Button, Modal, Form, Input, message, Flex, InputNumber, Select, theme } from 'antd';
|
||||||
import { GlobalOutlined, EditOutlined, SyncOutlined, FormOutlined } from '@ant-design/icons';
|
import { GlobalOutlined, EditOutlined, SyncOutlined, FormOutlined } from '@ant-design/icons';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useStore } from '../store';
|
import { useStore } from '../store';
|
||||||
import { cardStyles } from '../components/CardStyles';
|
import { worldSettingCardStyles } from '../components/CardStyles';
|
||||||
import { projectApi, wizardStreamApi } from '../services/api';
|
import { projectApi, wizardStreamApi } from '../services/api';
|
||||||
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
|
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ export default function WorldSetting() {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [isSavingPreview, setIsSavingPreview] = useState(false);
|
const [isSavingPreview, setIsSavingPreview] = useState(false);
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
// AI重新生成世界观
|
// AI重新生成世界观
|
||||||
const handleRegenerate = async () => {
|
const handleRegenerate = async () => {
|
||||||
@@ -140,14 +141,14 @@ export default function WorldSetting() {
|
|||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: token.colorBgContainer,
|
||||||
padding: '16px 0',
|
padding: '16px 0',
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
borderBottom: '1px solid var(--color-border-secondary)',
|
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
}}>
|
}}>
|
||||||
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: 'var(--color-primary)' }} />
|
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: token.colorPrimary }} />
|
||||||
<h2 style={{ margin: 0 }}>世界设定</h2>
|
<h2 style={{ margin: 0 }}>世界设定</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -174,10 +175,10 @@ export default function WorldSetting() {
|
|||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: token.colorBgContainer,
|
||||||
padding: '16px 0',
|
padding: '16px 0',
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
borderBottom: '1px solid #f0f0f0'
|
borderBottom: `1px solid ${token.colorBorderSecondary}`
|
||||||
}}>
|
}}>
|
||||||
<Flex
|
<Flex
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
@@ -186,7 +187,7 @@ export default function WorldSetting() {
|
|||||||
wrap="wrap"
|
wrap="wrap"
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', minWidth: 'fit-content' }}>
|
<div style={{ display: 'flex', alignItems: 'center', minWidth: 'fit-content' }}>
|
||||||
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: 'var(--color-primary)' }} />
|
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: token.colorPrimary }} />
|
||||||
<h2 style={{ margin: 0, whiteSpace: 'nowrap' }}>世界设定</h2>
|
<h2 style={{ margin: 0, whiteSpace: 'nowrap' }}>世界设定</h2>
|
||||||
</div>
|
</div>
|
||||||
<Flex gap={8} wrap="wrap" style={{ flex: '0 1 auto' }}>
|
<Flex gap={8} wrap="wrap" style={{ flex: '0 1 auto' }}>
|
||||||
@@ -249,7 +250,7 @@ export default function WorldSetting() {
|
|||||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||||
<Card
|
<Card
|
||||||
style={{
|
style={{
|
||||||
...cardStyles.base,
|
...worldSettingCardStyles.sectionCard,
|
||||||
marginBottom: 16
|
marginBottom: 16
|
||||||
}}
|
}}
|
||||||
title={
|
title={
|
||||||
@@ -274,7 +275,7 @@ export default function WorldSetting() {
|
|||||||
|
|
||||||
<Card
|
<Card
|
||||||
style={{
|
style={{
|
||||||
...cardStyles.base,
|
...worldSettingCardStyles.sectionCard,
|
||||||
marginBottom: 16
|
marginBottom: 16
|
||||||
}}
|
}}
|
||||||
title={
|
title={
|
||||||
@@ -287,16 +288,16 @@ export default function WorldSetting() {
|
|||||||
<div style={{ padding: '16px 0' }}>
|
<div style={{ padding: '16px 0' }}>
|
||||||
{currentProject.world_time_period && (
|
{currentProject.world_time_period && (
|
||||||
<div style={{ marginBottom: 24 }}>
|
<div style={{ marginBottom: 24 }}>
|
||||||
<Title level={5} style={{ color: 'var(--color-primary)', marginBottom: 12 }}>
|
<Title level={5} style={{ color: token.colorPrimary, marginBottom: 12 }}>
|
||||||
时间设定
|
时间设定
|
||||||
</Title>
|
</Title>
|
||||||
<Paragraph style={{
|
<Paragraph style={{
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
lineHeight: 1.8,
|
lineHeight: 1.8,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
background: 'var(--color-bg-layout)',
|
background: token.colorBgLayout,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
borderLeft: '4px solid var(--color-primary)'
|
borderLeft: `4px solid ${token.colorPrimary}`
|
||||||
}}>
|
}}>
|
||||||
{currentProject.world_time_period}
|
{currentProject.world_time_period}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
@@ -305,16 +306,16 @@ export default function WorldSetting() {
|
|||||||
|
|
||||||
{currentProject.world_location && (
|
{currentProject.world_location && (
|
||||||
<div style={{ marginBottom: 24 }}>
|
<div style={{ marginBottom: 24 }}>
|
||||||
<Title level={5} style={{ color: 'var(--color-success)', marginBottom: 12 }}>
|
<Title level={5} style={{ color: token.colorSuccess, marginBottom: 12 }}>
|
||||||
地点设定
|
地点设定
|
||||||
</Title>
|
</Title>
|
||||||
<Paragraph style={{
|
<Paragraph style={{
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
lineHeight: 1.8,
|
lineHeight: 1.8,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
background: 'var(--color-bg-layout)',
|
background: token.colorBgLayout,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
borderLeft: '4px solid var(--color-success)'
|
borderLeft: `4px solid ${token.colorSuccess}`
|
||||||
}}>
|
}}>
|
||||||
{currentProject.world_location}
|
{currentProject.world_location}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
@@ -323,16 +324,16 @@ export default function WorldSetting() {
|
|||||||
|
|
||||||
{currentProject.world_atmosphere && (
|
{currentProject.world_atmosphere && (
|
||||||
<div style={{ marginBottom: 24 }}>
|
<div style={{ marginBottom: 24 }}>
|
||||||
<Title level={5} style={{ color: 'var(--color-warning)', marginBottom: 12 }}>
|
<Title level={5} style={{ color: token.colorWarning, marginBottom: 12 }}>
|
||||||
氛围设定
|
氛围设定
|
||||||
</Title>
|
</Title>
|
||||||
<Paragraph style={{
|
<Paragraph style={{
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
lineHeight: 1.8,
|
lineHeight: 1.8,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
background: 'var(--color-bg-layout)',
|
background: token.colorBgLayout,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
borderLeft: '4px solid var(--color-warning)'
|
borderLeft: `4px solid ${token.colorWarning}`
|
||||||
}}>
|
}}>
|
||||||
{currentProject.world_atmosphere}
|
{currentProject.world_atmosphere}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
@@ -341,16 +342,16 @@ export default function WorldSetting() {
|
|||||||
|
|
||||||
{currentProject.world_rules && (
|
{currentProject.world_rules && (
|
||||||
<div style={{ marginBottom: 0 }}>
|
<div style={{ marginBottom: 0 }}>
|
||||||
<Title level={5} style={{ color: 'var(--color-error)', marginBottom: 12 }}>
|
<Title level={5} style={{ color: token.colorError, marginBottom: 12 }}>
|
||||||
规则设定
|
规则设定
|
||||||
</Title>
|
</Title>
|
||||||
<Paragraph style={{
|
<Paragraph style={{
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
lineHeight: 1.8,
|
lineHeight: 1.8,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
background: 'var(--color-bg-layout)',
|
background: token.colorBgLayout,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
borderLeft: '4px solid var(--color-error)'
|
borderLeft: `4px solid ${token.colorError}`
|
||||||
}}>
|
}}>
|
||||||
{currentProject.world_rules}
|
{currentProject.world_rules}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
@@ -616,71 +617,71 @@ export default function WorldSetting() {
|
|||||||
>
|
>
|
||||||
{newWorldData && (
|
{newWorldData && (
|
||||||
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
||||||
<div style={{ marginBottom: 24, padding: 16, background: 'var(--color-warning-bg)', border: '1px solid var(--color-warning-border)', borderRadius: 8 }}>
|
<div style={{ marginBottom: 24, padding: 16, background: token.colorWarningBg, border: `1px solid ${token.colorWarningBorder}`, borderRadius: 8 }}>
|
||||||
<Typography.Text type="warning" strong>
|
<Typography.Text type="warning" strong>
|
||||||
⚠️ 注意:点击"确认替换"将会用新内容替换当前的世界观设定
|
⚠️ 注意:点击"确认替换"将会用新内容替换当前的世界观设定
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 24 }}>
|
<div style={{ marginBottom: 24 }}>
|
||||||
<Title level={5} style={{ color: 'var(--color-primary)', marginBottom: 12 }}>
|
<Title level={5} style={{ color: token.colorPrimary, marginBottom: 12 }}>
|
||||||
时间设定
|
时间设定
|
||||||
</Title>
|
</Title>
|
||||||
<Paragraph style={{
|
<Paragraph style={{
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
lineHeight: 1.8,
|
lineHeight: 1.8,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
background: '#f5f5f5',
|
background: token.colorBgLayout,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
borderLeft: '4px solid #1890ff'
|
borderLeft: `4px solid ${token.colorPrimary}`
|
||||||
}}>
|
}}>
|
||||||
{newWorldData.time_period}
|
{newWorldData.time_period}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 24 }}>
|
<div style={{ marginBottom: 24 }}>
|
||||||
<Title level={5} style={{ color: '#52c41a', marginBottom: 12 }}>
|
<Title level={5} style={{ color: token.colorSuccess, marginBottom: 12 }}>
|
||||||
地点设定
|
地点设定
|
||||||
</Title>
|
</Title>
|
||||||
<Paragraph style={{
|
<Paragraph style={{
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
lineHeight: 1.8,
|
lineHeight: 1.8,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
background: '#f5f5f5',
|
background: token.colorBgLayout,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
borderLeft: '4px solid #52c41a'
|
borderLeft: `4px solid ${token.colorSuccess}`
|
||||||
}}>
|
}}>
|
||||||
{newWorldData.location}
|
{newWorldData.location}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 24 }}>
|
<div style={{ marginBottom: 24 }}>
|
||||||
<Title level={5} style={{ color: '#faad14', marginBottom: 12 }}>
|
<Title level={5} style={{ color: token.colorWarning, marginBottom: 12 }}>
|
||||||
氛围设定
|
氛围设定
|
||||||
</Title>
|
</Title>
|
||||||
<Paragraph style={{
|
<Paragraph style={{
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
lineHeight: 1.8,
|
lineHeight: 1.8,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
background: '#f5f5f5',
|
background: token.colorBgLayout,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
borderLeft: '4px solid #faad14'
|
borderLeft: `4px solid ${token.colorWarning}`
|
||||||
}}>
|
}}>
|
||||||
{newWorldData.atmosphere}
|
{newWorldData.atmosphere}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 0 }}>
|
<div style={{ marginBottom: 0 }}>
|
||||||
<Title level={5} style={{ color: '#f5222d', marginBottom: 12 }}>
|
<Title level={5} style={{ color: token.colorError, marginBottom: 12 }}>
|
||||||
规则设定
|
规则设定
|
||||||
</Title>
|
</Title>
|
||||||
<Paragraph style={{
|
<Paragraph style={{
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
lineHeight: 1.8,
|
lineHeight: 1.8,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
background: '#f5f5f5',
|
background: token.colorBgLayout,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
borderLeft: '4px solid #f5222d'
|
borderLeft: `4px solid ${token.colorError}`
|
||||||
}}>
|
}}>
|
||||||
{newWorldData.rules}
|
{newWorldData.rules}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
|
theme,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
@@ -38,6 +39,8 @@ export default function WritingStyles() {
|
|||||||
const [createForm] = Form.useForm();
|
const [createForm] = Form.useForm();
|
||||||
const [editForm] = Form.useForm();
|
const [editForm] = Form.useForm();
|
||||||
|
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
const isMobile = window.innerWidth <= 768;
|
const isMobile = window.innerWidth <= 768;
|
||||||
|
|
||||||
// 卡片网格配置
|
// 卡片网格配置
|
||||||
@@ -170,10 +173,10 @@ export default function WritingStyles() {
|
|||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: token.colorBgContainer,
|
||||||
padding: isMobile ? '12px 0' : '16px 0',
|
padding: isMobile ? '12px 0' : '16px 0',
|
||||||
marginBottom: isMobile ? 12 : 16,
|
marginBottom: isMobile ? 12 : 16,
|
||||||
borderBottom: '1px solid #f0f0f0',
|
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -220,7 +223,7 @@ export default function WritingStyles() {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
border: style.is_default ? '2px solid #1890ff' : '1px solid #f0f0f0',
|
border: style.is_default ? `2px solid ${token.colorPrimary}` : `1px solid ${token.colorBorderSecondary}`,
|
||||||
}}
|
}}
|
||||||
bodyStyle={{
|
bodyStyle={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -235,7 +238,7 @@ export default function WritingStyles() {
|
|||||||
style={{ cursor: style.is_default ? 'default' : 'pointer' }}
|
style={{ cursor: style.is_default ? 'default' : 'pointer' }}
|
||||||
>
|
>
|
||||||
{style.is_default ? (
|
{style.is_default ? (
|
||||||
<StarFilled style={{ color: '#faad14', fontSize: 18 }} />
|
<StarFilled style={{ color: token.colorWarning, fontSize: 18 }} />
|
||||||
) : (
|
) : (
|
||||||
<StarOutlined style={{ fontSize: 18 }} />
|
<StarOutlined style={{ fontSize: 18 }} />
|
||||||
)}
|
)}
|
||||||
@@ -246,7 +249,7 @@ export default function WritingStyles() {
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
cursor: style.user_id === null ? 'not-allowed' : 'pointer',
|
cursor: style.user_id === null ? 'not-allowed' : 'pointer',
|
||||||
color: style.user_id === null ? '#ccc' : undefined
|
color: style.user_id === null ? token.colorTextQuaternary : undefined
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
@@ -261,7 +264,7 @@ export default function WritingStyles() {
|
|||||||
<DeleteOutlined
|
<DeleteOutlined
|
||||||
style={{
|
style={{
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
color: style.user_id === null ? '#ccc' : undefined,
|
color: style.user_id === null ? token.colorTextQuaternary : undefined,
|
||||||
cursor: style.user_id === null ? 'not-allowed' : 'pointer'
|
cursor: style.user_id === null ? 'not-allowed' : 'pointer'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -292,7 +295,7 @@ export default function WritingStyles() {
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
backgroundColor: '#fafafa',
|
backgroundColor: token.colorFillAlter,
|
||||||
padding: 8,
|
padding: 8,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user