mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-03-04 15:47:52 +08:00
feat: web channel support multiple message and picture display
This commit is contained in:
@@ -170,6 +170,24 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#github-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 15px;
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
#github-link:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
#github-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
#messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -210,7 +228,7 @@
|
||||
}
|
||||
|
||||
.user-container .avatar {
|
||||
margin-left: 15px;
|
||||
margin-left: 20px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
@@ -219,6 +237,7 @@
|
||||
}
|
||||
|
||||
.user-container .message {
|
||||
padding: 13px 16px;
|
||||
background-color: var(--bot-msg-bg);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 8px;
|
||||
@@ -266,7 +285,7 @@
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 12px 16px;
|
||||
padding: 5px 16px;
|
||||
border-radius: 10px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
@@ -400,7 +419,7 @@
|
||||
.message img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 1em 0;
|
||||
margin: 0 0;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
@@ -548,10 +567,12 @@
|
||||
left: -260px;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
transition: left 0.3s ease;
|
||||
}
|
||||
|
||||
#sidebar.active {
|
||||
left: 0;
|
||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
#menu-toggle {
|
||||
@@ -577,6 +598,23 @@
|
||||
#header-logo {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* 添加遮罩层,当侧边栏打开时显示 */
|
||||
#sidebar-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#sidebar.active + #sidebar-overlay {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@@ -717,7 +755,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sidebar-overlay"></div>
|
||||
<div id="main-content">
|
||||
<div id="chat-header">
|
||||
<button id="menu-toggle">
|
||||
@@ -725,6 +763,9 @@
|
||||
</button>
|
||||
<img id="header-logo" src="assets/logo.jpg" alt="AI Assistant Logo">
|
||||
<div id="chat-title">AI 助手</div>
|
||||
<a id="github-link" href="https://github.com/zhayujie/chatgpt-on-wechat" target="_blank" rel="noopener noreferrer">
|
||||
<img id="github-icon" src="assets/github.png" alt="GitHub">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="messages">
|
||||
@@ -778,10 +819,17 @@
|
||||
const newChatButton = document.getElementById('new-chat');
|
||||
const chatHistory = document.getElementById('chat-history');
|
||||
|
||||
// 简化变量,只保留用户ID
|
||||
let userId = 'user_' + Math.random().toString(36).substring(2, 10);
|
||||
let currentSessionId = 'default_session'; // 使用固定会话ID
|
||||
|
||||
// 生成新的会话ID
|
||||
function generateSessionId() {
|
||||
return 'session_' + ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
}
|
||||
|
||||
// 生成初始会话ID
|
||||
let sessionId = generateSessionId();
|
||||
console.log('Session ID:', sessionId);
|
||||
|
||||
// 添加一个变量来跟踪输入法状态
|
||||
let isComposing = false;
|
||||
|
||||
@@ -815,15 +863,18 @@
|
||||
});
|
||||
|
||||
// 处理菜单切换
|
||||
menuToggle.addEventListener('click', function() {
|
||||
menuToggle.addEventListener('click', function(event) {
|
||||
event.stopPropagation(); // 防止事件冒泡到 main-content
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
|
||||
// 处理新对话按钮 - 创建新的用户ID和清空当前对话
|
||||
// 处理新对话按钮 - 创建新的会话ID和清空当前对话
|
||||
newChatButton.addEventListener('click', function() {
|
||||
// 生成新的用户ID
|
||||
userId = 'user_' + Math.random().toString(36).substring(2, 10);
|
||||
console.log('New conversation started with user ID:', userId);
|
||||
// 生成新的会话ID
|
||||
sessionId = generateSessionId();
|
||||
// 将新的会话ID保存到全局变量,供轮询函数使用
|
||||
window.sessionId = sessionId;
|
||||
console.log('New conversation started with new session ID:', sessionId);
|
||||
|
||||
// 清空聊天记录
|
||||
clearChat();
|
||||
@@ -881,27 +932,42 @@
|
||||
input.style.height = '52px';
|
||||
sendButton.disabled = true;
|
||||
|
||||
// 发送到服务器并等待响应
|
||||
// 使用当前的全局会话ID
|
||||
const currentSessionId = window.sessionId || sessionId;
|
||||
|
||||
// 发送到服务器并获取请求ID
|
||||
axios({
|
||||
method: 'post',
|
||||
url: '/message',
|
||||
data: {
|
||||
user_id: userId,
|
||||
session_id: currentSessionId, // 使用最新的会话ID
|
||||
message: userMessage,
|
||||
timestamp: timestamp.toISOString(),
|
||||
session_id: currentSessionId
|
||||
timestamp: timestamp.toISOString()
|
||||
},
|
||||
timeout: 120000 // 120秒超时
|
||||
timeout: 10000 // 10秒超时
|
||||
})
|
||||
.then(response => {
|
||||
// 移除加载消息
|
||||
if (loadingContainer.parentNode) {
|
||||
messagesDiv.removeChild(loadingContainer);
|
||||
}
|
||||
|
||||
// 添加AI回复
|
||||
if (response.data.reply) {
|
||||
addBotMessage(response.data.reply, new Date());
|
||||
if (response.data.status === "success") {
|
||||
// 保存当前请求ID,用于识别响应
|
||||
const currentRequestId = response.data.request_id;
|
||||
|
||||
// 如果还没有开始轮询,则开始轮询
|
||||
if (!window.isPolling) {
|
||||
startPolling(currentSessionId);
|
||||
}
|
||||
|
||||
// 将请求ID和加载容器关联起来
|
||||
window.loadingContainers = window.loadingContainers || {};
|
||||
window.loadingContainers[currentRequestId] = loadingContainer;
|
||||
|
||||
// 初始化请求的响应容器映射
|
||||
window.requestContainers = window.requestContainers || {};
|
||||
} else {
|
||||
// 处理错误
|
||||
if (loadingContainer.parentNode) {
|
||||
messagesDiv.removeChild(loadingContainer);
|
||||
}
|
||||
addBotMessage("抱歉,发生了错误,请稍后再试。", new Date());
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -920,178 +986,108 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 添加加载中的消息
|
||||
function addLoadingMessage() {
|
||||
const botContainer = document.createElement('div');
|
||||
botContainer.className = 'bot-container loading-container';
|
||||
// 修改轮询函数,确保正确处理多条回复
|
||||
function startPolling(sessionId) {
|
||||
if (window.isPolling) return;
|
||||
|
||||
const messageContainer = document.createElement('div');
|
||||
messageContainer.className = 'message-container';
|
||||
window.isPolling = true;
|
||||
console.log('Starting polling with session ID:', sessionId);
|
||||
|
||||
messageContainer.innerHTML = `
|
||||
<div class="avatar bot-avatar">
|
||||
<i class="fas fa-robot"></i>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message">
|
||||
<div class="typing-indicator">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
botContainer.appendChild(messageContainer);
|
||||
messagesDiv.appendChild(botContainer);
|
||||
scrollToBottom();
|
||||
|
||||
return botContainer;
|
||||
}
|
||||
|
||||
// 替换 formatMessage 函数,使用 markdown-it 替代 marked
|
||||
function formatMessage(content) {
|
||||
try {
|
||||
// 初始化 markdown-it 实例
|
||||
const md = window.markdownit({
|
||||
html: false, // 禁用 HTML 标签
|
||||
xhtmlOut: false, // 使用 '/' 关闭单标签
|
||||
breaks: true, // 将换行符转换为 <br>
|
||||
linkify: true, // 自动将 URL 转换为链接
|
||||
typographer: true, // 启用一些语言中性的替换和引号美化
|
||||
highlight: function(str, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(str, { language: lang }).value;
|
||||
} catch (e) {
|
||||
console.error('Error highlighting code:', e);
|
||||
function poll() {
|
||||
if (!window.isPolling) return;
|
||||
|
||||
// 如果页面已关闭或导航离开,停止轮询
|
||||
if (document.hidden) {
|
||||
setTimeout(poll, 5000); // 页面不可见时降低轮询频率
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用当前的会话ID,而不是闭包中的sessionId
|
||||
const currentSessionId = window.sessionId || sessionId;
|
||||
|
||||
axios({
|
||||
method: 'post',
|
||||
url: '/poll',
|
||||
data: {
|
||||
session_id: currentSessionId
|
||||
},
|
||||
timeout: 5000
|
||||
})
|
||||
.then(response => {
|
||||
if (response.data.status === "success") {
|
||||
if (response.data.has_content) {
|
||||
console.log('Received response:', response.data);
|
||||
|
||||
// 获取请求ID和内容
|
||||
const requestId = response.data.request_id;
|
||||
const content = response.data.content;
|
||||
const timestamp = new Date(response.data.timestamp * 1000);
|
||||
|
||||
// 检查是否有对应的加载容器
|
||||
if (window.loadingContainers && window.loadingContainers[requestId]) {
|
||||
// 移除加载容器
|
||||
const loadingContainer = window.loadingContainers[requestId];
|
||||
if (loadingContainer && loadingContainer.parentNode) {
|
||||
messagesDiv.removeChild(loadingContainer);
|
||||
}
|
||||
|
||||
// 删除已处理的加载容器引用
|
||||
delete window.loadingContainers[requestId];
|
||||
}
|
||||
|
||||
// 始终创建新的消息,无论是否是同一个请求的后续回复
|
||||
addBotMessage(content, timestamp, requestId);
|
||||
|
||||
// 滚动到底部
|
||||
scrollToBottom();
|
||||
}
|
||||
return hljs.highlightAuto(str).value;
|
||||
}
|
||||
});
|
||||
|
||||
// 渲染 Markdown
|
||||
return md.render(content);
|
||||
} catch (e) {
|
||||
console.error('Error parsing markdown:', e);
|
||||
// 如果解析失败,至少确保换行符正确显示
|
||||
return content.replace(/\n/g, '<br>');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 applyHighlighting 函数
|
||||
function applyHighlighting() {
|
||||
try {
|
||||
document.querySelectorAll('pre code').forEach((block) => {
|
||||
// 确保代码块有正确的类
|
||||
if (!block.classList.contains('hljs')) {
|
||||
block.classList.add('hljs');
|
||||
}
|
||||
|
||||
// 尝试获取语言
|
||||
let language = '';
|
||||
block.classList.forEach(cls => {
|
||||
if (cls.startsWith('language-')) {
|
||||
language = cls.replace('language-', '');
|
||||
}
|
||||
});
|
||||
|
||||
// 应用高亮
|
||||
if (language && hljs.getLanguage(language)) {
|
||||
try {
|
||||
hljs.highlightBlock(block);
|
||||
} catch (e) {
|
||||
console.error('Error highlighting specific language:', e);
|
||||
hljs.highlightAuto(block);
|
||||
}
|
||||
|
||||
// 继续轮询,使用原来的2秒间隔
|
||||
setTimeout(poll, 2000);
|
||||
} else {
|
||||
hljs.highlightAuto(block);
|
||||
// 处理错误但继续轮询
|
||||
console.error('Error in polling response:', response.data.message);
|
||||
setTimeout(poll, 3000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error polling for response:', error);
|
||||
// 出错后继续轮询,但间隔更长
|
||||
setTimeout(poll, 3000);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error applying code highlighting:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加用户消息的函数 (保存到localStorage)
|
||||
function addUserMessage(content, timestamp) {
|
||||
// 显示消息
|
||||
displayUserMessage(content, timestamp);
|
||||
|
||||
// 保存到localStorage
|
||||
saveMessageToLocalStorage({
|
||||
role: 'user',
|
||||
content: content,
|
||||
timestamp: timestamp.getTime()
|
||||
});
|
||||
// 开始轮询
|
||||
poll();
|
||||
}
|
||||
|
||||
// 添加机器人消息的函数 (保存到localStorage)
|
||||
function addBotMessage(content, timestamp) {
|
||||
// 添加机器人消息的函数 (保存到localStorage),增加requestId参数
|
||||
function addBotMessage(content, timestamp, requestId) {
|
||||
// 显示消息
|
||||
displayBotMessage(content, timestamp);
|
||||
displayBotMessage(content, timestamp, requestId);
|
||||
|
||||
// 保存到localStorage
|
||||
saveMessageToLocalStorage({
|
||||
role: 'assistant',
|
||||
content: content,
|
||||
timestamp: timestamp.getTime()
|
||||
timestamp: timestamp.getTime(),
|
||||
requestId: requestId
|
||||
});
|
||||
}
|
||||
|
||||
// 只显示用户消息而不保存到localStorage
|
||||
function displayUserMessage(content, timestamp) {
|
||||
const userContainer = document.createElement('div');
|
||||
userContainer.className = 'user-container';
|
||||
|
||||
const messageContainer = document.createElement('div');
|
||||
messageContainer.className = 'message-container';
|
||||
|
||||
// 安全地格式化消息
|
||||
let formattedContent;
|
||||
try {
|
||||
formattedContent = formatMessage(content);
|
||||
} catch (e) {
|
||||
console.error('Error formatting user message:', e);
|
||||
formattedContent = `<p>${content.replace(/\n/g, '<br>')}</p>`;
|
||||
}
|
||||
|
||||
messageContainer.innerHTML = `
|
||||
<div class="avatar user-avatar">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message">${formattedContent}</div>
|
||||
<div class="timestamp">${formatTimestamp(timestamp)}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
userContainer.appendChild(messageContainer);
|
||||
messagesDiv.appendChild(userContainer);
|
||||
|
||||
// 应用代码高亮
|
||||
setTimeout(() => {
|
||||
applyHighlighting();
|
||||
}, 0);
|
||||
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// 只显示机器人消息而不保存到localStorage
|
||||
function displayBotMessage(content, timestamp) {
|
||||
// 修改显示机器人消息的函数,增加requestId参数
|
||||
function displayBotMessage(content, timestamp, requestId) {
|
||||
const botContainer = document.createElement('div');
|
||||
botContainer.className = 'bot-container';
|
||||
|
||||
// 如果有requestId,将其存储在数据属性中
|
||||
if (requestId) {
|
||||
botContainer.dataset.requestId = requestId;
|
||||
}
|
||||
|
||||
const messageContainer = document.createElement('div');
|
||||
messageContainer.className = 'message-container';
|
||||
|
||||
// 确保时间戳是有效的 Date 对象
|
||||
if (!(timestamp instanceof Date) || isNaN(timestamp)) {
|
||||
timestamp = new Date();
|
||||
}
|
||||
|
||||
// 安全地格式化消息
|
||||
let formattedContent;
|
||||
try {
|
||||
@@ -1114,45 +1110,82 @@
|
||||
botContainer.appendChild(messageContainer);
|
||||
messagesDiv.appendChild(botContainer);
|
||||
|
||||
// 使用setTimeout确保DOM已更新,并延长等待时间
|
||||
// 应用代码高亮
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// 直接对新添加的消息应用高亮
|
||||
const codeBlocks = botContainer.querySelectorAll('pre code');
|
||||
codeBlocks.forEach(block => {
|
||||
// 确保代码块有正确的类
|
||||
if (!block.classList.contains('hljs')) {
|
||||
block.classList.add('hljs');
|
||||
}
|
||||
|
||||
// 尝试获取语言
|
||||
let language = '';
|
||||
block.classList.forEach(cls => {
|
||||
if (cls.startsWith('language-')) {
|
||||
language = cls.replace('language-', '');
|
||||
}
|
||||
});
|
||||
|
||||
// 应用高亮
|
||||
if (language && hljs.getLanguage(language)) {
|
||||
try {
|
||||
hljs.highlightBlock(block);
|
||||
} catch (e) {
|
||||
console.error('Error highlighting specific language:', e);
|
||||
hljs.highlightAuto(block);
|
||||
}
|
||||
} else {
|
||||
hljs.highlightAuto(block);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error in delayed highlighting:', e);
|
||||
}
|
||||
}, 100); // 增加延迟以确保DOM完全更新
|
||||
applyHighlighting();
|
||||
}, 0);
|
||||
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// 处理响应
|
||||
function handleResponse(requestId, content) {
|
||||
// 获取该请求的加载容器
|
||||
const loadingContainer = window.loadingContainers && window.loadingContainers[requestId];
|
||||
|
||||
// 如果有加载容器,移除它
|
||||
if (loadingContainer && loadingContainer.parentNode) {
|
||||
messagesDiv.removeChild(loadingContainer);
|
||||
delete window.loadingContainers[requestId];
|
||||
}
|
||||
|
||||
// 为每个请求创建一个新的消息容器
|
||||
if (!window.requestContainers[requestId]) {
|
||||
window.requestContainers[requestId] = createBotMessageContainer(content, new Date());
|
||||
} else {
|
||||
// 更新现有消息容器
|
||||
updateBotMessageContent(window.requestContainers[requestId], content);
|
||||
}
|
||||
|
||||
// 保存消息到localStorage
|
||||
saveMessageToLocalStorage({
|
||||
role: 'assistant',
|
||||
content: content,
|
||||
timestamp: new Date().getTime(),
|
||||
request_id: requestId
|
||||
});
|
||||
}
|
||||
|
||||
// 修改createBotMessageContainer函数,使其返回创建的容器
|
||||
function createBotMessageContainer(content, timestamp) {
|
||||
const botContainer = document.createElement('div');
|
||||
botContainer.className = 'bot-container';
|
||||
|
||||
const messageContainer = document.createElement('div');
|
||||
messageContainer.className = 'message-container';
|
||||
|
||||
// 安全地格式化消息
|
||||
let formattedContent;
|
||||
try {
|
||||
formattedContent = formatMessage(content);
|
||||
} catch (e) {
|
||||
console.error('Error formatting bot message:', e);
|
||||
formattedContent = `<p>${content.replace(/\n/g, '<br>')}</p>`;
|
||||
}
|
||||
|
||||
messageContainer.innerHTML = `
|
||||
<div class="avatar bot-avatar">
|
||||
<i class="fas fa-robot"></i>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message">${formattedContent}</div>
|
||||
<div class="timestamp">${formatTimestamp(timestamp)}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
botContainer.appendChild(messageContainer);
|
||||
messagesDiv.appendChild(botContainer);
|
||||
|
||||
// 应用代码高亮
|
||||
setTimeout(() => {
|
||||
applyHighlighting();
|
||||
}, 0);
|
||||
|
||||
scrollToBottom();
|
||||
|
||||
return botContainer;
|
||||
}
|
||||
|
||||
// 格式化时间戳
|
||||
function formatTimestamp(date) {
|
||||
return date.toLocaleTimeString();
|
||||
@@ -1223,8 +1256,8 @@
|
||||
});
|
||||
});
|
||||
|
||||
// 清空localStorage中的消息 - 使用用户ID作为键
|
||||
localStorage.setItem(`chatMessages_${userId}`, JSON.stringify([]));
|
||||
// 清空localStorage中的消息 - 使用会话ID作为键
|
||||
localStorage.setItem(`chatMessages_${sessionId}`, JSON.stringify([]));
|
||||
|
||||
// 在移动设备上关闭侧边栏
|
||||
if (window.innerWidth <= 768) {
|
||||
@@ -1232,26 +1265,242 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 从localStorage加载消息 - 使用用户ID作为键
|
||||
// 从localStorage加载消息 - 使用会话ID作为键
|
||||
function loadMessagesFromLocalStorage() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(`chatMessages_${userId}`) || '[]');
|
||||
return JSON.parse(localStorage.getItem(`chatMessages_${sessionId}`) || '[]');
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from localStorage:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 保存消息到localStorage - 使用用户ID作为键
|
||||
// 保存消息到localStorage - 使用会话ID作为键
|
||||
function saveMessageToLocalStorage(message) {
|
||||
try {
|
||||
const messages = loadMessagesFromLocalStorage();
|
||||
messages.push(message);
|
||||
localStorage.setItem(`chatMessages_${userId}`, JSON.stringify(messages));
|
||||
localStorage.setItem(`chatMessages_${sessionId}`, JSON.stringify(messages));
|
||||
} catch (error) {
|
||||
console.error('Error saving message to localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加用户消息的函数 (保存到localStorage)
|
||||
function addUserMessage(content, timestamp) {
|
||||
// 显示消息
|
||||
displayUserMessage(content, timestamp);
|
||||
|
||||
// 保存到localStorage
|
||||
saveMessageToLocalStorage({
|
||||
role: 'user',
|
||||
content: content,
|
||||
timestamp: timestamp.getTime()
|
||||
});
|
||||
}
|
||||
|
||||
// 只显示用户消息而不保存到localStorage
|
||||
function displayUserMessage(content, timestamp) {
|
||||
const userContainer = document.createElement('div');
|
||||
userContainer.className = 'user-container';
|
||||
|
||||
const messageContainer = document.createElement('div');
|
||||
messageContainer.className = 'message-container';
|
||||
|
||||
// 安全地格式化消息
|
||||
let formattedContent;
|
||||
try {
|
||||
formattedContent = formatMessage(content);
|
||||
} catch (e) {
|
||||
console.error('Error formatting user message:', e);
|
||||
formattedContent = `<p>${content.replace(/\n/g, '<br>')}</p>`;
|
||||
}
|
||||
|
||||
messageContainer.innerHTML = `
|
||||
<div class="avatar user-avatar">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message">${formattedContent}</div>
|
||||
<div class="timestamp">${formatTimestamp(timestamp)}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
userContainer.appendChild(messageContainer);
|
||||
messagesDiv.appendChild(userContainer);
|
||||
|
||||
// 应用代码高亮
|
||||
setTimeout(() => {
|
||||
applyHighlighting();
|
||||
}, 0);
|
||||
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// 添加加载中的消息
|
||||
function addLoadingMessage() {
|
||||
const botContainer = document.createElement('div');
|
||||
botContainer.className = 'bot-container loading-container';
|
||||
|
||||
const messageContainer = document.createElement('div');
|
||||
messageContainer.className = 'message-container';
|
||||
|
||||
messageContainer.innerHTML = `
|
||||
<div class="avatar bot-avatar">
|
||||
<i class="fas fa-robot"></i>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message">
|
||||
<div class="typing-indicator">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
botContainer.appendChild(messageContainer);
|
||||
messagesDiv.appendChild(botContainer);
|
||||
scrollToBottom();
|
||||
|
||||
return botContainer;
|
||||
}
|
||||
|
||||
// 自动将链接设置为在新标签页打开
|
||||
const externalLinksPlugin = (md) => {
|
||||
// 保存原始的链接渲染器
|
||||
const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) {
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
// 重写链接渲染器
|
||||
md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
|
||||
// 为所有链接添加 target="_blank" 和 rel="noopener noreferrer"
|
||||
const token = tokens[idx];
|
||||
|
||||
// 添加 target="_blank" 属性
|
||||
token.attrPush(['target', '_blank']);
|
||||
|
||||
// 添加 rel="noopener noreferrer" 以提高安全性
|
||||
token.attrPush(['rel', 'noopener noreferrer']);
|
||||
|
||||
// 调用默认渲染器
|
||||
return defaultRender(tokens, idx, options, env, self);
|
||||
};
|
||||
};
|
||||
|
||||
// 替换 formatMessage 函数,使用 markdown-it 替代 marked
|
||||
function formatMessage(content) {
|
||||
try {
|
||||
// 初始化 markdown-it 实例
|
||||
const md = window.markdownit({
|
||||
html: false, // 禁用 HTML 标签
|
||||
xhtmlOut: false, // 使用 '/' 关闭单标签
|
||||
breaks: true, // 将换行符转换为 <br>
|
||||
linkify: true, // 自动将 URL 转换为链接
|
||||
typographer: true, // 启用一些语言中性的替换和引号美化
|
||||
highlight: function(str, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(str, { language: lang }).value;
|
||||
} catch (e) {
|
||||
console.error('Error highlighting code:', e);
|
||||
}
|
||||
}
|
||||
return hljs.highlightAuto(str).value;
|
||||
}
|
||||
});
|
||||
|
||||
// 自动将图片URL转换为图片标签
|
||||
const autoImagePlugin = (md) => {
|
||||
const defaultRender = md.renderer.rules.text || function(tokens, idx, options, env, self) {
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
md.renderer.rules.text = function(tokens, idx, options, env, self) {
|
||||
const token = tokens[idx];
|
||||
const text = token.content.trim();
|
||||
|
||||
// 检测是否完全是一个图片链接 (以https://开头,以图片扩展名结尾)
|
||||
const imageRegex = /^https?:\/\/\S+\.(jpg|jpeg|png|gif|webp)(\?\S*)?$/i;
|
||||
if (imageRegex.test(text)) {
|
||||
return `<img src="${text}" alt="Image" style="max-width: 100%; height: auto;" />`;
|
||||
}
|
||||
|
||||
// 使用默认渲染
|
||||
return defaultRender(tokens, idx, options, env, self);
|
||||
};
|
||||
};
|
||||
|
||||
// 应用插件
|
||||
md.use(autoImagePlugin);
|
||||
|
||||
// 应用外部链接插件
|
||||
md.use(externalLinksPlugin);
|
||||
|
||||
// 渲染 Markdown
|
||||
return md.render(content);
|
||||
} catch (e) {
|
||||
console.error('Error parsing markdown:', e);
|
||||
// 如果解析失败,至少确保换行符正确显示
|
||||
return content.replace(/\n/g, '<br>');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 applyHighlighting 函数
|
||||
function applyHighlighting() {
|
||||
try {
|
||||
document.querySelectorAll('pre code').forEach((block) => {
|
||||
// 确保代码块有正确的类
|
||||
if (!block.classList.contains('hljs')) {
|
||||
block.classList.add('hljs');
|
||||
}
|
||||
|
||||
// 尝试获取语言
|
||||
let language = '';
|
||||
block.classList.forEach(cls => {
|
||||
if (cls.startsWith('language-')) {
|
||||
language = cls.replace('language-', '');
|
||||
}
|
||||
});
|
||||
|
||||
// 应用高亮
|
||||
if (language && hljs.getLanguage(language)) {
|
||||
try {
|
||||
hljs.highlightBlock(block);
|
||||
} catch (e) {
|
||||
console.error('Error highlighting specific language:', e);
|
||||
hljs.highlightAuto(block);
|
||||
}
|
||||
} else {
|
||||
hljs.highlightAuto(block);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error applying code highlighting:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 在 #main-content 上添加点击事件,用于关闭侧边栏
|
||||
document.getElementById('main-content').addEventListener('click', function(event) {
|
||||
// 只在移动视图下且侧边栏打开时处理
|
||||
if (window.innerWidth <= 768 && sidebar.classList.contains('active')) {
|
||||
sidebar.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 阻止侧边栏内部点击事件冒泡到 main-content
|
||||
document.getElementById('sidebar').addEventListener('click', function(event) {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
// 添加遮罩层点击事件,用于关闭侧边栏
|
||||
document.getElementById('sidebar-overlay').addEventListener('click', function() {
|
||||
if (sidebar.classList.contains('active')) {
|
||||
sidebar.classList.remove('active');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
channel/web/static/github.png
Normal file
BIN
channel/web/static/github.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
@@ -2,6 +2,7 @@ import sys
|
||||
import time
|
||||
import web
|
||||
import json
|
||||
import uuid
|
||||
from queue import Queue, Empty
|
||||
from bridge.context import *
|
||||
from bridge.reply import Reply, ReplyType
|
||||
@@ -12,6 +13,8 @@ from common.singleton import singleton
|
||||
from config import conf
|
||||
import os
|
||||
import mimetypes # 添加这行来处理MIME类型
|
||||
import threading
|
||||
import logging
|
||||
|
||||
class WebMessage(ChatMessage):
|
||||
def __init__(
|
||||
@@ -43,39 +46,54 @@ class WebChannel(ChatChannel):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.message_queues = {} # 为每个用户存储一个消息队列
|
||||
self.msg_id_counter = 0 # 添加消息ID计数器
|
||||
self.session_queues = {} # 存储session_id到队列的映射
|
||||
self.request_to_session = {} # 存储request_id到session_id的映射
|
||||
|
||||
def _generate_msg_id(self):
|
||||
"""生成唯一的消息ID"""
|
||||
self.msg_id_counter += 1
|
||||
return str(int(time.time())) + str(self.msg_id_counter)
|
||||
|
||||
def _generate_request_id(self):
|
||||
"""生成唯一的请求ID"""
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def send(self, reply: Reply, context: Context):
|
||||
try:
|
||||
if reply.type in self.NOT_SUPPORT_REPLYTYPE:
|
||||
logger.warning(f"Web channel doesn't support {reply.type} yet")
|
||||
return
|
||||
|
||||
if reply.type == ReplyType.IMAGE_URL:
|
||||
time.sleep(0.5)
|
||||
|
||||
# 获取请求ID和会话ID
|
||||
request_id = context.get("request_id", None)
|
||||
|
||||
if not request_id:
|
||||
logger.error("No request_id found in context, cannot send message")
|
||||
return
|
||||
|
||||
# 获取用户ID
|
||||
user_id = context.get("receiver", None)
|
||||
if not user_id:
|
||||
logger.error("No receiver found in context, cannot send message")
|
||||
# 通过request_id获取session_id
|
||||
session_id = self.request_to_session.get(request_id)
|
||||
if not session_id:
|
||||
logger.error(f"No session_id found for request {request_id}")
|
||||
return
|
||||
|
||||
# 检查是否有响应队列
|
||||
response_queue = context.get("response_queue", None)
|
||||
if response_queue:
|
||||
# 直接将响应放入队列
|
||||
# 检查是否有会话队列
|
||||
if session_id in self.session_queues:
|
||||
# 创建响应数据,包含请求ID以区分不同请求的响应
|
||||
response_data = {
|
||||
"type": str(reply.type),
|
||||
"content": reply.content,
|
||||
"timestamp": time.time()
|
||||
"timestamp": time.time(),
|
||||
"request_id": request_id
|
||||
}
|
||||
response_queue.put(response_data)
|
||||
logger.debug(f"Response sent to queue for user {user_id}")
|
||||
self.session_queues[session_id].put(response_data)
|
||||
logger.debug(f"Response sent to queue for session {session_id}, request {request_id}")
|
||||
else:
|
||||
logger.warning(f"No response queue found for user {user_id}, response dropped")
|
||||
logger.warning(f"No response queue found for session {session_id}, response dropped")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in send method: {e}")
|
||||
@@ -83,57 +101,83 @@ class WebChannel(ChatChannel):
|
||||
def post_message(self):
|
||||
"""
|
||||
Handle incoming messages from users via POST request.
|
||||
Returns a request_id for tracking this specific request.
|
||||
"""
|
||||
try:
|
||||
data = web.data() # 获取原始POST数据
|
||||
json_data = json.loads(data)
|
||||
user_id = json_data.get('user_id', 'default_user')
|
||||
prompt = json_data.get('message', '')
|
||||
session_id = json_data.get('session_id', f'session_{int(time.time())}')
|
||||
except json.JSONDecodeError:
|
||||
return json.dumps({"status": "error", "message": "Invalid JSON"})
|
||||
except Exception as e:
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
if not prompt:
|
||||
return json.dumps({"status": "error", "message": "No message provided"})
|
||||
prompt = json_data.get('message', '')
|
||||
|
||||
try:
|
||||
msg_id = self._generate_msg_id()
|
||||
web_message = WebMessage(
|
||||
msg_id=msg_id,
|
||||
content=prompt,
|
||||
from_user_id=user_id,
|
||||
to_user_id="Chatgpt",
|
||||
other_user_id=user_id
|
||||
)
|
||||
# 生成请求ID
|
||||
request_id = self._generate_request_id()
|
||||
|
||||
context = self._compose_context(ContextType.TEXT, prompt, msg=web_message)
|
||||
if not context:
|
||||
return json.dumps({"status": "error", "message": "Failed to process message"})
|
||||
|
||||
# 创建一个响应队列
|
||||
response_queue = Queue()
|
||||
# 将请求ID与会话ID关联
|
||||
self.request_to_session[request_id] = session_id
|
||||
|
||||
# 确保上下文包含必要的信息
|
||||
context["isgroup"] = False
|
||||
context["receiver"] = user_id
|
||||
context["session_id"] = user_id
|
||||
context["response_queue"] = response_queue
|
||||
|
||||
# 发送消息到处理队列
|
||||
self.produce(context)
|
||||
# 确保会话队列存在
|
||||
if session_id not in self.session_queues:
|
||||
self.session_queues[session_id] = Queue()
|
||||
|
||||
# 等待响应,最多等待30秒
|
||||
try:
|
||||
response = response_queue.get(timeout=120)
|
||||
return json.dumps({"status": "success", "reply": response["content"]})
|
||||
except Empty:
|
||||
return json.dumps({"status": "error", "message": "Response timeout"})
|
||||
# 创建消息对象
|
||||
msg = WebMessage(self._generate_msg_id(), prompt)
|
||||
msg.from_user_id = session_id # 使用会话ID作为用户ID
|
||||
|
||||
# 创建上下文
|
||||
context = self._compose_context(ContextType.TEXT, prompt, msg=msg)
|
||||
|
||||
# 添加必要的字段
|
||||
context["session_id"] = session_id
|
||||
context["request_id"] = request_id
|
||||
context["isgroup"] = False # 添加 isgroup 字段
|
||||
context["receiver"] = session_id # 添加 receiver 字段
|
||||
|
||||
# 异步处理消息 - 只传递上下文
|
||||
threading.Thread(target=self.produce, args=(context,)).start()
|
||||
|
||||
# 返回请求ID
|
||||
return json.dumps({"status": "success", "request_id": request_id})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing message: {e}")
|
||||
return json.dumps({"status": "error", "message": "Internal server error"})
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
def poll_response(self):
|
||||
"""
|
||||
Poll for responses using the session_id.
|
||||
"""
|
||||
try:
|
||||
# 不记录轮询请求的日志
|
||||
web.ctx.log_request = False
|
||||
|
||||
data = web.data()
|
||||
json_data = json.loads(data)
|
||||
session_id = json_data.get('session_id')
|
||||
|
||||
if not session_id or session_id not in self.session_queues:
|
||||
return json.dumps({"status": "error", "message": "Invalid session ID"})
|
||||
|
||||
# 尝试从队列获取响应,不等待
|
||||
try:
|
||||
# 使用peek而不是get,这样如果前端没有成功处理,下次还能获取到
|
||||
response = self.session_queues[session_id].get(block=False)
|
||||
|
||||
# 返回响应,包含请求ID以区分不同请求
|
||||
return json.dumps({
|
||||
"status": "success",
|
||||
"has_content": True,
|
||||
"content": response["content"],
|
||||
"request_id": response["request_id"],
|
||||
"timestamp": response["timestamp"]
|
||||
})
|
||||
|
||||
except Empty:
|
||||
# 没有新响应
|
||||
return json.dumps({"status": "success", "has_content": False})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error polling response: {e}")
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
def chat_page(self):
|
||||
"""Serve the chat HTML page."""
|
||||
@@ -153,6 +197,7 @@ class WebChannel(ChatChannel):
|
||||
urls = (
|
||||
'/', 'RootHandler', # 添加根路径处理器
|
||||
'/message', 'MessageHandler',
|
||||
'/poll', 'PollHandler', # 添加轮询处理器
|
||||
'/chat', 'ChatHandler',
|
||||
'/assets/(.*)', 'AssetsHandler', # 匹配 /assets/任何路径
|
||||
)
|
||||
@@ -163,6 +208,12 @@ class WebChannel(ChatChannel):
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
# 配置web.py的日志级别为ERROR,只显示错误
|
||||
logging.getLogger("web").setLevel(logging.ERROR)
|
||||
|
||||
# 禁用web.httpserver的日志
|
||||
logging.getLogger("web.httpserver").setLevel(logging.ERROR)
|
||||
|
||||
# 临时重定向标准输出,捕获web.py的启动消息
|
||||
with redirect_stdout(io.StringIO()):
|
||||
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
|
||||
@@ -179,6 +230,11 @@ class MessageHandler:
|
||||
return WebChannel().post_message()
|
||||
|
||||
|
||||
class PollHandler:
|
||||
def POST(self):
|
||||
return WebChannel().poll_response()
|
||||
|
||||
|
||||
class ChatHandler:
|
||||
def GET(self):
|
||||
# 正常返回聊天页面
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"channel_type": "wx",
|
||||
"channel_type": "web",
|
||||
"model": "",
|
||||
"open_ai_api_key": "YOUR API KEY",
|
||||
"claude_api_key": "YOUR API KEY",
|
||||
|
||||
278
simple_login_form_test.html
Normal file
278
simple_login_form_test.html
Normal file
@@ -0,0 +1,278 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>登录</title>
|
||||
<style>
|
||||
/* Reset and base */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body, html {
|
||||
margin: 0; padding: 0; height: 100%;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.login-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 2.5rem 3rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.4rem;
|
||||
color: #444;
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"] {
|
||||
padding: 0.6rem 0.8rem;
|
||||
font-size: 1rem;
|
||||
border: 1.8px solid #ccc;
|
||||
border-radius: 6px;
|
||||
transition: border-color 0.3s ease;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
input[type="text"]:focus,
|
||||
input[type="email"]:focus,
|
||||
input[type="password"]:focus {
|
||||
border-color: #667eea;
|
||||
}
|
||||
.password-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.toggle-password {
|
||||
position: absolute;
|
||||
right: 0.8rem;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
color: #667eea;
|
||||
user-select: none;
|
||||
}
|
||||
.login-button {
|
||||
margin-top: 1.5rem;
|
||||
padding: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
background-color: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.login-button:disabled {
|
||||
background-color: #a3a9f7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.forgot-password {
|
||||
margin-top: 1rem;
|
||||
text-align: right;
|
||||
}
|
||||
.forgot-password a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.forgot-password a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.error-message {
|
||||
margin-top: 1rem;
|
||||
color: #d93025;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
.loading-spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 8px;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg);}
|
||||
100% { transform: rotate(360deg);}
|
||||
}
|
||||
/* Responsive */
|
||||
@media (max-width: 480px) {
|
||||
.login-container {
|
||||
margin: 1rem;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container" role="main" aria-label="登录表单">
|
||||
<h2>用户登录</h2>
|
||||
<form id="loginForm" novalidate>
|
||||
<label for="usernameEmail">用户名或邮箱</label>
|
||||
<input type="text" id="usernameEmail" name="usernameEmail" autocomplete="username" placeholder="请输入用户名或邮箱" required aria-describedby="usernameEmailError" />
|
||||
<div id="usernameEmailError" class="error-message" aria-live="polite"></div>
|
||||
|
||||
<label for="password" style="margin-top:1rem;">密码</label>
|
||||
<div class="password-wrapper">
|
||||
<input type="password" id="password" name="password" autocomplete="current-password" placeholder="请输入密码" required minlength="6" aria-describedby="passwordError" />
|
||||
<button type="button" class="toggle-password" aria-label="切换密码可见性" title="切换密码可见性">👁️</button>
|
||||
</div>
|
||||
<div id="passwordError" class="error-message" aria-live="polite"></div>
|
||||
|
||||
<button type="submit" id="loginButton" class="login-button" disabled>登录</button>
|
||||
<div class="forgot-password">
|
||||
<a href="/forgot-password.html" target="_blank" rel="noopener noreferrer">忘记密码?</a>
|
||||
</div>
|
||||
<div id="submitError" class="error-message" aria-live="polite"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const usernameEmailInput = document.getElementById('usernameEmail');
|
||||
const passwordInput = document.getElementById('password');
|
||||
const loginButton = document.getElementById('loginButton');
|
||||
const usernameEmailError = document.getElementById('usernameEmailError');
|
||||
const passwordError = document.getElementById('passwordError');
|
||||
const submitError = document.getElementById('submitError');
|
||||
const togglePasswordBtn = document.querySelector('.toggle-password');
|
||||
const form = document.getElementById('loginForm');
|
||||
|
||||
// 校验用户名或邮箱格式
|
||||
function validateUsernameEmail(value) {
|
||||
if (!value.trim()) {
|
||||
return "用户名或邮箱不能为空";
|
||||
}
|
||||
// 简单邮箱正则
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
// 用户名规则:允许字母数字下划线,长度3-20
|
||||
const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
|
||||
if (emailRegex.test(value)) {
|
||||
return "";
|
||||
} else if (usernameRegex.test(value)) {
|
||||
return "";
|
||||
} else {
|
||||
return "请输入有效的用户名或邮箱格式";
|
||||
}
|
||||
}
|
||||
|
||||
// 校验密码格式
|
||||
function validatePassword(value) {
|
||||
if (!value) {
|
||||
return "密码不能为空";
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return "密码长度不能少于6位";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// 实时校验并更新错误提示和按钮状态
|
||||
function validateForm() {
|
||||
const usernameEmailVal = usernameEmailInput.value;
|
||||
const passwordVal = passwordInput.value;
|
||||
|
||||
const usernameEmailErrMsg = validateUsernameEmail(usernameEmailVal);
|
||||
const passwordErrMsg = validatePassword(passwordVal);
|
||||
|
||||
usernameEmailError.textContent = usernameEmailErrMsg;
|
||||
passwordError.textContent = passwordErrMsg;
|
||||
submitError.textContent = "";
|
||||
|
||||
const isValid = !usernameEmailErrMsg && !passwordErrMsg;
|
||||
loginButton.disabled = !isValid;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
// 密码可见切换
|
||||
togglePasswordBtn.addEventListener('click', () => {
|
||||
if (passwordInput.type === 'password') {
|
||||
passwordInput.type = 'text';
|
||||
togglePasswordBtn.textContent = '🙈';
|
||||
togglePasswordBtn.setAttribute('aria-label', '隐藏密码');
|
||||
togglePasswordBtn.setAttribute('title', '隐藏密码');
|
||||
} else {
|
||||
passwordInput.type = 'password';
|
||||
togglePasswordBtn.textContent = '👁️';
|
||||
togglePasswordBtn.setAttribute('aria-label', '显示密码');
|
||||
togglePasswordBtn.setAttribute('title', '显示密码');
|
||||
}
|
||||
});
|
||||
|
||||
// 监听输入事件实时校验
|
||||
usernameEmailInput.addEventListener('input', validateForm);
|
||||
passwordInput.addEventListener('input', validateForm);
|
||||
|
||||
// 模拟登录请求
|
||||
function fakeLoginRequest(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
// 模拟用户名/邮箱为 "user" 或 "user@example.com" 且密码为 "password123" 才成功
|
||||
const validUsers = ["user", "user@example.com"];
|
||||
if (validUsers.includes(data.usernameEmail.toLowerCase()) && data.password === "password123") {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error("用户名或密码错误"));
|
||||
}
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
// 表单提交处理
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
if (!validateForm()) return;
|
||||
|
||||
loginButton.disabled = true;
|
||||
const originalText = loginButton.textContent;
|
||||
loginButton.textContent = "登录中";
|
||||
const spinner = document.createElement('span');
|
||||
spinner.className = 'loading-spinner';
|
||||
loginButton.appendChild(spinner);
|
||||
submitError.textContent = "";
|
||||
|
||||
try {
|
||||
await fakeLoginRequest({
|
||||
usernameEmail: usernameEmailInput.value.trim(),
|
||||
password: passwordInput.value
|
||||
});
|
||||
// 登录成功跳转(此处用alert模拟)
|
||||
alert("登录成功,跳转到用户主页");
|
||||
// window.location.href = "/user-home.html"; // 实际跳转
|
||||
} catch (err) {
|
||||
submitError.textContent = err.message;
|
||||
} finally {
|
||||
loginButton.disabled = false;
|
||||
loginButton.textContent = originalText;
|
||||
}
|
||||
});
|
||||
|
||||
// 页面加载时校验一次,防止缓存值导致按钮状态异常
|
||||
validateForm();
|
||||
})();
|
||||
<\/script>
|
||||
<\/body>
|
||||
<\/html>
|
||||
Reference in New Issue
Block a user