读取本地chatandrole完成并优化排列

This commit is contained in:
2026-03-19 23:01:38 +08:00
parent 91d11abe90
commit a371039ee6
7 changed files with 302 additions and 214 deletions

View File

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import Toolbar from './components/Toolbar/Toolbar';
import Toolbar from './components/ToolBar/ToolBar';
import ChatBox from './components/ChatBox/ChatBox';
import DicePanel from './components/DicePanel/DicePanel';
import ImageDisplay from './components/ImageDisplay/ImageDisplay';
@@ -7,7 +7,6 @@ import PresetPanel from './components/PresetPanel/PresetPanel';
import './index.css';
function App() {
const [isToolbarExpanded, setIsToolbarExpanded] = useState(false);
const [selectedRole, setSelectedRole] = useState(null);
const [selectedChat, setSelectedChat] = useState(null);
@@ -24,11 +23,8 @@ function App() {
return (
<div className="app">
<Toolbar
isExpanded={isToolbarExpanded}
onToggle={() => setIsToolbarExpanded(!isToolbarExpanded)}
onRoleChange={handleRoleChange}
onChatChange={handleChatChange}
selectedRole={selectedRole}
/>
{/* 主内容容器 */}

View File

@@ -2,18 +2,19 @@
/* React 组件根容器 */
.chat-box {
height: 95%;
height: 100%; /* 修改为100%,填满父容器 */
width: 100%;
display: flex;
flex-direction: column;
background-color: #fafafa;
overflow: hidden; /* 防止内容溢出 */
}
/* 消息列表容器 */
.chat-messages {
flex: 1;
min-height: 0;
max-height: 100%;
max-height: calc(95vh - 62px); /* 减去输入区域的高度 */
overflow-y: auto;
padding: 20px;
padding-top: 60px;
@@ -147,7 +148,7 @@
color: #333;
}
/* 消息工具栏 */
/* 消息工具栏 - 优化布局使图标更紧凑 */
.message-toolbar {
display: flex;
align-items: center;
@@ -172,7 +173,7 @@
.toolbar-buttons {
display: flex;
gap: 5px;
gap: 3px; /* 减少图标之间的间距 */
margin: 0 5px;
}
@@ -355,13 +356,17 @@
background-color: #fff;
border-top: 1px solid #ddd;
padding: 10px 20px;
flex: 0 0 auto;
height: 62px; /* 明确设置高度 */
flex-shrink: 0; /* 防止被压缩 */
display: flex;
align-items: flex-end;
gap: 10px;
width: 100%;
position: relative;
z-index: 10;
}
.chat-input-area {
flex: 1;
display: flex;

View File

@@ -1,80 +1,69 @@
/* ==================== 顶部工具栏区域 ==================== */
.toolbar {
position: absolute;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 50px;
background-color: #fff;
border-bottom: 1px solid #ddd;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
display: flex;
align-items: center;
justify-content: flex-end;
z-index: 100;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
transition: height 0.3s ease;
padding: 0 20px;
justify-content: flex-start; /* 确保内容从左到右排列 */
transition: all 0.3s ease;
}
.toolbar.expanded {
height: auto;
max-height: 300px;
overflow-y: auto;
padding-bottom: 10px;
/* 工具栏图标容器 */
.toolbar-icons {
display: flex;
align-items: center;
gap: 15px;
margin-right: auto; /* 添加这行,确保图标靠左 */
}
.toolbar-content {
display: none;
width: 100%;
padding: 10px 20px;
}
.toolbar.expanded .toolbar-content {
display: block;
}
.toolbar-toggle-btn {
position: absolute;
right: 0;
top: 0;
height: 50px;
width: 50px;
background: none;
border: none;
border-left: 1px solid #ddd;
cursor: pointer;
font-size: 0.8rem;
color: #666;
/* 工具栏图标 */
.toolbar-icon {
width: 36px;
height: 36px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
z-index: 101;
cursor: pointer;
transition: all 0.2s ease;
font-size: 18px;
color: #555;
background-color: #f5f5f5;
}
.toolbar-toggle-btn:hover {
background-color: #f5f5f5;
.toolbar-icon:hover {
background-color: #e6f7ff;
color: #1890ff;
transform: translateY(-2px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
/* ==================== 下拉框样式 ==================== */
/* 工具栏下拉框容器 */
.toolbar-dropdown {
margin-bottom: 15px;
margin-bottom: 10px;
}
.toolbar-dropdown label {
display: block;
margin-bottom: 5px;
font-size: 14px;
font-size: 13px;
color: #333;
font-weight: 500;
}
/* 下拉框容器 */
.dropdown-container {
position: relative;
width: 100%;
max-width: 300px;
}
/* 下拉框触发器 */
@@ -83,16 +72,28 @@
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-size: 13px;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.dropdown-trigger:hover {
border-color: #1890ff;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
.dropdown-arrow {
margin-left: 8px;
font-size: 12px;
font-size: 10px;
transition: transform 0.2s ease;
}
.dropdown-trigger.open .dropdown-arrow {
transform: rotate(180deg);
}
/* 下拉菜单 */
@@ -100,23 +101,36 @@
position: absolute;
top: 100%;
left: 0;
width: 100%;
min-width: 180px;
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 10;
max-height: 200px;
overflow-y: auto;
margin-top: 5px;
border: 1px solid #e0e0e0;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 1001;
margin-top: 8px;
padding: 5px 0;
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.2s ease, transform 0.2s ease;
pointer-events: none;
}
.dropdown-menu.visible {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
/* 下拉菜单项 */
.dropdown-item {
padding: 8px 12px;
padding: 10px 15px;
cursor: pointer;
position: relative;
font-size: 14px;
transition: background-color 0.2s ease;
display: flex;
justify-content: space-between;
align-items: center;
}
.dropdown-item:hover {
@@ -126,6 +140,7 @@
.dropdown-item.selected {
background-color: #e6f7ff;
color: #1890ff;
font-weight: 500;
}
/* 嵌套下拉框 */
@@ -133,16 +148,60 @@
position: absolute;
left: 100%;
top: 0;
width: 100%;
min-width: 180px;
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 11;
max-height: 200px;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 1002;
margin-left: 5px;
padding: 5px 0;
opacity: 0;
transform: translateX(-10px);
transition: opacity 0.2s ease, transform 0.2s ease;
pointer-events: none;
/* 移除滚动条相关设置,让下拉框根据内容自动扩展 */
}
.nested-dropdown .dropdown-item {
padding-left: 20px;
.nested-dropdown.visible {
opacity: 1;
transform: translateX(0);
pointer-events: auto;
}
/* 添加指示箭头 */
.dropdown-item.has-children::after {
content: '';
font-size: 18px;
color: #999;
margin-left: 8px;
font-weight: 300;
}
.dropdown-item.selected.has-children::after {
color: #1890ff;
}
/* 主内容区域 */
.main-container {
margin-top: 50px; /* 为固定工具栏留出空间 */
height: calc(100vh - 50px);
display: flex;
overflow: hidden;
}
/* 加载动画 */
.loading-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(24, 144, 255, 0.2);
border-radius: 50%;
border-top-color: #1890ff;
animation: spin 1s ease-in-out infinite;
margin-right: 8px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}

View File

@@ -1,9 +1,7 @@
import React, { useState } from 'react';
import './Toolbar.css';
import React, { useState, useEffect, useRef } from 'react';
import './ToolBar.css';
const Toolbar = ({
isExpanded,
onToggle,
onRoleChange,
onChatChange
}) => {
@@ -11,107 +9,175 @@ const Toolbar = ({
const [selectedChat, setSelectedChat] = useState(null);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [roleData, setRoleData] = useState({});
const [hoveredRole, setHoveredRole] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const dropdownRef = useRef(null);
// 获取角色和聊天数据
const fetchRoleData = async () => {
console.log('开始获取角色数据...');
try {
const response = await fetch('/api/tool_bar/get_all_role_and_chat', {
method: 'GET',
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Expires': '0'
const fetchRoleData = async () => {
setIsLoading(true);
console.log('开始获取角色数据...');
try {
const response = await fetch('/api/tool_bar/get_all_role_and_chat', {
method: 'GET',
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Expires': '0'
}
});
console.log('响应状态:', response.status);
const data = await response.json();
console.log('获取到的数据:', data);
setRoleData(data);
} catch (error) {
console.error('获取角色数据失败:', error);
} finally {
setIsLoading(false);
}
};
// 点击外部关闭下拉菜单
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsDropdownOpen(false);
setHoveredRole(null);
}
});
console.log('响应状态:', response.status);
const data = await response.json();
console.log('获取到的数据:', data);
setRoleData(data);
} catch (error) {
console.error('获取角色数据失败:', error);
}
};
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// 处理下拉框展开/收起
const handleDropdownToggle = async () => {
await fetchRoleData();
setIsDropdownOpen(!isDropdownOpen);
};
const handleDropdownToggle = () => {
// 每次点击都重新获取数据
fetchRoleData();
setIsDropdownOpen(!isDropdownOpen);
};
// 处理角色选择
const handleRoleSelect = (role) => {
setSelectedRole(role);
setSelectedChat(null);
if (onRoleChange) {
onRoleChange(role);
}
// 如果该角色有聊天记录,默认选择第一个
if (roleData[role] && roleData[role].length > 0) {
const firstChat = roleData[role][0];
setSelectedChat(firstChat);
if (onChatChange) {
onChatChange(role, firstChat);
}
} else {
setSelectedChat(null);
}
};
// 处理聊天选择
const handleChatSelect = (chat) => {
setSelectedChat(chat);
setIsDropdownOpen(false);
setHoveredRole(null);
if (onChatChange) {
onChatChange(selectedRole, chat);
}
};
// 处理角色项悬停
const handleRoleHover = (role) => {
setHoveredRole(role);
};
// 处理角色项离开
const handleRoleLeave = () => {
// 不立即清除hoveredRole以便用户可以移动到二级菜单
};
// 处理二级菜单进入
const handleNestedMenuEnter = () => {
// 防止鼠标移动时菜单消失
};
// 处理二级菜单离开
const handleNestedMenuLeave = () => {
setHoveredRole(null);
};
return (
<div className={`toolbar ${isExpanded ? 'expanded' : ''}`}>
<button
className="toolbar-toggle-btn"
onClick={onToggle}
>
{isExpanded ? '▼' : '▲'}
</button>
<div className="toolbar">
<div className="toolbar-icons">
{/* Logo图标 */}
<div className="toolbar-icon" title="首页">
🤖
</div>
{isExpanded && (
<div className="toolbar-content">
<div className="toolbar-dropdown">
<label>选择角色</label>
<div className="dropdown-container">
<div
className="dropdown-trigger"
onClick={handleDropdownToggle}
>
{selectedRole || '选择角色'}
<span className="dropdown-arrow"></span>
</div>
{/* 角色选择下拉框 */}
<div className="dropdown-container" ref={dropdownRef}>
<div
className="toolbar-icon"
onClick={handleDropdownToggle}
title="选择角色"
>
👤
</div>
{isDropdownOpen && (
<div className="dropdown-menu">
{Object.keys(roleData).map(role => (
<div
key={role}
className={`dropdown-item ${selectedRole === role ? 'selected' : ''}`}
onClick={() => handleRoleSelect(role)}
>
{role}
{selectedRole === role && (
<div className="nested-dropdown">
{roleData[role].map(chat => (
<div
key={chat}
className={`dropdown-item ${selectedChat === chat ? 'selected' : ''}`}
onClick={(e) => {
e.stopPropagation();
handleChatSelect(chat);
}}
>
{chat}
</div>
))}
</div>
)}
</div>
))}
{isDropdownOpen && (
<div className="dropdown-menu visible">
{isLoading ? (
<div className="dropdown-item">
加载中...
</div>
) : (
Object.keys(roleData).map(role => (
<div
key={role}
className={`dropdown-item ${selectedRole === role ? 'selected' : ''} ${roleData[role] && roleData[role].length > 0 ? 'has-children' : ''}`}
onClick={() => handleRoleSelect(role)}
onMouseEnter={() => handleRoleHover(role)}
onMouseLeave={handleRoleLeave}
>
{role}
{/* 当悬停在角色上时显示二级菜单 */}
{hoveredRole === role && roleData[role] && roleData[role].length > 0 && (
<div
className={`nested-dropdown visible`}
onMouseEnter={handleNestedMenuEnter}
onMouseLeave={handleNestedMenuLeave}
>
{roleData[role].map(chat => (
<div
key={chat}
className={`dropdown-item ${selectedChat === chat ? 'selected' : ''}`}
onClick={(e) => {
e.stopPropagation();
handleChatSelect(chat);
}}
>
{chat}
</div>
))}
</div>
)}
</div>
))
)}
</div>
</div>
)}
</div>
)}
{/* 其他工具栏图标 */}
<div className="toolbar-icon" title="设置">
</div>
<div className="toolbar-icon" title="帮助">
</div>
</div>
</div>
);
};

View File

@@ -1,99 +1,61 @@
/* ==================== 全局样式 ==================== */
/* 全局重置与基础设置 */
/* 重置样式 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #root {
html, body, .app {
height: 100%;
width: 100%;
overflow: hidden; /* 禁止全局滚动 */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f5f5f5;
color: #333;
overflow: hidden; /* 防止出现滚动 */
}
/* 布局容器 */
#root {
.app {
display: flex;
flex-direction: column;
position: relative;
height: 100%;
width: 100%;
}
/* 滚动条美化 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #aaa;
}
/* ==================== 主内容区域 ==================== */
/* 主内容区域 */
.main-container {
position: absolute;
top: 50px;
bottom: 0;
left: 0;
right: 0;
height: 100%;
width: 100%;
flex: 1; /* 这会让主容器占据剩余的所有空间 */
display: flex;
overflow: hidden;
overflow: hidden; /* 防止内容溢出 */
height: 100%; /* 确保高度填满 */
}
/* 左侧栏 */
.sidebar-left {
width: 20%;
background-color: #fff;
border-right: 1px solid #ddd;
overflow-y: auto;
height: 100%;
width: 250px; /* 或者你想要的宽度 */
height: ; /* 确保高度填满 */
overflow-y: auto; /* 内容过多时显示滚动条 */
}
/* 中间栏:聊天框 */
/* 中间聊天区域 */
.chat-area {
flex: 3;
background-color: #fafafa;
height: 100%;
flex: 1; /* 占据剩余空间 */
height: 100%; /* 确保高度填满 */
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
overflow: hidden; /* 防止内容溢出 */
}
/* 右侧栏 */
.sidebar-right {
width: 20%;
background-color: #fff;
border-left: 1px solid #ddd;
width: 300px; /* 或者你想要的宽度 */
height: 100%; /* 确保高度填满 */
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
/* 右侧上下分割 */
.right-top, .right-bottom {
flex: 1;
overflow-y: auto;
padding: 10px;
min-height: 0;
}
/* 右侧栏顶部 */
.right-top {
border-bottom: 1px solid #ddd;
flex: 1; /* 占据剩余空间 */
overflow-y: auto;
}
/* 右侧栏底部 */
.right-bottom {
height: 200px; /* 或者你想要的高度 */
overflow-y: auto;
}