完成世界书、骰子、apiconfig页面处理

This commit is contained in:
2026-04-30 01:35:10 +08:00
parent a3e3711b2b
commit ba9b925c32
4602 changed files with 785225 additions and 23 deletions

View 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;
}

View 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;

View File

@@ -0,0 +1 @@
export { default } from './TopBar';

View File

@@ -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;
}

View File

@@ -0,0 +1 @@
export { default } from './CurrentUserRole';

View File

@@ -0,0 +1,4 @@
/* Theme Toggle - Inherits from action-btn */
.theme-toggle {
/* All styles inherited from .action-btn */
}

View File

@@ -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;

View File

@@ -0,0 +1 @@
export { default } from './ThemeToggle';