swipe、右键菜单

This commit is contained in:
2026-05-05 21:51:34 +08:00
parent 44df56c8d2
commit f843a74715
19 changed files with 1319 additions and 57 deletions

View File

@@ -128,7 +128,7 @@ const useChatBoxStore = create(
},
// 发送消息
sendMessage: async (content) => {
sendMessage: async (content, targetFloor = null) => {
const { messages, userName, characterName, currentRole, currentChat, options, wsConnection } = get();
// ✅ 如果启用了自动掷骰子,处理内容
@@ -194,15 +194,22 @@ const useChatBoxStore = create(
wsConnection.close();
}
// 计算下一个楼层号
const nextFloor = get().getNextFloor(messages);
// ✅ 判断是重roll还是新消息现在重roll也创建新消息
const isReroll = targetFloor !== null;
let userFloor, assistantFloor, nextFloor;
// ✅ 重roll模式也创建新消息而不是更新现有消息
nextFloor = get().getNextFloor(messages);
userFloor = nextFloor;
assistantFloor = nextFloor + 1;
set({
isGenerating: true,
wsConnection: null, // 重置连接
messages: [...messages, {
id: Date.now(),
floor: nextFloor,
id: `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, // ✅ 使用唯一ID
floor: userFloor,
mes: processedContent, // 使用处理后的内容
is_user: true,
name: userName || 'User',
@@ -235,9 +242,11 @@ const useChatBoxStore = create(
// 保存WebSocket连接到store
set({ wsConnection: ws });
// 添加一个空的助手消息,稍后会更新
const newMessageId = Date.now();
const assistantFloor = nextFloor + 1; // 助手消息的楼层是用户消息楼层+1
// ✅ 根据模式处理 AI 消息
let newMessageId;
// ✅ 正常模式:添加一个空的助手消息,稍后会更新
newMessageId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // ✅ 使用唯一ID
set((state) => ({
messages: [...state.messages, {
id: newMessageId,
@@ -246,7 +255,8 @@ const useChatBoxStore = create(
is_user: false,
name: characterName || 'Assistant',
sendDate: new Date().toISOString()
}]
}],
isGenerating: true
}));
let assistantMessage = '';
@@ -284,6 +294,8 @@ const useChatBoxStore = create(
// 处理流式数据块
assistantMessage += data.content;
// ✅ 更新新创建的消息
set((state) => ({
messages: state.messages.map((msg) =>
msg.id === newMessageId ? { ...msg, mes: assistantMessage } : msg
@@ -345,6 +357,7 @@ const useChatBoxStore = create(
console.log('\n[WebSocket] ✅ 收到完成信号');
console.log(' - 总 Chunks:', chunkCount);
console.log(' - 消息长度:', assistantMessage.length);
// 完成响应
isStreamComplete = true;
ws.close();
@@ -382,7 +395,10 @@ const useChatBoxStore = create(
// 发送请求到WebSocket确保连接已建立
const sendAfterConnect = () => {
console.log('[WebSocket] 📤 sendAfterConnect 被调用, readyState:', ws.readyState);
if (ws.readyState === WebSocket.OPEN) {
console.log('[WebSocket] ✅ 连接已打开,准备发送消息');
// ✅ 获取 API 配置
const apiConfigData = {
api_url: apiConfigStore.currentProfile?.apis?.mainLLM?.apiUrl || '',
@@ -393,6 +409,7 @@ const useChatBoxStore = create(
console.log('\n' + '-'.repeat(80));
console.log('[WebSocket] 📤 发送消息:');
console.log(' - Floor:', nextFloor);
console.log(' - Mode:', isReroll ? '🔄 Reroll (新消息)' : ' New Message');
console.log(' - Role:', currentRole);
console.log(' - Chat:', actualChat);
console.log(' - Stream:', options.streamOutput);
@@ -401,8 +418,9 @@ const useChatBoxStore = create(
console.log(' - Current Profile:', apiConfigStore.currentProfile);
console.log('-'.repeat(80) + '\n');
ws.send(JSON.stringify({
floor: nextFloor,
// ✅ 实际发送消息
const messageData = JSON.stringify({
floor: nextFloor, // ✅ 总是使用 nextFloor因为重roll也创建新消息
mes: processedContent, // 使用处理后的内容
is_user: true,
currentRole: currentRole,
@@ -452,7 +470,11 @@ const useChatBoxStore = create(
// ✅ 时间戳(用于冲突解决)
timestamp: Date.now(),
stream: options.streamOutput
}));
});
console.log('[WebSocket] 📤 正在发送消息,数据长度:', messageData.length);
ws.send(messageData);
console.log('[WebSocket] ✅ 消息已发送');
} else if (ws.readyState === WebSocket.CONNECTING) {
// 如果正在连接,继续等待
console.log('[WebSocket] 等待连接...', { readyState: ws.readyState });

View File

@@ -138,6 +138,7 @@
.message-content {
position: relative;
min-height: calc(1lh + var(--spacing-xs) * 2); /* ✅ 确保编辑时保持高度 */
}
.bubble {
@@ -171,22 +172,24 @@
flex-direction: column;
gap: var(--spacing-md);
width: 100%;
min-height: calc(1lh + var(--spacing-xs) * 2); /* ✅ 至少保持一行文本的高度 */
}
.edit-textarea {
width: 100%;
min-height: auto; /* 自适应内容高度 */
min-height: calc(1lh + var(--spacing-xs) * 2); /* ✅ 与 bubble 的 padding 保持一致 */
max-height: none; /* 不限制最大高度 */
padding: var(--spacing-xs) 0; /* 与 bubble 保持一致 */
border: none; /* 去掉边框 */
border-radius: 0;
border: 2px solid var(--color-accent); /* ✅ 添加边框提示编辑状态 */
border-radius: var(--radius-sm);
resize: vertical;
font-family: inherit;
font-size: 1rem;
line-height: 1.7;
background-color: transparent; /* 透明背景 */
background-color: rgba(102, 126, 234, 0.05); /* ✅ 轻微背景色区分编辑状态 */
color: var(--color-text-primary);
outline: none;
box-sizing: border-box; /* ✅ 确保宽度计算正确 */
}
.edit-textarea:focus {
@@ -234,33 +237,65 @@
.swipe-controls {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
justify-content: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
justify-content: space-between; /* ✅ 左右分布 */
padding: 0 var(--spacing-xs); /* ✅ 减少内边距 */
width: 100%; /* ✅ 占满宽度 */
position: relative;
}
.swipe-button {
background: none;
border: 1px solid #ddd;
border-radius: 4px;
background: rgba(255, 255, 255, 0.15); /* ✅ 半透明背景 */
backdrop-filter: blur(8px); /* ✅ 毛玻璃效果 */
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: var(--radius-sm);
cursor: pointer;
padding: 2px 8px;
font-size: 0.8rem;
transition: background-color 0.2s;
padding: 6px 12px;
font-size: 0.9rem;
color: var(--color-text-secondary);
transition: all 0.2s ease;
min-width: 36px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.7; /* ✅ 默认半透明 */
}
/* 深色主题下的 swipe 按钮 */
[data-color-theme='dark'] .swipe-button {
background: rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.1);
}
.swipe-button:hover:not(:disabled) {
background-color: rgba(0, 0, 0, 0.1);
background-color: rgba(102, 126, 234, 0.2); /* ✅ hover 时更明显 */
border-color: var(--color-accent);
color: var(--color-accent);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
opacity: 1; /* ✅ hover 时完全不透明 */
}
.swipe-button:active:not(:disabled) {
transform: translateY(0);
}
.swipe-button:disabled {
opacity: 0.5;
opacity: 0.3;
cursor: not-allowed;
background-color: rgba(0, 0, 0, 0.05);
}
.swipe-counter {
font-size: 0.8rem;
color: #666;
font-size: 0.85rem;
color: var(--color-text-secondary);
font-weight: 600;
min-width: 50px;
text-align: center;
padding: 2px 8px;
background-color: rgba(0, 0, 0, 0.05);
border-radius: var(--radius-sm);
}
/* ==================== Input Area - Fixed at Bottom ==================== */
@@ -721,3 +756,5 @@
margin: 0;
font-size: 0.9rem;
}
/* ✅ 右键菜单样式已移至全局 context-menu.css */

View File

@@ -9,6 +9,17 @@ const ChatBox = () => {
const messagesEndRef = useRef(null);
const optionsRef = useRef(null);
const chatSelectorRef = useRef(null);
const messagesContainerRef = useRef(null); // ✅ 新增:消息容器引用
const isUserAtBottomRef = useRef(true); // ✅ 新增:跟踪用户是否在底部
const contextMenuRef = useRef(null); // ✅ 新增:右键菜单引用
// ✅ 新增:右键菜单状态
const [contextMenu, setContextMenu] = React.useState({
visible: false,
x: 0,
y: 0,
message: null
});
// ✅ 从 ChatBoxUIStore 获取 UI 状态
const {
@@ -86,6 +97,26 @@ const ChatBox = () => {
};
}, [showChatSelector]);
// ✅ 点击外部关闭右键菜单
useEffect(() => {
const handleClickOutside = (event) => {
if (contextMenuRef.current && !contextMenuRef.current.contains(event.target)) {
closeContextMenu();
}
};
if (contextMenu.visible) {
document.addEventListener('mousedown', handleClickOutside);
// ✅ 添加滚动监听,防止滚动时菜单不关闭
document.addEventListener('scroll', closeContextMenu, true);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('scroll', closeContextMenu, true);
};
}, [contextMenu.visible]);
const handleInputHeight = (e) => {
const textarea = e.target;
@@ -114,15 +145,105 @@ const ChatBox = () => {
}
};
// 自动滚动到底部
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
// 自动滚动到底部(智能滚动)
const scrollToBottom = (force = false) => {
// 只有在用户在底部或强制滚动时才执行
if (isUserAtBottomRef.current || force) {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}
};
// 检测用户是否在底部
const checkIfUserAtBottom = () => {
if (!messagesContainerRef.current) return true;
const { scrollTop, scrollHeight, clientHeight } = messagesContainerRef.current;
// 允许 20px 的误差范围
const isAtBottom = scrollHeight - scrollTop - clientHeight < 20;
isUserAtBottomRef.current = isAtBottom;
return isAtBottom;
};
// 监听消息容器的滚动事件
useEffect(() => {
const container = messagesContainerRef.current;
if (!container) return;
const handleScroll = () => {
checkIfUserAtBottom();
};
container.addEventListener('scroll', handleScroll);
return () => container.removeEventListener('scroll', handleScroll);
}, []);
// 当消息变化时,智能滚动
useEffect(() => {
scrollToBottom();
}, [messages]);
// ✅ 监听全局键盘事件 - 左右键切换 swipe 或触发重roll
useEffect(() => {
const handleGlobalKeyDown = (e) => {
// 如果正在输入框中输入,不处理
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') {
return;
}
// 只处理左右方向键
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') {
return;
}
e.preventDefault();
// 找到最后一条 AI 消息
const lastAiMessage = [...messages].reverse().find(m => !m.is_user);
if (!lastAiMessage) {
return;
}
const hasSwipes = lastAiMessage.swipes && lastAiMessage.swipes.length > 0;
if (e.key === 'ArrowLeft') {
// 左键:如果有 swipe切换到上一个版本否则不做任何操作
if (hasSwipes) {
const currentIndex = currentSwipeId[lastAiMessage.floor] !== undefined
? currentSwipeId[lastAiMessage.floor]
: lastAiMessage.swipe_id;
if (currentIndex > 0) {
handleSwipeChange(lastAiMessage.floor, -1);
}
}
// 如果没有 swipe左键不做任何操作
} else if (e.key === 'ArrowRight') {
// 右键:如果有 swipe 且不是最后一个版本切换到下一个版本否则触发重roll
if (hasSwipes) {
const currentIndex = currentSwipeId[lastAiMessage.floor] !== undefined
? currentSwipeId[lastAiMessage.floor]
: lastAiMessage.swipe_id;
if (currentIndex < lastAiMessage.swipes.length - 1) {
handleSwipeChange(lastAiMessage.floor, 1);
} else {
// 已在最后一个版本触发重roll
console.log('[ChatBox] 已在最后一个版本触发重roll发送新消息');
handleRerollMessage(lastAiMessage);
}
} else {
// 没有 swipe直接触发重roll
console.log('[ChatBox] 没有 swipe触发重roll发送新消息');
handleRerollMessage(lastAiMessage);
}
}
};
window.addEventListener('keydown', handleGlobalKeyDown);
return () => window.removeEventListener('keydown', handleGlobalKeyDown);
}, [messages, currentSwipeId]);
// 处理编辑消息
const handleEdit = (message) => {
startEditing(message.floor, message.mes); // ✅ 使用 store 方法
@@ -155,6 +276,109 @@ const ChatBox = () => {
}
};
// ✅ 新增:右键菜单处理
const handleContextMenu = (e, message) => {
e.preventDefault();
setContextMenu({
visible: true,
x: e.clientX,
y: e.clientY,
message: message
});
};
// ✅ 关闭右键菜单
const closeContextMenu = () => {
setContextMenu({ visible: false, x: 0, y: 0, message: null });
};
// ✅ 复制消息内容
const handleCopyMessage = async (message) => {
try {
await navigator.clipboard.writeText(message.mes);
console.log('[ChatBox] 消息已复制到剪贴板');
closeContextMenu();
} catch (err) {
console.error('[ChatBox] 复制失败:', err);
alert('复制失败');
}
};
// ✅ 删除消息
const handleDeleteMessage = async (message) => {
if (!confirm(`确定要删除这条消息吗?`)) {
closeContextMenu();
return;
}
try {
const { deleteMessage } = useChatBoxStore.getState();
await deleteMessage(message.floor);
console.log('[ChatBox] 消息已删除');
closeContextMenu();
} catch (err) {
console.error('[ChatBox] 删除失败:', err);
alert('删除失败');
}
};
// ✅ 从右键菜单编辑
const handleEditFromContext = (message) => {
startEditing(message.floor, message.mes);
closeContextMenu();
};
// ✅ 重roll消息重新生成- 在目标消息的swipe数组中添加新版本
const handleRerollMessage = async (message) => {
// 只有 AI 消息才能重roll
if (message.is_user) {
closeContextMenu();
return;
}
try {
const { currentRole, currentChat, wsConnection } = useChatBoxStore.getState();
if (!currentRole || !currentChat) {
alert('请先选择角色和聊天');
closeContextMenu();
return;
}
// 获取上一条用户消息
const messageIndex = messages.findIndex(m => m.floor === message.floor);
let userMessage = null;
// 向前查找最近的用户消息
for (let i = messageIndex - 1; i >= 0; i--) {
if (messages[i].is_user) {
userMessage = messages[i];
break;
}
}
if (!userMessage) {
alert('找不到上一条用户消息');
closeContextMenu();
return;
}
console.log('[ChatBox] 重roll消息使用用户输入:', userMessage.mes);
console.log('[ChatBox] 目标消息 floor:', message.floor);
// 关闭菜单
closeContextMenu();
// ✅ 调用 sendMessage传入 targetFloor 参数在目标消息的swipe数组中添加新版本
const { sendMessage } = useChatBoxStore.getState();
await sendMessage(userMessage.mes, message.floor); // ✅ 传入 targetFloor
} catch (err) {
console.error('[ChatBox] 重roll失败:', err);
alert('重roll失败: ' + err.message);
}
};
// 切换选项显示
const toggleOptionsPanel = () => {
toggleOptions(); // ✅ 使用 store 方法
@@ -268,28 +492,16 @@ const ChatBox = () => {
const uniqueKey = message.id || `${message.floor}-${index}`;
return (
<div key={uniqueKey} className={`message ${isUser ? 'user' : 'ai'}`}>
<div
key={uniqueKey}
className={`message ${isUser ? 'user' : 'ai'}`}
onContextMenu={(e) => handleContextMenu(e, message)} // 添加右键菜单
>
<div className="message-container">
<div className="message-header">
<span className="message-name">{displayName}</span>
<span className="message-id">#{message.floor}</span>
<div className="message-toolbar">
<div className="toolbar-buttons">
<button
className="toolbar-button"
onClick={() => handleEdit(message)}
title="编辑"
>
</button>
<button
className="toolbar-button"
title="更多"
>
</button>
</div>
</div>
{/* ✅ 移除工具栏按钮,改用右键菜单 */}
</div>
<div className="message-content">
{isEditing ? (
@@ -339,22 +551,25 @@ const ChatBox = () => {
) : (
<div className="plain-text">{currentMes}</div>
)}
{hasSwipes && !isUser && isLatestMessage && (
{/* ✅ Swipe 控制按钮 - 所有有 swipe 的 AI 消息都显示 */}
{hasSwipes && !isUser && (
<div className="swipe-controls">
<button
className="swipe-button"
onClick={() => handleSwipeChange(message.floor, -1)}
disabled={currentSwipeIndex === 0}
title="上一个版本"
>
</button>
<span className="swipe-counter">
<span className="swipe-counter" title={`${message.swipes.length} 个版本`}>
{currentSwipeIndex + 1}/{message.swipes.length}
</span>
<button
className="swipe-button"
onClick={() => handleSwipeChange(message.floor, 1)}
disabled={currentSwipeIndex === message.swipes.length - 1}
title="下一个版本"
>
</button>
@@ -370,7 +585,7 @@ const ChatBox = () => {
return (
<div className="chat-box">
<div className="chat-messages">
<div className="chat-messages" ref={messagesContainerRef}>
{isLoading ? (
<div className="loading">加载中...</div>
) : error ? (
@@ -548,6 +763,45 @@ const ChatBox = () => {
</div>
</div>
)}
{/* ✅ 右键菜单 */}
{contextMenu.visible && contextMenu.message && (
<div
className="context-menu-overlay"
onClick={closeContextMenu}
>
<div
className="context-menu"
ref={contextMenuRef}
style={{
left: `${contextMenu.x}px`,
top: `${contextMenu.y}px`
}}
onClick={(e) => e.stopPropagation()}
>
<div className="context-menu-item" onClick={() => handleEditFromContext(contextMenu.message)}>
<span className="menu-icon"></span>
<span className="menu-label">编辑</span>
</div>
<div className="context-menu-item" onClick={() => handleCopyMessage(contextMenu.message)}>
<span className="menu-icon">📋</span>
<span className="menu-label">复制</span>
</div>
{/* ✅ 只有 AI 消息才显示重roll选项 */}
{!contextMenu.message.is_user && (
<div className="context-menu-item" onClick={() => handleRerollMessage(contextMenu.message)}>
<span className="menu-icon">🔄</span>
<span className="menu-label">重roll</span>
</div>
)}
<div className="context-menu-divider"></div>
<div className="context-menu-item danger" onClick={() => handleDeleteMessage(contextMenu.message)}>
<span className="menu-icon">🗑</span>
<span className="menu-label">删除</span>
</div>
</div>
</div>
)}
</div>
);
};

View File

@@ -94,3 +94,29 @@
font-size: 0.85rem;
line-height: 1.5;
}
/* ✅ 左侧边栏标签右键菜单样式 */
.sidebar-tab-context-menu {
min-width: 200px;
max-width: 280px;
}
.context-menu-header {
padding: var(--spacing-sm) var(--spacing-md);
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(102, 126, 234, 0.05));
border-bottom: 1px solid var(--color-border-light);
}
.menu-title {
font-weight: 600;
font-size: 0.95rem;
color: var(--color-accent);
}
.context-menu-description {
padding: var(--spacing-sm) var(--spacing-md);
font-size: 0.8rem;
color: var(--color-text-secondary);
line-height: 1.5;
background-color: rgba(0, 0, 0, 0.02);
}

View File

@@ -1,5 +1,5 @@
// frontend-react/src/components/SideBarLeft/SideBarLeft.jsx
import React from 'react';
import React, { useRef } from 'react';
import './SideBarLeft.css';
import { useSideBarLeftStore } from '../../Store/indexStore';
import usePresetStore from '../../Store/SideBarLeft/PresetSlice';
@@ -15,6 +15,15 @@ const SideBarLeft = () => {
const { activeTab, tabs, setActiveTab } = useSideBarLeftStore();
const { fetchPresets } = usePresetStore();
const { fetchProfiles } = useApiConfigStore();
const contextMenuRef = useRef(null);
// ✅ 右键菜单状态
const [contextMenu, setContextMenu] = React.useState({
visible: false,
x: 0,
y: 0,
tab: null
});
// 处理标签切换
const handleTabClick = (tabId) => {
@@ -31,6 +40,28 @@ const SideBarLeft = () => {
}
};
// ✅ 显示右键菜单
const handleContextMenu = (e, tab) => {
e.preventDefault();
setContextMenu({
visible: true,
x: e.clientX,
y: e.clientY,
tab: tab
});
};
// ✅ 关闭右键菜单
const closeContextMenu = () => {
setContextMenu({ visible: false, x: 0, y: 0, tab: null });
};
// ✅ 点击菜单项后自动切换到对应标签
const handleSwitchToTab = (tabId) => {
handleTabClick(tabId);
closeContextMenu();
};
return (
<div className="sidebar-left">
<div className="sidebar-tabs">
@@ -39,7 +70,8 @@ const SideBarLeft = () => {
key={tab.id}
className={`tab-button ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => handleTabClick(tab.id)}
title={tab.title}
onContextMenu={(e) => handleContextMenu(e, tab)} // 添加右键菜单
// 移除 title改用右键菜单显示详情
>
{tab.label}
</button>
@@ -54,6 +86,67 @@ const SideBarLeft = () => {
{activeTab === 'worldbook' && <WorldBook />}
{activeTab === 'tokenUsage' && <TokenUsage />}
</div>
{/* ✅ 右键菜单 */}
{contextMenu.visible && contextMenu.tab && (
<div
className="context-menu-overlay"
onClick={closeContextMenu}
>
<div
className="context-menu sidebar-tab-context-menu"
ref={contextMenuRef}
style={{ left: `${contextMenu.x}px`, top: `${contextMenu.y}px` }}
onClick={(e) => e.stopPropagation()}
>
{/* 菜单标题 - 显示标签名称 */}
<div className="context-menu-header">
<span className="menu-title">{contextMenu.tab.label}</span>
</div>
{/* 菜单描述 */}
<div className="context-menu-description">
{contextMenu.tab.title}
</div>
<div className="context-menu-divider"></div>
{/* 操作项 */}
<div className="context-menu-item" onClick={() => handleSwitchToTab(contextMenu.tab.id)}>
<span className="menu-icon">📂</span>
<span className="menu-label">打开</span>
</div>
{/* 根据标签类型显示不同的快捷操作 */}
{contextMenu.tab.id === 'character' && (
<>
<div className="context-menu-item" onClick={() => { handleSwitchToTab('character'); setTimeout(() => document.querySelector('.create-character-btn')?.click(), 100); }}>
<span className="menu-icon"></span>
<span className="menu-label">新建角色</span>
</div>
</>
)}
{contextMenu.tab.id === 'presets' && (
<>
<div className="context-menu-item" onClick={() => { handleSwitchToTab('presets'); setTimeout(() => document.querySelector('.create-preset-btn')?.click(), 100); }}>
<span className="menu-icon"></span>
<span className="menu-label">新建预设</span>
</div>
</>
)}
{contextMenu.tab.id === 'worldbook' && (
<>
<div className="context-menu-item" onClick={() => { handleSwitchToTab('worldbook'); setTimeout(() => document.querySelector('.action-btn')?.click(), 100); }}>
<span className="menu-icon"></span>
<span className="menu-label">新建世界书</span>
</div>
</>
)}
</div>
</div>
)}
</div>
);
};

View File

@@ -3,6 +3,7 @@ import React, { useState, useEffect, useRef, useCallback, memo } from 'react'; /
import { useCharacterStore, useCharacterCardUIStore } from '../../../../Store/SideBarLeft'; // ✅ 新增
import useChatBoxStore from '../../../../Store/Mid/ChatBoxSlice';
import useWorldBookStore from '../../../../Store/SideBarLeft/WorldBookSlice'; // 引入世界书 Store
import useSideBarLeftStore from '../../../../Store/SideBarLeft/SideBarLeftSlice'; // ✅ 引入侧边栏 Store
import './CharacterCard.css';
// 优化的角色卡片项组件 - 使用 memo 和 useCallback
@@ -236,6 +237,12 @@ const CharacterCard = () => {
try {
await deleteCharacter(name);
// ✅ 删除成功后自动切换到角色分页
const { setActiveTab } = useSideBarLeftStore.getState();
setActiveTab('character');
console.log('[CharacterCard] 角色已删除,已切换到角色分页');
} catch (err) {
console.error('删除失败:', err);
}

View File

@@ -2,6 +2,7 @@
@import './styles/variables.css';
@import './styles/reset.css';
@import './styles/z-index.css'; /* ✅ Z-Index 层级规范 */
@import './styles/context-menu.css'; /* ✅ 右键菜单通用样式 */
/* ==================== Main Layout ==================== */
.app {

View File

@@ -0,0 +1,73 @@
/* ==================== 右键菜单通用样式 ==================== */
.context-menu-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: var(--z-context-menu); /* ✅ 上下文菜单层 */
}
.context-menu {
position: fixed;
min-width: 160px;
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: var(--spacing-xs) 0;
z-index: calc(var(--z-context-menu) + 1);
animation: contextMenuFadeIn 0.1s ease;
}
@keyframes contextMenuFadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.context-menu-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
transition: all var(--transition-fast);
color: var(--color-text-primary);
font-size: 0.9rem;
}
.context-menu-item:hover {
background-color: var(--color-accent-light);
color: var(--color-accent);
}
.context-menu-item.danger {
color: var(--color-error);
}
.context-menu-item.danger:hover {
background-color: rgba(239, 68, 68, 0.1);
color: var(--color-error);
}
.menu-icon {
font-size: 1rem;
width: 20px;
text-align: center;
}
.menu-label {
flex: 1;
}
.context-menu-divider {
height: 1px;
background-color: var(--color-border);
margin: var(--spacing-xs) 0;
}

View File

@@ -21,6 +21,7 @@
--z-tooltip: 1200;
--z-chat-actions: 1000;
--z-character-preview: 1000;
--z-context-menu: 1500; /* ✅ 右键菜单 */
/* ==================== 弹窗层 (10000-19999) ==================== */
--z-modal-overlay: 10000;