style: 将Tooltip组件替换为原生title属性,统一提示样式

This commit is contained in:
xiamuceer
2026-01-01 17:32:15 +08:00
parent 0ffa0ec4b5
commit fe22881194
19 changed files with 993 additions and 431 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "1.2.3",
"version": "1.2.4",
"type": "module",
"scripts": {
"dev": "vite",
Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

+1 -1
View File
@@ -50,7 +50,7 @@ function App() {
<Route path="/user-management" element={<ProtectedRoute><UserManagement /></ProtectedRoute>} />
<Route path="/chapters/:chapterId/reader" element={<ProtectedRoute><ChapterReader /></ProtectedRoute>} />
<Route path="/project/:projectId" element={<ProtectedRoute><ProjectDetail /></ProtectedRoute>}>
<Route index element={<Navigate to="world-setting" replace />} />
<Route index element={<Navigate to="sponsor" replace />} />
<Route path="world-setting" element={<WorldSetting />} />
<Route path="careers" element={<Careers />} />
<Route path="outline" element={<Outline />} />
+41 -101
View File
@@ -1,5 +1,4 @@
import React, { useMemo, useEffect, useRef } from 'react';
import { Tooltip } from 'antd';
// 标注数据类型
export interface MemoryAnnotation {
@@ -219,113 +218,54 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
const icon = TYPE_ICONS[annotation.type];
const isActive = activeAnnotationId === annotation.id;
// 🔧 工具提示内容:如果有多个标注,显示所有标注信息
const tooltipContent = (
<div style={{ maxWidth: 350 }}>
{annotations && annotations.length > 1 ? (
// 多个标注
<div>
<div style={{ fontWeight: 'bold', marginBottom: 8, borderBottom: '1px solid rgba(255,255,255,0.3)', paddingBottom: 4 }}>
📍 {annotations.length}
</div>
{annotations.map((ann, idx) => (
<div key={ann.id} style={{
marginBottom: idx < annotations.length - 1 ? 8 : 0,
paddingBottom: idx < annotations.length - 1 ? 8 : 0,
borderBottom: idx < annotations.length - 1 ? '1px solid rgba(255,255,255,0.1)' : 'none'
}}>
<div style={{ fontWeight: 'bold', marginBottom: 4, fontSize: 13 }}>
{TYPE_ICONS[ann.type]} {ann.title}
</div>
<div style={{ fontSize: 11, opacity: 0.9 }}>
{ann.content.slice(0, 80)}
{ann.content.length > 80 ? '...' : ''}
</div>
<div style={{ marginTop: 4, fontSize: 10, opacity: 0.7 }}>
: {(ann.importance * 10).toFixed(1)}/10
</div>
</div>
))}
</div>
) : (
// 单个标注
<div>
<div style={{ fontWeight: 'bold', marginBottom: 4 }}>
{icon} {annotation.title}
</div>
<div style={{ fontSize: 12, opacity: 0.9 }}>
{annotation.content.slice(0, 100)}
{annotation.content.length > 100 ? '...' : ''}
</div>
<div style={{ marginTop: 8, fontSize: 11, opacity: 0.7 }}>
: {(annotation.importance * 10).toFixed(1)}/10
</div>
{annotation.tags && annotation.tags.length > 0 && (
<div style={{ marginTop: 4, fontSize: 11 }}>
{annotation.tags.map((tag, i) => (
<span
key={i}
style={{
display: 'inline-block',
background: 'rgba(255,255,255,0.2)',
padding: '2px 6px',
borderRadius: 3,
marginRight: 4,
}}
>
{tag}
</span>
))}
</div>
)}
</div>
)}
</div>
);
// 简化工具提示内容,不再使用复杂的React元素,改为纯文本或移除Tooltip
const tooltipText = annotations && annotations.length > 1
? `此处有 ${annotations.length} 个标注`
: `${annotation.title}: ${annotation.content.slice(0, 100)}${annotation.content.length > 100 ? '...' : ''}`;
return (
<Tooltip key={index} title={tooltipContent} placement="top">
<span
key={index}
title={tooltipText}
ref={(el) => {
if (annotation) {
annotationRefs.current[annotation.id] = el;
}
}}
data-annotation-id={annotation?.id}
className={`annotated-text ${isActive ? 'active' : ''}`}
style={{
position: 'relative',
borderBottom: `2px solid ${color}`,
cursor: 'pointer',
backgroundColor: isActive ? `${color}22` : 'transparent',
transition: 'all 0.2s',
padding: '2px 0',
}}
onClick={() => onAnnotationClick?.(annotation)}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = `${color}33`;
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = isActive
? `${color}22`
: 'transparent';
}}
>
{segment.content}
<span
ref={(el) => {
if (annotation) {
annotationRefs.current[annotation.id] = el;
}
}}
data-annotation-id={annotation?.id}
className={`annotated-text ${isActive ? 'active' : ''}`}
style={{
position: 'relative',
borderBottom: `2px solid ${color}`,
cursor: 'pointer',
backgroundColor: isActive ? `${color}22` : 'transparent',
transition: 'all 0.2s',
padding: '2px 0',
}}
onClick={() => onAnnotationClick?.(annotation)}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = `${color}33`;
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = isActive
? `${color}22`
: 'transparent';
position: 'absolute',
top: -20,
left: '50%',
transform: 'translateX(-50%)',
fontSize: 14,
pointerEvents: 'none',
}}
>
{segment.content}
<span
style={{
position: 'absolute',
top: -20,
left: '50%',
transform: 'translateX(-50%)',
fontSize: 14,
pointerEvents: 'none',
}}
>
{icon}
</span>
{icon}
</span>
</Tooltip>
</span>
);
};
+43 -45
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Typography, Space, Divider, Badge, Tooltip, Button } from 'antd';
import { Typography, Space, Divider, Badge, Button } from 'antd';
import { GithubOutlined, CopyrightOutlined, HeartFilled, ClockCircleOutlined, GiftOutlined } from '@ant-design/icons';
import { VERSION_INFO, getVersionString } from '../config/version';
import { checkLatestVersion } from '../services/versionService';
@@ -70,22 +70,21 @@ export default function AppFooter() {
flexWrap: 'wrap'
}}>
<Badge dot={hasUpdate} offset={[-8, 2]}>
<Tooltip title={hasUpdate ? `发现新版本 v${latestVersion},点击查看` : '当前版本'}>
<Text
onClick={handleVersionClick}
style={{
fontSize: 11,
display: 'flex',
alignItems: 'center',
gap: 4,
color: 'var(--color-primary)',
cursor: hasUpdate ? 'pointer' : 'default',
}}
>
<strong style={{ color: 'var(--color-text-primary)' }}>{VERSION_INFO.projectName}</strong>
<span>{getVersionString()}</span>
</Text>
</Tooltip>
<Text
onClick={handleVersionClick}
style={{
fontSize: 11,
display: 'flex',
alignItems: 'center',
gap: 4,
color: 'var(--color-primary)',
cursor: hasUpdate ? 'pointer' : 'default',
}}
title={hasUpdate ? `发现新版本 v${latestVersion},点击查看` : '当前版本'}
>
<strong style={{ color: 'var(--color-text-primary)' }}>{VERSION_INFO.projectName}</strong>
<span>{getVersionString()}</span>
</Text>
</Badge>
<Divider type="vertical" style={{ margin: '0 4px', borderColor: 'var(--color-border)' }} />
<Button
@@ -144,34 +143,33 @@ export default function AppFooter() {
>
{/* 版本信息 */}
<Badge dot={hasUpdate} offset={[-8, 2]}>
<Tooltip title={hasUpdate ? `发现新版本 v${latestVersion},点击查看` : '当前版本'}>
<Text
onClick={handleVersionClick}
style={{
fontSize: 12,
display: 'flex',
alignItems: 'center',
gap: 6,
color: 'var(--color-text-secondary)',
textShadow: 'none',
cursor: hasUpdate ? 'pointer' : 'default',
transition: 'all 0.3s',
}}
onMouseEnter={(e) => {
if (hasUpdate) {
e.currentTarget.style.transform = 'scale(1.05)';
}
}}
onMouseLeave={(e) => {
if (hasUpdate) {
e.currentTarget.style.transform = 'scale(1)';
}
}}
>
<strong style={{ color: 'var(--color-text-primary)' }}>{VERSION_INFO.projectName}</strong>
<span>{getVersionString()}</span>
</Text>
</Tooltip>
<Text
onClick={handleVersionClick}
style={{
fontSize: 12,
display: 'flex',
alignItems: 'center',
gap: 6,
color: 'var(--color-text-secondary)',
textShadow: 'none',
cursor: hasUpdate ? 'pointer' : 'default',
transition: 'all 0.3s',
}}
onMouseEnter={(e) => {
if (hasUpdate) {
e.currentTarget.style.transform = 'scale(1.05)';
}
}}
onMouseLeave={(e) => {
if (hasUpdate) {
e.currentTarget.style.transform = 'scale(1)';
}
}}
title={hasUpdate ? `发现新版本 v${latestVersion},点击查看` : '当前版本'}
>
<strong style={{ color: 'var(--color-text-primary)' }}>{VERSION_INFO.projectName}</strong>
<span>{getVersionString()}</span>
</Text>
</Badge>
{/* GitHub 链接 */}
+9 -10
View File
@@ -1,4 +1,4 @@
import { Modal, Timeline, Tag, Avatar, Empty, Spin, Button, Space, Tooltip } from 'antd';
import { Modal, Timeline, Tag, Avatar, Empty, Spin, Button, Space } from 'antd';
import { useState, useEffect } from 'react';
import {
BugOutlined,
@@ -130,15 +130,14 @@ export default function ChangelogModal({ visible, onClose }: ChangelogModalProps
<Space>
<GithubOutlined />
<span></span>
<Tooltip title="刷新">
<Button
type="text"
size="small"
icon={<ReloadOutlined />}
onClick={handleRefresh}
loading={loading}
/>
</Tooltip>
<Button
type="text"
size="small"
icon={<ReloadOutlined />}
onClick={handleRefresh}
loading={loading}
title="刷新"
/>
</Space>
}
open={visible}
+465
View File
@@ -0,0 +1,465 @@
/**
* 章节阅读器组件
* 提供沉浸式阅读体验,支持主题切换、字体调节、翻页导航等功能
*/
import { useState, useEffect, useCallback } from 'react';
import { Modal, Button, Slider, Radio, Space, Typography, Spin, message } from 'antd';
import {
LeftOutlined,
RightOutlined,
SettingOutlined,
FontSizeOutlined,
BgColorsOutlined,
CloseOutlined,
ColumnHeightOutlined
} from '@ant-design/icons';
import type { Chapter } from '../types';
// 阅读器设置接口
interface ReaderSettings {
fontSize: number; // 字体大小
theme: 'light' | 'sepia' | 'dark'; // 主题模式
lineHeight: number; // 行高
}
// 组件属性接口
interface ChapterReaderProps {
visible: boolean; // 是否显示
chapter: Chapter; // 当前章节
onClose: () => void; // 关闭回调
onChapterChange: (chapterId: string) => void; // 章节切换回调
}
// 导航信息接口
interface NavigationInfo {
previous: { id: string; chapter_number: number; title: string } | null;
next: { id: string; chapter_number: number; title: string } | null;
current: { id: string; chapter_number: number; title: string };
}
// 主题样式配置
const themeStyles = {
light: {
bg: '#ffffff',
text: '#333333',
headerBg: '#fafafa',
border: '#e8e8e8'
},
sepia: {
bg: '#f5e6c8',
text: '#5b4636',
headerBg: '#e8d9b8',
border: '#d4c5a5'
},
dark: {
bg: '#1a1a1a',
text: '#cccccc',
headerBg: '#252525',
border: '#333333'
}
};
// 本地存储key
const SETTINGS_STORAGE_KEY = 'chapter-reader-settings';
// 从本地存储加载设置
const loadSettings = (): ReaderSettings => {
try {
const saved = localStorage.getItem(SETTINGS_STORAGE_KEY);
if (saved) {
return JSON.parse(saved);
}
} catch (e) {
console.warn('加载阅读器设置失败:', e);
}
return {
fontSize: 18,
theme: 'light',
lineHeight: 1.8
};
};
// 保存设置到本地存储
const saveSettings = (settings: ReaderSettings) => {
try {
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
} catch (e) {
console.warn('保存阅读器设置失败:', e);
}
};
export default function ChapterReader({
visible,
chapter,
onClose,
onChapterChange
}: ChapterReaderProps) {
// 阅读器设置
const [settings, setSettings] = useState<ReaderSettings>(loadSettings);
// 导航信息
const [navigation, setNavigation] = useState<NavigationInfo | null>(null);
// 加载状态
const [loading, setLoading] = useState(false);
// 设置面板显示状态
const [showSettings, setShowSettings] = useState(false);
// 移动端检测
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
// 响应式检测
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// 获取章节导航信息
useEffect(() => {
if (visible && chapter?.id) {
setLoading(true);
fetch(`/api/chapters/${chapter.id}/navigation`)
.then(res => {
if (!res.ok) throw new Error('获取导航失败');
return res.json();
})
.then(data => {
setNavigation(data);
setLoading(false);
})
.catch(err => {
console.error('获取导航信息失败:', err);
message.error('获取章节导航信息失败');
setLoading(false);
});
}
}, [visible, chapter?.id]);
// 保存设置变更
useEffect(() => {
saveSettings(settings);
}, [settings]);
// 上一章
const handlePrevious = useCallback(() => {
if (navigation?.previous) {
setLoading(true);
onChapterChange(navigation.previous.id);
}
}, [navigation?.previous, onChapterChange]);
// 下一章
const handleNext = useCallback(() => {
if (navigation?.next) {
setLoading(true);
onChapterChange(navigation.next.id);
}
}, [navigation?.next, onChapterChange]);
// 键盘快捷键
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!visible) return;
// 忽略输入框中的按键
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
switch (e.key) {
case 'ArrowLeft':
handlePrevious();
break;
case 'ArrowRight':
handleNext();
break;
case 'Escape':
onClose();
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [visible, handlePrevious, handleNext, onClose]);
// 章节变化后自动回到顶部
useEffect(() => {
if (chapter?.id) {
setLoading(false);
// 找到滚动容器并滚动到顶部
const scrollContainer = document.querySelector('.reader-scroll-container');
if (scrollContainer) {
scrollContainer.scrollTop = 0;
}
}
}, [chapter?.id]);
// 当前主题样式
const currentTheme = themeStyles[settings.theme];
// 更新设置的便捷函数
const updateSettings = (key: keyof ReaderSettings, value: number | string) => {
setSettings(prev => ({ ...prev, [key]: value }));
};
return (
<Modal
open={visible}
onCancel={onClose}
footer={null}
width="100%"
style={{
maxWidth: '100vw',
top: 0,
margin: 0,
padding: 0,
height: '100vh',
overflow: 'hidden'
}}
styles={{
content: {
height: '100vh',
borderRadius: 0,
boxShadow: 'none',
padding: 0,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden'
},
body: {
flex: 1,
padding: 0,
background: currentTheme.bg,
overflow: 'hidden',
height: '100%',
scrollbarWidth: 'thin',
display: 'flex',
flexDirection: 'column'
}
}}
closable={false}
maskClosable={false}
>
{/* 顶部工具栏 */}
<div style={{
flex: 'none',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: isMobile ? '10px 12px' : '12px 20px',
borderBottom: `1px solid ${currentTheme.border}`,
background: currentTheme.headerBg,
zIndex: 10
}}>
<Button
type="text"
icon={<CloseOutlined />}
onClick={onClose}
style={{ color: currentTheme.text }}
>
{!isMobile && '关闭'}
</Button>
<Typography.Title
level={5}
style={{
margin: 0,
color: currentTheme.text,
maxWidth: isMobile ? '60%' : '70%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontSize: isMobile ? 14 : 16
}}
>
{chapter.chapter_number}{chapter.title}
</Typography.Title>
<Button
type={showSettings ? 'primary' : 'text'}
icon={<SettingOutlined />}
onClick={() => setShowSettings(!showSettings)}
style={{ color: showSettings ? undefined : currentTheme.text }}
title="阅读设置"
/>
</div>
{/* 设置面板 */}
{showSettings && (
<div style={{
padding: isMobile ? '12px 16px' : '16px 24px',
borderBottom: `1px solid ${currentTheme.border}`,
background: currentTheme.headerBg
}}>
<Space
direction={isMobile ? 'vertical' : 'horizontal'}
size="large"
style={{ width: '100%' }}
wrap
>
{/* 字体大小 */}
<div style={{ minWidth: isMobile ? '100%' : 200 }}>
<Space style={{ marginBottom: 8, color: currentTheme.text }}>
<FontSizeOutlined />
<span>: {settings.fontSize}px</span>
</Space>
<Slider
min={14}
max={28}
value={settings.fontSize}
onChange={v => updateSettings('fontSize', v)}
style={{ margin: '8px 0' }}
/>
</div>
{/* 行高 */}
<div style={{ minWidth: isMobile ? '100%' : 200 }}>
<Space style={{ marginBottom: 8, color: currentTheme.text }}>
<ColumnHeightOutlined />
<span>: {settings.lineHeight}</span>
</Space>
<Slider
min={1.4}
max={2.5}
step={0.1}
value={settings.lineHeight}
onChange={v => updateSettings('lineHeight', v)}
style={{ margin: '8px 0' }}
/>
</div>
{/* 主题 */}
<div>
<Space style={{ marginBottom: 8, color: currentTheme.text }}>
<BgColorsOutlined />
<span></span>
</Space>
<div>
<Radio.Group
value={settings.theme}
onChange={e => updateSettings('theme', e.target.value)}
buttonStyle="solid"
size={isMobile ? 'small' : 'middle'}
>
<Radio.Button value="light"></Radio.Button>
<Radio.Button value="sepia"></Radio.Button>
<Radio.Button value="dark"></Radio.Button>
</Radio.Group>
</div>
</div>
</Space>
</div>
)}
{/* 章节内容区域 */}
<div
className="reader-scroll-container"
style={{
flex: 1,
overflowY: 'auto',
position: 'relative',
scrollBehavior: 'smooth'
}}
>
<Spin spinning={loading} tip="加载中...">
<div
style={{
maxWidth: 1000,
margin: '0 auto',
padding: isMobile ? '24px 16px 40px' : '40px 60px 40px',
minHeight: '100%',
fontSize: settings.fontSize,
lineHeight: settings.lineHeight,
color: currentTheme.text,
whiteSpace: 'pre-wrap',
textAlign: 'justify',
wordBreak: 'break-word',
overflowWrap: 'break-word'
}}
>
{chapter.content ? (
// 按段落渲染内容,优化阅读体验
chapter.content.split('\n').map((paragraph, index) => (
paragraph.trim() ? (
<p
key={index}
style={{
textIndent: '2em',
margin: 0,
marginBottom: '0.8em'
}}
>
{paragraph}
</p>
) : (
<br key={index} />
)
))
) : (
<div style={{
textAlign: 'center',
padding: '60px 20px',
color: currentTheme.text,
opacity: 0.6
}}>
</div>
)}
</div>
</Spin>
</div>
{/* 底部导航栏 */}
<div style={{
flex: 'none',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: isMobile ? '12px 16px' : '16px 24px',
borderTop: `1px solid ${currentTheme.border}`,
background: currentTheme.headerBg,
zIndex: 100
}}>
<Button
type="primary"
icon={<LeftOutlined />}
disabled={!navigation?.previous || loading}
onClick={handlePrevious}
size={isMobile ? 'middle' : 'large'}
>
{!isMobile && '上一章'}
</Button>
<div style={{
textAlign: 'center',
color: currentTheme.text,
fontSize: isMobile ? 12 : 14
}}>
<div>{chapter.word_count || 0} </div>
{navigation && (
<div style={{ fontSize: isMobile ? 10 : 12, opacity: 0.7 }}>
{navigation.previous ? `${navigation.previous.title}` : '已是第一章'}
{' | '}
{navigation.next ? `${navigation.next.title}` : '已是最后一章'}
</div>
)}
</div>
<Button
type="primary"
disabled={!navigation?.next || loading}
onClick={handleNext}
size={isMobile ? 'middle' : 'large'}
>
{!isMobile && '下一章'}
<RightOutlined />
</Button>
</div>
</Modal>
);
}
+40 -48
View File
@@ -12,7 +12,6 @@ import {
Select,
message,
Tag,
Tooltip,
Spin,
Empty,
Alert,
@@ -307,9 +306,7 @@ export default function MCPPluginsPage() {
return <Tag color="success" icon={<CheckCircleOutlined />}></Tag>;
case 'error':
return (
<Tooltip title={plugin.last_error}>
<Tag color="error" icon={<CloseCircleOutlined />}></Tag>
</Tooltip>
<Tag color="error" icon={<CloseCircleOutlined />} title={plugin.last_error}></Tag>
);
default:
return <Tag color="default"></Tag>;
@@ -553,50 +550,45 @@ export default function MCPPluginsPage() {
</div>
<Space size="small" wrap>
<Tooltip title={plugin.enabled ? '禁用插件' : '启用插件'}>
<Switch
checked={plugin.enabled}
onChange={(checked) => handleToggle(plugin, checked)}
size={isMobile ? 'small' : 'default'}
style={{
flexShrink: 0,
height: isMobile ? 16 : 22,
minHeight: isMobile ? 16 : 22,
lineHeight: isMobile ? '16px' : '22px'
}}
/>
</Tooltip>
<Tooltip title="测试连接">
<Button
icon={<ThunderboltOutlined />}
onClick={() => handleTest(plugin.id)}
loading={testingPluginId === plugin.id}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
<Tooltip title="查看工具">
<Button
icon={<ToolOutlined />}
onClick={() => handleViewTools(plugin.id)}
disabled={!plugin.enabled || plugin.status !== 'active'}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
<Tooltip title="编辑">
<Button
icon={<EditOutlined />}
onClick={() => handleEdit(plugin)}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
<Tooltip title="删除">
<Button
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(plugin)}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
<Switch
title={plugin.enabled ? '禁用插件' : '启用插件'}
checked={plugin.enabled}
onChange={(checked) => handleToggle(plugin, checked)}
size={isMobile ? 'small' : 'default'}
style={{
flexShrink: 0,
height: isMobile ? 16 : 22,
minHeight: isMobile ? 16 : 22,
lineHeight: isMobile ? '16px' : '22px'
}}
/>
<Button
title="测试连接"
icon={<ThunderboltOutlined />}
onClick={() => handleTest(plugin.id)}
loading={testingPluginId === plugin.id}
size={isMobile ? 'small' : 'middle'}
/>
<Button
title="查看工具"
icon={<ToolOutlined />}
onClick={() => handleViewTools(plugin.id)}
disabled={!plugin.enabled || plugin.status !== 'active'}
size={isMobile ? 'small' : 'middle'}
/>
<Button
title="编辑"
icon={<EditOutlined />}
onClick={() => handleEdit(plugin)}
size={isMobile ? 'small' : 'middle'}
/>
<Button
title="删除"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(plugin)}
size={isMobile ? 'small' : 'middle'}
/>
</Space>
</div>
</Card>
+28 -30
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Button, List, Modal, Form, Input, message, Empty, Space, Popconfirm, Card, Select, Radio, Tag, InputNumber, Tooltip, Tabs } from 'antd';
import { Button, List, Modal, Form, Input, message, Empty, Space, Popconfirm, Card, Select, Radio, Tag, InputNumber, Tabs } from 'antd';
import { EditOutlined, DeleteOutlined, ThunderboltOutlined, BranchesOutlined, AppstoreAddOutlined, CheckCircleOutlined, ExclamationCircleOutlined, PlusOutlined, FileTextOutlined } from '@ant-design/icons';
import { useStore } from '../store';
import { useOutlineSync } from '../store/hooks';
@@ -1932,16 +1932,15 @@ export default function Outline() {
{isMobile ? 'AI生成/续写' : 'AI生成/续写大纲'}
</Button>
{outlines.length > 0 && currentProject?.outline_mode === 'one-to-many' && (
<Tooltip title="将所有大纲展开为多章,实现从大纲到章节的一对多关系">
<Button
icon={<AppstoreAddOutlined />}
onClick={handleBatchExpandOutlines}
loading={isExpanding}
disabled={isGenerating}
>
{isMobile ? '批量展开' : '批量展开为多章'}
</Button>
</Tooltip>
<Button
icon={<AppstoreAddOutlined />}
onClick={handleBatchExpandOutlines}
loading={isExpanding}
disabled={isGenerating}
title="将所有大纲展开为多章,实现从大纲到章节的一对多关系"
>
{isMobile ? '批量展开' : '批量展开为多章'}
</Button>
)}
</Space>
</div>
@@ -1965,16 +1964,16 @@ export default function Outline() {
}}
actions={isMobile ? undefined : [
...(currentProject?.outline_mode === 'one-to-many' ? [
<Tooltip title="展开为多章">
<Button
type="text"
icon={<BranchesOutlined />}
onClick={() => handleExpandOutline(item.id, item.title)}
loading={isExpanding}
>
</Button>
</Tooltip>
<Button
key="expand"
type="text"
icon={<BranchesOutlined />}
onClick={() => handleExpandOutline(item.id, item.title)}
loading={isExpanding}
title="展开为多章"
>
</Button>
] : []), // 一对一模式:不显示任何展开/创建按钮
<Button
type="text"
@@ -2034,15 +2033,14 @@ export default function Outline() {
/>
{/* 一对多模式:显示展开按钮 */}
{currentProject?.outline_mode === 'one-to-many' && (
<Tooltip title="展开为多章">
<Button
type="text"
icon={<BranchesOutlined />}
onClick={() => handleExpandOutline(item.id, item.title)}
loading={isExpanding}
size="small"
/>
</Tooltip>
<Button
type="text"
icon={<BranchesOutlined />}
onClick={() => handleExpandOutline(item.id, item.title)}
loading={isExpanding}
size="small"
title="展开为多章"
/>
)}
{/* 一对一模式:不显示任何展开/创建按钮 */}
<Popconfirm
+6 -6
View File
@@ -95,6 +95,11 @@ export default function ProjectDetail() {
// Hook 内部已经更新了 store,不需要再次刷新
const menuItems = [
{
key: 'sponsor',
icon: <HeartOutlined />,
label: <Link to={`/project/${projectId}/sponsor`}></Link>,
},
{
key: 'world-setting',
icon: <GlobalOutlined />,
@@ -145,11 +150,6 @@ export default function ProjectDetail() {
// icon: <ToolOutlined />,
// label: <Link to={`/project/${projectId}/polish`}>AI去味</Link>,
// },
{
key: 'sponsor',
icon: <HeartOutlined />,
label: <Link to={`/project/${projectId}/sponsor`}></Link>,
},
];
// 根据当前路径动态确定选中的菜单项
@@ -166,7 +166,7 @@ export default function ProjectDetail() {
if (path.includes('/writing-styles')) return 'writing-styles';
if (path.includes('/sponsor')) return 'sponsor';
// if (path.includes('/polish')) return 'polish';
return 'world-setting'; // 默认选中世界设定
return 'sponsor'; // 默认选中赞助支持
}, [location.pathname]);
if (loading || !currentProject) {
+37 -39
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, Button, Empty, Modal, message, Spin, Row, Col, Statistic, Space, Tag, Progress, Typography, Tooltip, Badge, Alert, Upload, Checkbox, Divider, Switch, Dropdown, Form, Input, InputNumber } from 'antd';
import { Card, Button, Empty, Modal, message, Spin, Row, Col, Statistic, Space, Tag, Progress, Typography, Badge, Alert, Upload, Checkbox, Divider, Switch, Dropdown, Form, Input, InputNumber } from 'antd';
import { EditOutlined, DeleteOutlined, BookOutlined, RocketOutlined, CalendarOutlined, FileTextOutlined, TrophyOutlined, FireOutlined, SettingOutlined, InfoCircleOutlined, CloseOutlined, UploadOutlined, DownloadOutlined, ApiOutlined, MoreOutlined, BulbOutlined, LoadingOutlined, FileSearchOutlined } from '@ant-design/icons';
import { projectApi } from '../services/api';
import { useStore } from '../store';
@@ -1031,38 +1031,34 @@ export default function ProjectList() {
{formatDate(project.updated_at)}
</Text>
<Space size={4}>
<Tooltip title="编辑">
<Button
type="text"
size="small"
icon={<EditOutlined />}
onClick={(e) => {
e.stopPropagation();
handleEditProject(project);
}}
style={{
borderRadius: 8,
color: 'var(--color-primary)',
transition: 'all 0.2s ease'
}}
/>
</Tooltip>
<Tooltip title="删除">
<Button
type="text"
size="small"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
handleDelete(project.id);
}}
style={{
borderRadius: 8,
transition: 'all 0.2s ease'
}}
/>
</Tooltip>
<Button
type="text"
size="small"
icon={<EditOutlined />}
onClick={(e) => {
e.stopPropagation();
handleEditProject(project);
}}
style={{
borderRadius: 8,
color: 'var(--color-primary)',
transition: 'all 0.2s ease'
}}
/>
<Button
type="text"
size="small"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
handleDelete(project.id);
}}
style={{
borderRadius: 8,
transition: 'all 0.2s ease'
}}
/>
</Space>
</div>
</div>
@@ -1249,9 +1245,10 @@ export default function ProjectList() {
}}
/>
<Text style={{ fontSize: window.innerWidth <= 768 ? 13 : 14 }}></Text>
<Tooltip title="导出项目关联的写作风格数据">
<InfoCircleOutlined style={{ color: '#999', fontSize: window.innerWidth <= 768 ? 12 : 14 }} />
</Tooltip>
<InfoCircleOutlined
title="导出项目关联的写作风格数据"
style={{ color: '#999', fontSize: window.innerWidth <= 768 ? 12 : 14 }}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
@@ -1266,9 +1263,10 @@ export default function ProjectList() {
}}
/>
<Text style={{ fontSize: window.innerWidth <= 768 ? 13 : 14 }}></Text>
<Tooltip title="导出AI生成的历史记录(最多100条)">
<InfoCircleOutlined style={{ color: '#999', fontSize: window.innerWidth <= 768 ? 12 : 14 }} />
</Tooltip>
<InfoCircleOutlined
title="导出AI生成的历史记录(最多100条)"
style={{ color: '#999', fontSize: window.innerWidth <= 768 ? 12 : 14 }}
/>
</div>
</Space>
</Card>
+38 -32
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, Form, Input, Button, Select, Slider, InputNumber, message, Space, Typography, Spin, Modal, Tooltip, 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 } from 'antd';
import { SettingOutlined, SaveOutlined, DeleteOutlined, ReloadOutlined, ArrowLeftOutlined, InfoCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined, PlusOutlined, EditOutlined, CopyOutlined } from '@ant-design/icons';
import { settingsApi } from '../services/api';
import type { SettingsUpdate, APIKeyPreset, PresetCreateRequest, APIKeyPresetConfig } from '../types';
@@ -544,16 +544,15 @@ export default function SettingsPage() {
</Button>
),
<Tooltip title="测试连接">
<Button
type="link"
icon={<ThunderboltOutlined />}
loading={testingPresetId === preset.id}
onClick={() => handlePresetTest(preset.id)}
>
</Button>
</Tooltip>,
<Button
key="test"
type="link"
icon={<ThunderboltOutlined />}
loading={testingPresetId === preset.id}
onClick={() => handlePresetTest(preset.id)}
>
</Button>,
<Button
type="link"
icon={<EditOutlined />}
@@ -766,9 +765,10 @@ export default function SettingsPage() {
label={
<Space size={4}>
<span>API </span>
<Tooltip title="选择你的AI服务提供商">
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
</Tooltip>
<InfoCircleOutlined
title="选择你的AI服务提供商"
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
/>
</Space>
}
name="api_provider"
@@ -787,9 +787,10 @@ export default function SettingsPage() {
label={
<Space size={4}>
<span>API </span>
<Tooltip title="你的API密钥,将加密存储">
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
</Tooltip>
<InfoCircleOutlined
title="你的API密钥,将加密存储"
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
/>
</Space>
}
name="api_key"
@@ -806,9 +807,10 @@ export default function SettingsPage() {
label={
<Space size={4}>
<span>API </span>
<Tooltip title="API的基础URL地址">
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
</Tooltip>
<InfoCircleOutlined
title="API的基础URL地址"
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
/>
</Space>
}
name="api_base_url"
@@ -827,9 +829,10 @@ export default function SettingsPage() {
label={
<Space size={4}>
<span></span>
<Tooltip title="AI模型的名称,如 gpt-4, gpt-3.5-turbo">
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
</Tooltip>
<InfoCircleOutlined
title="AI模型的名称,如 gpt-4, gpt-3.5-turbo"
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
/>
</Space>
}
name="llm_model"
@@ -931,9 +934,10 @@ export default function SettingsPage() {
label={
<Space size={4}>
<span></span>
<Tooltip title="控制输出的随机性,值越高越随机(0.0-2.0)">
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
</Tooltip>
<InfoCircleOutlined
title="控制输出的随机性,值越高越随机(0.0-2.0)"
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
/>
</Space>
}
name="temperature"
@@ -955,9 +959,10 @@ export default function SettingsPage() {
label={
<Space size={4}>
<span> Token </span>
<Tooltip title="单次请求的最大token数量">
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
</Tooltip>
<InfoCircleOutlined
title="单次请求的最大token数量"
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
/>
</Space>
}
name="max_tokens"
@@ -978,9 +983,10 @@ export default function SettingsPage() {
label={
<Space size={4}>
<span></span>
<Tooltip title="设置全局系统提示词,每次AI调用时都会自动使用。可用于设定AI的角色、语言风格等">
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
</Tooltip>
<InfoCircleOutlined
title="设置全局系统提示词,每次AI调用时都会自动使用。可用于设定AI的角色、语言风格等"
style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }}
/>
</Space>
}
name="system_prompt"
+2 -2
View File
@@ -21,9 +21,9 @@ interface SponsorOption {
const sponsorOptions: SponsorOption[] = [
{ amount: 5, label: '🌶️ 一包辣条', image: '/5.png', description: '¥5' },
{ amount: 10, label: '🍱 一顿拼好饭', image: '/10.png', description: '¥10' },
{ amount: 20, label: '🧋 一杯奶茶', image: '/20.png', description: '¥20' },
{ amount: 20, label: '🧋 一杯咖啡', image: '/20.png', description: '¥20' },
{ amount: 50, label: '🍖 一次烧烤', image: '/50.png', description: '¥50' },
{ amount: 'custom', label: '💰 任意金额', image: '/xx.png', description: '自定义' },
{ amount: 99, label: '🍲 一顿海底捞', image: '/99.png', description: '¥99' },
];
const benefits = [
+32 -41
View File
@@ -15,7 +15,6 @@ import {
Typography,
Badge,
InputNumber,
Tooltip,
Row,
Col,
Pagination,
@@ -350,27 +349,23 @@ export default function UserManagement() {
// 桌面端:保持原有按钮样式
return (
<Space size="small">
<Tooltip title="编辑用户">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
>
</Button>
</Tooltip>
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
>
</Button>
<Tooltip title="重置密码">
<Button
type="link"
size="small"
icon={<KeyOutlined />}
onClick={() => handleResetPassword(record)}
>
</Button>
</Tooltip>
<Button
type="link"
size="small"
icon={<KeyOutlined />}
onClick={() => handleResetPassword(record)}
>
</Button>
<Popconfirm
title={`确定${isActive ? '禁用' : '启用'}该用户吗?`}
@@ -378,16 +373,14 @@ export default function UserManagement() {
okText="确定"
cancelText="取消"
>
<Tooltip title={isActive ? '禁用用户' : '启用用户'}>
<Button
type="link"
size="small"
danger={isActive}
icon={isActive ? <StopOutlined /> : <CheckCircleOutlined />}
>
{isActive ? '禁用' : '启用'}
</Button>
</Tooltip>
<Button
type="link"
size="small"
danger={isActive}
icon={isActive ? <StopOutlined /> : <CheckCircleOutlined />}
>
{isActive ? '禁用' : '启用'}
</Button>
</Popconfirm>
{!record.is_admin && (
@@ -398,16 +391,14 @@ export default function UserManagement() {
cancelText="取消"
okButtonProps={{ danger: true }}
>
<Tooltip title="删除用户">
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
>
</Button>
</Tooltip>
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
>
</Button>
</Popconfirm>
)}
</Space>
+28 -37
View File
@@ -12,8 +12,7 @@ import {
Empty,
Typography,
Row,
Col,
Tooltip
Col
} from 'antd';
import {
PlusOutlined,
@@ -232,28 +231,26 @@ export default function WritingStyles() {
padding: '16px',
}}
actions={[
<Tooltip key="default" title={style.is_default ? '当前默认' : '设为默认'}>
<span
onClick={() => !style.is_default && handleSetDefault(style.id)}
style={{ cursor: style.is_default ? 'default' : 'pointer' }}
>
{style.is_default ? (
<StarFilled style={{ color: '#faad14', fontSize: 18 }} />
) : (
<StarOutlined style={{ fontSize: 18 }} />
)}
</span>
</Tooltip>,
<Tooltip key="edit" title={style.user_id === null ? '预设风格不可编辑' : '编辑'}>
<EditOutlined
onClick={() => style.user_id !== null && handleEdit(style)}
style={{
fontSize: 18,
cursor: style.user_id === null ? 'not-allowed' : 'pointer',
color: style.user_id === null ? '#ccc' : undefined
}}
/>
</Tooltip>,
<span
key="default"
onClick={() => !style.is_default && handleSetDefault(style.id)}
style={{ cursor: style.is_default ? 'default' : 'pointer' }}
>
{style.is_default ? (
<StarFilled style={{ color: '#faad14', fontSize: 18 }} />
) : (
<StarOutlined style={{ fontSize: 18 }} />
)}
</span>,
<EditOutlined
key="edit"
onClick={() => style.user_id !== null && handleEdit(style)}
style={{
fontSize: 18,
cursor: style.user_id === null ? 'not-allowed' : 'pointer',
color: style.user_id === null ? '#ccc' : undefined
}}
/>,
<Popconfirm
key="delete"
title="确定删除这个风格吗?"
@@ -263,19 +260,13 @@ export default function WritingStyles() {
cancelText="取消"
disabled={style.user_id === null}
>
<Tooltip title={
style.user_id === null
? '预设风格不可删除'
: '删除'
}>
<DeleteOutlined
style={{
fontSize: 18,
color: style.user_id === null ? '#ccc' : undefined,
cursor: style.user_id === null ? 'not-allowed' : 'pointer'
}}
/>
</Tooltip>
<DeleteOutlined
style={{
fontSize: 18,
color: style.user_id === null ? '#ccc' : undefined,
cursor: style.user_id === null ? 'not-allowed' : 'pointer'
}}
/>
</Popconfirm>,
]}
>