完成世界书、骰子、apiconfig页面处理
This commit is contained in:
259
frontend/src/components/TopBar/TopBar.css
Normal file
259
frontend/src/components/TopBar/TopBar.css
Normal file
@@ -0,0 +1,259 @@
|
||||
/* ==================== TopBar - Reference Design ==================== */
|
||||
|
||||
.top-bar {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--color-bg-secondary);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
box-shadow: var(--shadow-sm);
|
||||
z-index: 100; /* 降低层级,不遮挡悬浮提示 */
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.top-bar-content {
|
||||
width: 100%;
|
||||
padding: 0 var(--spacing-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* Status Section - Left side */
|
||||
.status-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
flex: 1;
|
||||
justify-content: flex-start;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Actions Section - Right side */
|
||||
.actions-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Status Badge Styles - Minimalist */
|
||||
.status-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all var(--transition-fast);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.status-badge:hover {
|
||||
background-color: var(--color-bg-tertiary);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 400;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* World Info badge - allow more space */
|
||||
.world-info-badge {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.world-info-badge .status-label {
|
||||
max-width: 330px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Action Button Styles - Badge-like (Similar to status-badge) */
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background-color: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all var(--transition-fast);
|
||||
cursor: pointer;
|
||||
font-size: 1.4rem; /* 更大的图标 */
|
||||
line-height: 1;
|
||||
min-height: 36px; /* 与 status-badge 一致 */
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background-color: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
background-color: var(--color-accent-ultra-light);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.action-btn svg {
|
||||
display: none; /* Hide SVG, use text instead */
|
||||
}
|
||||
|
||||
/* ==================== 弹出面板通用样式 ==================== */
|
||||
|
||||
.close-panel-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-muted);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
margin-left: auto;
|
||||
transition: all var(--transition-fast);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.close-panel-button:hover {
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-tertiary);
|
||||
}
|
||||
|
||||
.panel-overlay {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: var(--z-modal-backdrop);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: var(--spacing-xl);
|
||||
animation: fadeIn var(--transition-normal);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
max-height: calc(100vh - 80px);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: slideDown var(--transition-smooth);
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--gradient-subtle);
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: var(--spacing-lg);
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
background-color: var(--color-bg-primary);
|
||||
}
|
||||
|
||||
/* TopBar 全局世界书列表 */
|
||||
.global-books-list-topbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.global-book-item-topbar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 14px;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.08) 100%);
|
||||
border: 1px solid rgba(102, 126, 234, 0.2);
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.global-book-item-topbar:hover {
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.12) 100%);
|
||||
border-color: rgba(102, 126, 234, 0.3);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
.global-book-name-topbar {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-global-books-topbar {
|
||||
text-align: center;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 14px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 主内容区域 */
|
||||
.main-container {
|
||||
margin-top: 50px;
|
||||
height: calc(100vh - 50px);
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
237
frontend/src/components/TopBar/TopBar.jsx
Normal file
237
frontend/src/components/TopBar/TopBar.jsx
Normal file
@@ -0,0 +1,237 @@
|
||||
// frontend-react/src/components/TopBar/TopBar.jsx
|
||||
import React, {useState, useRef, useEffect} from 'react';
|
||||
import './TopBar.css';
|
||||
import ThemeToggle from './items/ThemeToggle';
|
||||
import useWorldBookStore from '../../Store/SideBarLeft/WorldBookSlice';
|
||||
import useApiConfigStore from '../../Store/SideBarLeft/ApiConfigSlice';
|
||||
|
||||
const Toolbar = () => {
|
||||
const [activePanel, setActivePanel] = useState(null);
|
||||
const panelRef = useRef(null);
|
||||
const [currentUserRole, setCurrentUserRole] = useState({ name: '', description: '' });
|
||||
|
||||
// 从 Store 获取全局世界书
|
||||
const { globalWorldBooks } = useWorldBookStore();
|
||||
|
||||
// 从 ApiConfigStore 获取核心和辅助 API 配置
|
||||
const { activeMap, fetchProfile } = useApiConfigStore();
|
||||
const [coreModel, setCoreModel] = useState('未设置');
|
||||
const [assistModel, setAssistModel] = useState('未设置');
|
||||
|
||||
// 加载核心和辅助 API 配置的模型名
|
||||
useEffect(() => {
|
||||
const loadApiModels = async () => {
|
||||
// 加载核心配置
|
||||
if (activeMap['core']) {
|
||||
try {
|
||||
const profile = await fetchProfile(activeMap['core']);
|
||||
const coreApi = profile?.apis?.find(api => api.category === 'core');
|
||||
setCoreModel(coreApi?.model || '未设置');
|
||||
} catch (e) {
|
||||
console.error('加载核心配置失败:', e);
|
||||
setCoreModel('未设置');
|
||||
}
|
||||
} else {
|
||||
setCoreModel('未设置');
|
||||
}
|
||||
|
||||
// 加载辅助配置
|
||||
if (activeMap['assist']) {
|
||||
try {
|
||||
const profile = await fetchProfile(activeMap['assist']);
|
||||
const assistApi = profile?.apis?.find(api => api.category === 'assist');
|
||||
setAssistModel(assistApi?.model || '未设置');
|
||||
} catch (e) {
|
||||
console.error('加载辅助配置失败:', e);
|
||||
setAssistModel('未设置');
|
||||
}
|
||||
} else {
|
||||
setAssistModel('未设置');
|
||||
}
|
||||
};
|
||||
|
||||
loadApiModels();
|
||||
}, [activeMap, fetchProfile]);
|
||||
|
||||
// 点击外部关闭面板
|
||||
React.useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (panelRef.current && !panelRef.current.contains(event.target)) {
|
||||
setActivePanel(null);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 加载当前用户角色
|
||||
useEffect(() => {
|
||||
const savedRole = localStorage.getItem('currentUserRole');
|
||||
if (savedRole) {
|
||||
try {
|
||||
setCurrentUserRole(JSON.parse(savedRole));
|
||||
} catch (e) {
|
||||
console.error('解析用户角色失败:', e);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 处理面板切换
|
||||
const handlePanelToggle = (panelName) => {
|
||||
if (activePanel === panelName) {
|
||||
setActivePanel(null);
|
||||
} else {
|
||||
setActivePanel(panelName);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭面板
|
||||
const handleClosePanel = () => {
|
||||
setActivePanel(null);
|
||||
};
|
||||
|
||||
// 截断文本
|
||||
const truncateText = (text, maxLength = 20) => {
|
||||
if (!text) return '未选择';
|
||||
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="top-bar">
|
||||
<div className="top-bar-content">
|
||||
{/* 左侧:状态徽章区域 */}
|
||||
<div className="status-section">
|
||||
{/* 当前玩家角色 */}
|
||||
<div
|
||||
className="status-badge"
|
||||
title="当前玩家角色"
|
||||
onClick={() => handlePanelToggle('currentRole')}
|
||||
>
|
||||
<span className="status-icon">😊</span>
|
||||
<span className="status-label">{truncateText(currentUserRole.name || '未设置', 15)}</span>
|
||||
</div>
|
||||
|
||||
{/* 核心配置 */}
|
||||
<div className="status-badge" title={`核心配置: ${coreModel}`}>
|
||||
<span className="status-icon">🔌</span>
|
||||
<span className="status-label">{truncateText(coreModel, 12)}</span>
|
||||
</div>
|
||||
|
||||
{/* 辅助配置 */}
|
||||
<div className="status-badge" title={`辅助配置: ${assistModel}`}>
|
||||
<span className="status-icon">🔗</span>
|
||||
<span className="status-label">{truncateText(assistModel, 12)}</span>
|
||||
</div>
|
||||
|
||||
{/* 当前预设 */}
|
||||
<div className="status-badge" title="当前预设名">
|
||||
<span className="status-icon">⚙️</span>
|
||||
<span className="status-label">默认预设</span>
|
||||
</div>
|
||||
|
||||
{/* 激活的世界书 */}
|
||||
<div className="status-badge world-info-badge" title="已激活全局世界书" onClick={() => handlePanelToggle('worldBook')}>
|
||||
<span className="status-icon">📚</span>
|
||||
<span className="status-label">
|
||||
{globalWorldBooks.length > 0
|
||||
? globalWorldBooks.map(wb => wb.name).join(', ')
|
||||
: '无'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧:操作按钮区域 */}
|
||||
<div className="actions-section">
|
||||
{/* 设置按钮 */}
|
||||
<button
|
||||
className="action-btn"
|
||||
title="设置"
|
||||
onClick={() => handlePanelToggle('settings')}
|
||||
>
|
||||
⚙
|
||||
</button>
|
||||
|
||||
{/* 扩展按钮 */}
|
||||
<button
|
||||
className="action-btn"
|
||||
title="扩展"
|
||||
onClick={() => handlePanelToggle('extensions')}
|
||||
>
|
||||
⊞
|
||||
</button>
|
||||
|
||||
{/* 主题切换 */}
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 全局世界书面板 */}
|
||||
{activePanel === 'worldBook' && (
|
||||
<div className="panel-overlay" ref={panelRef}>
|
||||
<div className="panel-content">
|
||||
<div className="panel-header">
|
||||
<h3>全局世界书 ({globalWorldBooks.length})</h3>
|
||||
<button className="close-panel-button" onClick={handleClosePanel} title="关闭">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
{globalWorldBooks.length > 0 ? (
|
||||
<div className="global-books-list-topbar">
|
||||
{globalWorldBooks.map(book => (
|
||||
<div key={book.name} className="global-book-item-topbar">
|
||||
<span className="global-book-name-topbar">{book.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="no-global-books-topbar">暂无全局世界书</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 设置面板 */}
|
||||
{activePanel === 'settings' && (
|
||||
<div className="panel-overlay" ref={panelRef}>
|
||||
<div className="panel-content">
|
||||
<div className="panel-header">
|
||||
<h3>系统设置</h3>
|
||||
<button className="close-panel-button" onClick={handleClosePanel} title="关闭">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<p>系统设置内容...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 拓展面板 */}
|
||||
{activePanel === 'extensions' && (
|
||||
<div className="panel-overlay" ref={panelRef}>
|
||||
<div className="panel-content">
|
||||
<div className="panel-header">
|
||||
<h3>功能拓展</h3>
|
||||
<button className="close-panel-button" onClick={handleClosePanel} title="关闭">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<p>功能拓展内容...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toolbar;
|
||||
1
frontend/src/components/TopBar/index.js
Normal file
1
frontend/src/components/TopBar/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './TopBar';
|
||||
@@ -0,0 +1,80 @@
|
||||
.current-user-role {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.role-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
background-color: #2a2a2a;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #6c63ff;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.role-preview {
|
||||
margin-top: 8px;
|
||||
padding: 12px;
|
||||
background-color: #2a2a2a;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.preview-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #6c63ff;
|
||||
}
|
||||
|
||||
.preview-description {
|
||||
font-size: 14px;
|
||||
color: #cccccc;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './CurrentUserRole';
|
||||
@@ -0,0 +1,4 @@
|
||||
/* Theme Toggle - Inherits from action-btn */
|
||||
.theme-toggle {
|
||||
/* All styles inherited from .action-btn */
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './ThemeToggle.css';
|
||||
|
||||
const ThemeToggle = () => {
|
||||
const [theme, setTheme] = useState(() => {
|
||||
// 从 localStorage 读取主题,默认为 dark
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
return savedTheme || 'dark';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// 应用主题到 document
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
// 保存到 localStorage
|
||||
localStorage.setItem('theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prev => prev === 'dark' ? 'light' : 'dark');
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className="action-btn theme-toggle"
|
||||
onClick={toggleTheme}
|
||||
title={theme === 'light' ? '切换到夜间模式' : '切换到白天模式'}
|
||||
>
|
||||
{theme === 'light' ? '☾' : '☀'}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeToggle;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ThemeToggle';
|
||||
Reference in New Issue
Block a user