2025-07-29 15:46:53 +08:00
|
|
|
|
// ===== index.js (Redis 存储版) =====
|
2025-07-29 10:03:49 +08:00
|
|
|
|
const chatArea = document.getElementById('chatArea');
|
|
|
|
|
const messageInput = document.getElementById('messageInput');
|
|
|
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
|
|
|
const newChatBtn = document.getElementById('newChatBtn');
|
|
|
|
|
const chatList = document.getElementById('chatList');
|
|
|
|
|
const welcomeMessage = document.getElementById('welcomeMessage');
|
|
|
|
|
const toggleSidebarBtn = document.getElementById('toggleSidebar');
|
|
|
|
|
const sidebar = document.getElementById('sidebar');
|
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
// RAG 相关 DOM
|
|
|
|
|
const ragSelect = document.getElementById('ragSelect');
|
|
|
|
|
const deleteRagBtn = document.getElementById('deleteRagBtn');
|
|
|
|
|
const refreshRagBtn = document.getElementById('refreshRagBtn');
|
|
|
|
|
|
|
|
|
|
let currentEventSource = null;
|
|
|
|
|
let currentChatId = localStorage.getItem('currentChatId') || null;
|
|
|
|
|
|
|
|
|
|
// ==================== Redis 存储相关函数 ====================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取所有聊天会话及其标题
|
|
|
|
|
*/
|
|
|
|
|
async function getAllChats() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('http://localhost:8095/api/v1/chat');
|
|
|
|
|
const chatIds = await response.json();
|
|
|
|
|
|
|
|
|
|
return Promise.all(chatIds.map(async chatId => {
|
|
|
|
|
const nameResponse = await fetch(`http://localhost:8095/api/v1/chat/${chatId}/name`);
|
|
|
|
|
const name = await nameResponse.text();
|
|
|
|
|
return { chatId, name };
|
|
|
|
|
}));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取聊天列表失败:', error);
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-29 10:03:49 +08:00
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
/**
|
|
|
|
|
* 创建新聊天会话
|
|
|
|
|
*/
|
|
|
|
|
async function createNewChat() {
|
2025-07-29 10:03:49 +08:00
|
|
|
|
const chatId = Date.now().toString();
|
2025-07-29 15:46:53 +08:00
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 设置默认名称
|
|
|
|
|
await fetch(`http://localhost:8095/api/v1/chat/${chatId}/rename`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ name: '新聊天' })
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
currentChatId = chatId;
|
|
|
|
|
localStorage.setItem('currentChatId', chatId);
|
|
|
|
|
|
|
|
|
|
updateChatList();
|
|
|
|
|
clearChatArea();
|
|
|
|
|
return chatId;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('创建聊天失败:', error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
2025-07-29 10:03:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
/**
|
|
|
|
|
* 删除聊天会话
|
|
|
|
|
*/
|
|
|
|
|
async function deleteChat(chatId) {
|
|
|
|
|
if (!confirm('确定要删除这个聊天记录吗?')) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`http://localhost:8095/api/v1/chat/${chatId}`, {
|
|
|
|
|
method: 'DELETE'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
if (currentChatId === chatId) {
|
|
|
|
|
await createNewChat();
|
|
|
|
|
}
|
|
|
|
|
updateChatList();
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(`HTTP ${response.status}`);
|
2025-07-29 10:03:49 +08:00
|
|
|
|
}
|
2025-07-29 15:46:53 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('删除聊天失败:', error);
|
|
|
|
|
alert('删除失败: ' + error.message);
|
2025-07-29 10:03:49 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
/**
|
|
|
|
|
* 重命名聊天会话
|
|
|
|
|
*/
|
|
|
|
|
async function renameChat(chatId) {
|
|
|
|
|
const currentName = await (await fetch(`http://localhost:8095/api/v1/chat/${chatId}/name`)).text();
|
|
|
|
|
const newName = prompt('请输入新的聊天名称', currentName);
|
2025-07-29 10:03:49 +08:00
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
if (newName) {
|
|
|
|
|
try {
|
|
|
|
|
await fetch(`http://localhost:8095/api/v1/chat/${chatId}/rename`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ name: newName })
|
|
|
|
|
});
|
|
|
|
|
updateChatList();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('重命名失败:', error);
|
|
|
|
|
alert('重命名失败: ' + error.message);
|
2025-07-29 10:03:49 +08:00
|
|
|
|
}
|
2025-07-29 15:46:53 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 加载聊天历史
|
|
|
|
|
*/
|
|
|
|
|
async function loadChat(chatId) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`http://localhost:8095/chat/${chatId}`);
|
|
|
|
|
const messages = await response.json();
|
|
|
|
|
|
|
|
|
|
currentChatId = chatId;
|
|
|
|
|
localStorage.setItem('currentChatId', chatId);
|
|
|
|
|
clearChatArea();
|
|
|
|
|
|
|
|
|
|
messages.forEach(msg => {
|
|
|
|
|
appendMessage(msg.content, msg.role === 'assistant', false);
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('加载聊天失败:', error);
|
|
|
|
|
alert('加载聊天失败: ' + error.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-29 10:03:49 +08:00
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
/**
|
|
|
|
|
* 保存消息到 Redis
|
|
|
|
|
*/
|
|
|
|
|
async function saveMessage(chatId, content, isAssistant) {
|
|
|
|
|
const msg = {
|
|
|
|
|
role: isAssistant ? "assistant" : "user",
|
|
|
|
|
content: content,
|
|
|
|
|
ts: Date.now()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await fetch(`http://localhost:8095/api/v1/chat/${chatId}/message`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(msg)
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('保存消息失败:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新聊天列表 UI
|
|
|
|
|
*/
|
|
|
|
|
async function updateChatList() {
|
|
|
|
|
chatList.innerHTML = '';
|
|
|
|
|
const chats = await getAllChats();
|
|
|
|
|
|
|
|
|
|
// 当前聊天置顶
|
|
|
|
|
chats.sort((a, b) => {
|
|
|
|
|
if (a.chatId === currentChatId) return -1;
|
|
|
|
|
if (b.chatId === currentChatId) return 1;
|
|
|
|
|
return parseInt(b.chatId) - parseInt(a.chatId); // 按时间倒序
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
chats.forEach(chat => {
|
2025-07-29 10:03:49 +08:00
|
|
|
|
const li = document.createElement('li');
|
2025-07-29 15:46:53 +08:00
|
|
|
|
li.className = `chat-item flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg cursor-pointer transition-colors ${
|
|
|
|
|
chat.chatId === currentChatId ? 'bg-blue-50' : ''
|
|
|
|
|
}`;
|
|
|
|
|
|
|
|
|
|
const date = new Date(parseInt(chat.chatId)).toLocaleDateString('zh-CN', {
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
day: '2-digit'
|
|
|
|
|
});
|
|
|
|
|
|
2025-07-29 10:03:49 +08:00
|
|
|
|
li.innerHTML = `
|
|
|
|
|
<div class="flex-1">
|
2025-07-29 15:46:53 +08:00
|
|
|
|
<div class="text-sm font-medium">${chat.name}</div>
|
|
|
|
|
<div class="text-xs text-gray-400">${date}</div>
|
2025-07-29 10:03:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
<div class="chat-actions flex items-center gap-1 opacity-0 transition-opacity duration-200">
|
2025-07-29 15:46:53 +08:00
|
|
|
|
<button class="p-1 hover:bg-gray-200 rounded text-gray-500">重命名</button>
|
|
|
|
|
<button class="p-1 hover:bg-red-200 rounded text-red-500">删除</button>
|
2025-07-29 10:03:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
`;
|
2025-07-29 15:46:53 +08:00
|
|
|
|
|
|
|
|
|
// 事件绑定
|
|
|
|
|
li.querySelectorAll('button')[0].addEventListener('click', (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
renameChat(chat.chatId);
|
2025-07-29 10:03:49 +08:00
|
|
|
|
});
|
2025-07-29 15:46:53 +08:00
|
|
|
|
|
|
|
|
|
li.querySelectorAll('button')[1].addEventListener('click', (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
deleteChat(chat.chatId);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
li.addEventListener('click', () => loadChat(chat.chatId));
|
2025-07-29 10:03:49 +08:00
|
|
|
|
li.addEventListener('mouseenter', () => {
|
|
|
|
|
li.querySelector('.chat-actions').classList.remove('opacity-0');
|
|
|
|
|
});
|
|
|
|
|
li.addEventListener('mouseleave', () => {
|
|
|
|
|
li.querySelector('.chat-actions').classList.add('opacity-0');
|
|
|
|
|
});
|
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
chatList.appendChild(li);
|
2025-07-29 10:03:49 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
// ==================== 消息显示相关函数 ====================
|
2025-07-29 10:03:49 +08:00
|
|
|
|
|
|
|
|
|
function clearChatArea() {
|
|
|
|
|
chatArea.innerHTML = '';
|
|
|
|
|
welcomeMessage.style.display = 'flex';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendMessage(content, isAssistant = false, saveToStorage = true) {
|
|
|
|
|
welcomeMessage.style.display = 'none';
|
|
|
|
|
const messageDiv = document.createElement('div');
|
2025-07-29 15:46:53 +08:00
|
|
|
|
messageDiv.className = `max-w-4xl mx-auto mb-4 p-4 rounded-lg ${
|
|
|
|
|
isAssistant ? 'bg-gray-100' : 'bg-white border'
|
|
|
|
|
} markdown-body relative`;
|
2025-07-29 10:03:49 +08:00
|
|
|
|
|
|
|
|
|
const renderedContent = DOMPurify.sanitize(marked.parse(content));
|
|
|
|
|
messageDiv.innerHTML = renderedContent;
|
|
|
|
|
|
|
|
|
|
const copyBtn = document.createElement('button');
|
|
|
|
|
copyBtn.className = 'absolute top-2 right-2 p-1 bg-gray-200 rounded-md text-xs';
|
|
|
|
|
copyBtn.textContent = '复制';
|
|
|
|
|
copyBtn.onclick = () => {
|
|
|
|
|
navigator.clipboard.writeText(content).then(() => {
|
|
|
|
|
copyBtn.textContent = '已复制';
|
2025-07-29 15:46:53 +08:00
|
|
|
|
setTimeout(() => (copyBtn.textContent = '复制'), 2000);
|
2025-07-29 10:03:49 +08:00
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
messageDiv.appendChild(copyBtn);
|
|
|
|
|
|
|
|
|
|
chatArea.appendChild(messageDiv);
|
|
|
|
|
chatArea.scrollTop = chatArea.scrollHeight;
|
|
|
|
|
|
|
|
|
|
if (saveToStorage && currentChatId) {
|
2025-07-29 15:46:53 +08:00
|
|
|
|
saveMessage(currentChatId, content, isAssistant);
|
2025-07-29 10:03:49 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
// ==================== 事件流处理 ====================
|
2025-07-29 10:03:49 +08:00
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
function startEventStream(message) {
|
|
|
|
|
if (currentEventSource) currentEventSource.close();
|
|
|
|
|
|
|
|
|
|
const ragTag = ragSelect.value;
|
2025-07-29 10:03:49 +08:00
|
|
|
|
const aiModelSelect = document.getElementById('aiModel');
|
2025-07-29 15:46:53 +08:00
|
|
|
|
const aiModelValue = aiModelSelect.value;
|
|
|
|
|
const aiModelModel = aiModelSelect.options[aiModelSelect.selectedIndex].getAttribute('model');
|
2025-07-29 10:03:49 +08:00
|
|
|
|
|
|
|
|
|
let url;
|
2025-07-29 15:46:53 +08:00
|
|
|
|
const base = `http://localhost:8095/api/v1/${aiModelValue}`;
|
|
|
|
|
const params = new URLSearchParams({ message, model: aiModelModel });
|
2025-07-29 10:03:49 +08:00
|
|
|
|
if (ragTag) {
|
2025-07-29 15:46:53 +08:00
|
|
|
|
params.append('ragTag', ragTag);
|
|
|
|
|
url = `${base}/generate_stream_rag?${params.toString()}`;
|
2025-07-29 10:03:49 +08:00
|
|
|
|
} else {
|
2025-07-29 15:46:53 +08:00
|
|
|
|
url = `${base}/generate_stream?${params.toString()}`;
|
2025-07-29 10:03:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentEventSource = new EventSource(url);
|
|
|
|
|
let accumulatedContent = '';
|
|
|
|
|
let tempMessageDiv = null;
|
|
|
|
|
|
|
|
|
|
currentEventSource.onmessage = function(event) {
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(event.data);
|
2025-07-29 15:46:53 +08:00
|
|
|
|
const content = data.result?.output?.content || data.content;
|
2025-07-29 10:03:49 +08:00
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
if (content) {
|
|
|
|
|
accumulatedContent += content;
|
2025-07-29 10:03:49 +08:00
|
|
|
|
|
|
|
|
|
if (!tempMessageDiv) {
|
|
|
|
|
tempMessageDiv = document.createElement('div');
|
|
|
|
|
tempMessageDiv.className = 'max-w-4xl mx-auto mb-4 p-4 rounded-lg bg-gray-100 markdown-body relative';
|
|
|
|
|
chatArea.appendChild(tempMessageDiv);
|
|
|
|
|
welcomeMessage.style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tempMessageDiv.textContent = accumulatedContent;
|
|
|
|
|
chatArea.scrollTop = chatArea.scrollHeight;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.result?.output?.properties?.finishReason === 'STOP') {
|
|
|
|
|
currentEventSource.close();
|
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
if (tempMessageDiv) {
|
|
|
|
|
tempMessageDiv.innerHTML = DOMPurify.sanitize(marked.parse(accumulatedContent));
|
|
|
|
|
|
|
|
|
|
const copyBtn = document.createElement('button');
|
|
|
|
|
copyBtn.className = 'absolute top-2 right-2 p-1 bg-gray-200 rounded-md text-xs';
|
|
|
|
|
copyBtn.textContent = '复制';
|
|
|
|
|
copyBtn.onclick = () => {
|
|
|
|
|
navigator.clipboard.writeText(accumulatedContent).then(() => {
|
|
|
|
|
copyBtn.textContent = '已复制';
|
|
|
|
|
setTimeout(() => (copyBtn.textContent = '复制'), 2000);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
tempMessageDiv.appendChild(copyBtn);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 10:03:49 +08:00
|
|
|
|
if (currentChatId) {
|
2025-07-29 15:46:53 +08:00
|
|
|
|
saveMessage(currentChatId, accumulatedContent, true);
|
2025-07-29 10:03:49 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
2025-07-29 15:46:53 +08:00
|
|
|
|
console.error('解析事件数据失败:', e);
|
2025-07-29 10:03:49 +08:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
currentEventSource.onerror = function(error) {
|
2025-07-29 15:46:53 +08:00
|
|
|
|
console.error('EventSource 错误:', error);
|
2025-07-29 10:03:49 +08:00
|
|
|
|
currentEventSource.close();
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
// ==================== 事件绑定 ====================
|
|
|
|
|
|
|
|
|
|
submitBtn.addEventListener('click', async () => {
|
2025-07-29 10:03:49 +08:00
|
|
|
|
const message = messageInput.value.trim();
|
|
|
|
|
if (!message) return;
|
|
|
|
|
|
|
|
|
|
if (!currentChatId) {
|
2025-07-29 15:46:53 +08:00
|
|
|
|
await createNewChat();
|
2025-07-29 10:03:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appendMessage(message, false);
|
|
|
|
|
messageInput.value = '';
|
|
|
|
|
startEventStream(message);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
messageInput.addEventListener('keypress', (e) => {
|
|
|
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
submitBtn.click();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
newChatBtn.addEventListener('click', createNewChat);
|
|
|
|
|
|
|
|
|
|
toggleSidebarBtn.addEventListener('click', () => {
|
|
|
|
|
sidebar.classList.toggle('-translate-x-full');
|
|
|
|
|
updateSidebarIcon();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function updateSidebarIcon() {
|
|
|
|
|
const iconPath = document.getElementById('sidebarIconPath');
|
|
|
|
|
if (sidebar.classList.contains('-translate-x-full')) {
|
|
|
|
|
iconPath.setAttribute('d', 'M4 6h16M4 12h4m12 0h-4M4 18h16');
|
|
|
|
|
} else {
|
|
|
|
|
iconPath.setAttribute('d', 'M4 6h16M4 12h16M4 18h16');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
// ==================== 初始化 ====================
|
|
|
|
|
|
|
|
|
|
// 初始化聊天列表
|
2025-07-29 10:03:49 +08:00
|
|
|
|
updateChatList();
|
2025-07-29 15:46:53 +08:00
|
|
|
|
|
|
|
|
|
// 加载当前聊天
|
|
|
|
|
if (currentChatId) {
|
|
|
|
|
loadChat(currentChatId);
|
2025-07-29 10:03:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
// 响应式处理
|
2025-07-29 10:03:49 +08:00
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
|
|
if (window.innerWidth > 768) {
|
|
|
|
|
sidebar.classList.remove('-translate-x-full');
|
|
|
|
|
} else {
|
|
|
|
|
sidebar.classList.add('-translate-x-full');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (window.innerWidth <= 768) {
|
|
|
|
|
sidebar.classList.add('-translate-x-full');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateSidebarIcon();
|
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
/** -------------------------
|
|
|
|
|
* 上传知识下拉菜单(保持你现有 HTML 结构)
|
|
|
|
|
* - 点击按钮展开/收起菜单
|
|
|
|
|
* - 点击外部区域/按下 Esc 关闭
|
|
|
|
|
* - 点击菜单项后自动关闭
|
|
|
|
|
* ------------------------- */
|
|
|
|
|
(function initUploadMenu() {
|
|
|
|
|
const uploadMenuButton = document.getElementById('uploadMenuButton');
|
|
|
|
|
const uploadMenu = document.getElementById('uploadMenu');
|
|
|
|
|
if (!uploadMenuButton || !uploadMenu) return;
|
|
|
|
|
|
|
|
|
|
// 切换菜单显示/隐藏
|
|
|
|
|
const toggleMenu = () => {
|
|
|
|
|
uploadMenu.classList.toggle('hidden');
|
|
|
|
|
};
|
2025-07-29 10:03:49 +08:00
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
// 显示菜单
|
|
|
|
|
const openMenu = () => {
|
|
|
|
|
uploadMenu.classList.remove('hidden');
|
|
|
|
|
};
|
2025-07-29 10:03:49 +08:00
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
// 隐藏菜单
|
|
|
|
|
const closeMenu = () => {
|
2025-07-29 10:03:49 +08:00
|
|
|
|
uploadMenu.classList.add('hidden');
|
2025-07-29 15:46:53 +08:00
|
|
|
|
};
|
2025-07-29 10:03:49 +08:00
|
|
|
|
|
2025-07-29 15:46:53 +08:00
|
|
|
|
// 点击按钮展开/收起
|
|
|
|
|
uploadMenuButton.addEventListener('click', (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
toggleMenu();
|
2025-07-29 10:03:49 +08:00
|
|
|
|
});
|
2025-07-29 15:46:53 +08:00
|
|
|
|
|
|
|
|
|
// 键盘辅助:Enter/Space 展开,Esc 关闭
|
|
|
|
|
uploadMenuButton.addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
openMenu();
|
|
|
|
|
} else if (e.key === 'Escape') {
|
|
|
|
|
closeMenu();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 点击菜单项后自动关闭
|
|
|
|
|
uploadMenu.querySelectorAll('a').forEach(a => {
|
|
|
|
|
a.addEventListener('click', () => closeMenu());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 点击外部任意区域关闭
|
|
|
|
|
document.addEventListener('click', (e) => {
|
|
|
|
|
if (!uploadMenu.classList.contains('hidden')) {
|
|
|
|
|
if (!uploadMenu.contains(e.target) && e.target !== uploadMenuButton) {
|
|
|
|
|
closeMenu();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Esc 关闭(全局)
|
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.key === 'Escape') closeMenu();
|
|
|
|
|
});
|
|
|
|
|
})();
|