first commit
This commit is contained in:
480
AIEC-server/js/auth-integration.js
Normal file
480
AIEC-server/js/auth-integration.js
Normal file
@ -0,0 +1,480 @@
|
||||
/**
|
||||
* 首页认证集成 - 处理登录状态检查和用户菜单
|
||||
*/
|
||||
class HomeAuthIntegration {
|
||||
|
||||
constructor() {
|
||||
this.currentUser = null;
|
||||
this.isAuthenticated = false;
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
async init() {
|
||||
await this.checkAuthStatus();
|
||||
this.setupEventListeners();
|
||||
this.updateUI();
|
||||
|
||||
// 隐藏加载遮罩
|
||||
const loader = document.getElementById('authCheckLoader');
|
||||
if (loader) {
|
||||
loader.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查认证状态
|
||||
*/
|
||||
async checkAuthStatus() {
|
||||
try {
|
||||
const authStatus = await AuthService.checkAuthStatus();
|
||||
this.isAuthenticated = authStatus.isAuthenticated;
|
||||
this.currentUser = authStatus.user;
|
||||
|
||||
// 如果用户已登录,保存用户信息到localStorage供ChatManager使用
|
||||
if (this.isAuthenticated && this.currentUser) {
|
||||
localStorage.setItem('yundage_current_user', JSON.stringify(this.currentUser));
|
||||
} else {
|
||||
// 如果用户未登录,清除本地存储的用户信息
|
||||
localStorage.removeItem('yundage_current_user');
|
||||
}
|
||||
|
||||
console.log('Auth Status:', authStatus);
|
||||
|
||||
// 触发认证状态检查完成事件
|
||||
window.dispatchEvent(new CustomEvent('authStatusChecked', {
|
||||
detail: {
|
||||
isAuthenticated: this.isAuthenticated,
|
||||
user: this.currentUser
|
||||
}
|
||||
}));
|
||||
|
||||
// 路由保护逻辑
|
||||
this.handleRouteProtection();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to check auth status:', error);
|
||||
this.isAuthenticated = false;
|
||||
this.currentUser = null;
|
||||
|
||||
// 清除本地存储的用户信息
|
||||
localStorage.removeItem('yundage_current_user');
|
||||
|
||||
// 即使检查失败也要触发事件
|
||||
window.dispatchEvent(new CustomEvent('authStatusChecked', {
|
||||
detail: {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
error: error
|
||||
}
|
||||
}));
|
||||
|
||||
// 检查失败时也要处理路由保护
|
||||
this.handleRouteProtection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理路由保护
|
||||
*/
|
||||
handleRouteProtection() {
|
||||
const currentPath = window.location.pathname;
|
||||
const isAuthPage = currentPath.includes('/auth/pages/');
|
||||
const isMainPage = currentPath.includes('/index.html') ||
|
||||
currentPath === '/' ||
|
||||
currentPath.endsWith('/yundage/') ||
|
||||
currentPath.endsWith('/yundage');
|
||||
|
||||
// 如果在登录页面且已登录,跳转到首页
|
||||
if (this.isAuthenticated && isAuthPage) {
|
||||
console.log('已登录用户访问登录页面,跳转到首页');
|
||||
setTimeout(() => {
|
||||
window.location.href = '../../index.html';
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果在主页且未登录,跳转到登录页面
|
||||
if (!this.isAuthenticated && isMainPage) {
|
||||
console.log('未登录用户访问主页,跳转到登录页面');
|
||||
|
||||
// 更新加载遮罩文字
|
||||
const loader = document.getElementById('authCheckLoader');
|
||||
if (loader) {
|
||||
const loaderText = loader.querySelector('p');
|
||||
if (loaderText) {
|
||||
loaderText.textContent = '正在跳转到登录页面...';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示提示
|
||||
this.showNotification('请先登录', 'info');
|
||||
|
||||
// 短暂延迟后跳转
|
||||
setTimeout(() => {
|
||||
window.location.href = 'auth/pages/login.html?redirect=' + encodeURIComponent(window.location.href);
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件监听器
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// 用户菜单切换
|
||||
const userMenuToggle = document.getElementById('userMenuToggle');
|
||||
if (userMenuToggle) {
|
||||
userMenuToggle.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleUserMenu();
|
||||
});
|
||||
}
|
||||
|
||||
// 退出登录按钮
|
||||
const logoutBtn = document.getElementById('logoutBtn');
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.handleLogout();
|
||||
});
|
||||
}
|
||||
|
||||
// 点击页面其他地方关闭菜单
|
||||
document.addEventListener('click', (e) => {
|
||||
const userDropdown = document.getElementById('userDropdown');
|
||||
const userMenuToggle = document.getElementById('userMenuToggle');
|
||||
|
||||
if (userDropdown && userMenuToggle) {
|
||||
if (!userMenuToggle.contains(e.target) && !userDropdown.contains(e.target)) {
|
||||
userDropdown.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 监听存储变化(其他标签页登录/登出)
|
||||
window.addEventListener('storage', (e) => {
|
||||
if (e.key === TokenManager.TOKEN_KEY || e.key === TokenManager.USER_KEY) {
|
||||
this.handleStorageChange();
|
||||
}
|
||||
});
|
||||
|
||||
// 定期检查token状态
|
||||
this.startTokenCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新UI显示
|
||||
*/
|
||||
updateUI() {
|
||||
const guestMenu = document.getElementById('guestMenu');
|
||||
const userMenu = document.getElementById('userMenu');
|
||||
const userName = document.getElementById('userName');
|
||||
const userAccount = document.getElementById('userAccount');
|
||||
|
||||
if (this.isAuthenticated && this.currentUser) {
|
||||
// 显示已登录状态
|
||||
if (guestMenu) guestMenu.classList.add('hidden');
|
||||
if (userMenu) userMenu.classList.remove('hidden');
|
||||
|
||||
// 更新用户信息显示
|
||||
if (userName) {
|
||||
userName.textContent = this.currentUser.username || this.currentUser.nickname || '用户';
|
||||
}
|
||||
if (userAccount) {
|
||||
userAccount.textContent = this.currentUser.phone || this.currentUser.email || '';
|
||||
}
|
||||
|
||||
// 更新用户头像(可选)
|
||||
this.updateUserAvatar();
|
||||
|
||||
// 添加刷新按钮事件(如果存在)
|
||||
this.setupRefreshButton();
|
||||
|
||||
} else {
|
||||
// 显示未登录状态
|
||||
if (guestMenu) guestMenu.classList.remove('hidden');
|
||||
if (userMenu) userMenu.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置刷新按钮
|
||||
*/
|
||||
setupRefreshButton() {
|
||||
const refreshBtn = document.getElementById('refreshUserInfo');
|
||||
if (refreshBtn && !refreshBtn._listenerAdded) {
|
||||
refreshBtn._listenerAdded = true;
|
||||
refreshBtn.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
await this.refreshUserInfo();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新用户信息
|
||||
*/
|
||||
async refreshUserInfo() {
|
||||
try {
|
||||
console.log('手动刷新用户信息...');
|
||||
const result = await AuthService.getUserInfo();
|
||||
|
||||
if (result.success && result.data) {
|
||||
this.currentUser = result.data;
|
||||
// 更新本地存储
|
||||
localStorage.setItem('yundage_current_user', JSON.stringify(this.currentUser));
|
||||
// 更新UI
|
||||
this.updateUI();
|
||||
|
||||
// 显示成功提示
|
||||
this.showNotification('用户信息已更新', 'success');
|
||||
} else {
|
||||
throw new Error(result.message || '获取用户信息失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新用户信息失败:', error);
|
||||
|
||||
// 特殊处理不同类型的错误
|
||||
if (error.message.includes('403')) {
|
||||
this.showNotification('没有权限访问此用户信息', 'warning');
|
||||
} else if (error.message.includes('404')) {
|
||||
this.showNotification('用户不存在', 'error');
|
||||
} else {
|
||||
this.showNotification('刷新失败:' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示通知消息
|
||||
*/
|
||||
showNotification(message, type = 'info') {
|
||||
// 创建通知元素
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed top-4 right-4 p-4 rounded-lg shadow-lg z-50 transition-all duration-300 transform translate-x-full`;
|
||||
|
||||
// 根据类型设置样式
|
||||
const typeStyles = {
|
||||
success: 'bg-green-500 text-white',
|
||||
error: 'bg-red-500 text-white',
|
||||
warning: 'bg-yellow-500 text-white',
|
||||
info: 'bg-blue-500 text-white'
|
||||
};
|
||||
|
||||
notification.className += ` ${typeStyles[type] || typeStyles.info}`;
|
||||
notification.textContent = message;
|
||||
|
||||
// 添加到页面
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 显示动画
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('translate-x-full');
|
||||
}, 100);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
notification.classList.add('translate-x-full');
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换用户菜单显示
|
||||
*/
|
||||
toggleUserMenu() {
|
||||
const userDropdown = document.getElementById('userDropdown');
|
||||
if (userDropdown) {
|
||||
userDropdown.classList.toggle('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理退出登录
|
||||
*/
|
||||
async handleLogout() {
|
||||
try {
|
||||
// 显示确认对话框
|
||||
const confirmed = confirm('确定要退出登录吗?');
|
||||
if (!confirmed) return;
|
||||
|
||||
// 执行登出
|
||||
const result = await AuthService.logout();
|
||||
|
||||
if (result.success) {
|
||||
this.isAuthenticated = false;
|
||||
this.currentUser = null;
|
||||
// 清除localStorage中的用户信息
|
||||
localStorage.removeItem('yundage_current_user');
|
||||
this.updateUI();
|
||||
|
||||
// 隐藏用户菜单
|
||||
const userDropdown = document.getElementById('userDropdown');
|
||||
if (userDropdown) {
|
||||
userDropdown.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 显示成功提示
|
||||
this.showNotification('已退出登录', 'success');
|
||||
|
||||
// 可选:刷新页面
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
this.showNotification(result.message || '退出登录失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
this.showNotification('退出登录失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理存储变化(跨标签页同步)
|
||||
*/
|
||||
async handleStorageChange() {
|
||||
await this.checkAuthStatus();
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户头像
|
||||
*/
|
||||
updateUserAvatar() {
|
||||
// 如果用户有头像,可以在这里更新头像显示
|
||||
// 当前使用默认的太阳云朵图标
|
||||
|
||||
const userMenuToggle = document.getElementById('userMenuToggle');
|
||||
if (userMenuToggle && this.currentUser && this.currentUser.avatar) {
|
||||
// 可以在这里添加自定义头像逻辑
|
||||
userMenuToggle.style.backgroundImage = `url(${this.currentUser.avatar})`;
|
||||
userMenuToggle.style.backgroundSize = 'cover';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示通知消息
|
||||
* @param {string} message - 消息内容
|
||||
* @param {string} type - 消息类型:success/error/info/warning
|
||||
*/
|
||||
showNotification(message, type = 'info') {
|
||||
// 创建通知元素
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed top-20 right-4 z-50 px-4 py-3 rounded-lg shadow-lg max-w-sm transition-all duration-300 transform translate-x-full`;
|
||||
|
||||
// 设置样式
|
||||
switch (type) {
|
||||
case 'success':
|
||||
notification.classList.add('bg-green-500', 'text-white');
|
||||
break;
|
||||
case 'error':
|
||||
notification.classList.add('bg-red-500', 'text-white');
|
||||
break;
|
||||
case 'warning':
|
||||
notification.classList.add('bg-yellow-500', 'text-white');
|
||||
break;
|
||||
default:
|
||||
notification.classList.add('bg-blue-500', 'text-white');
|
||||
}
|
||||
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
|
||||
<span>${message}</span>
|
||||
<button class="ml-2 text-white hover:text-gray-200" onclick="this.parentElement.parentElement.remove()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 显示动画
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('translate-x-full');
|
||||
}, 100);
|
||||
|
||||
// 自动隐藏
|
||||
setTimeout(() => {
|
||||
notification.classList.add('translate-x-full');
|
||||
setTimeout(() => {
|
||||
if (notification.parentElement) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始token检查定时器
|
||||
*/
|
||||
startTokenCheck() {
|
||||
// 每5分钟检查一次token状态
|
||||
setInterval(async () => {
|
||||
const isValid = TokenManager.isTokenValid();
|
||||
|
||||
if (this.isAuthenticated && !isValid) {
|
||||
// Token已过期,尝试刷新
|
||||
const refreshResult = await AuthService.refreshToken();
|
||||
|
||||
if (!refreshResult.success) {
|
||||
// 刷新失败,清除登录状态
|
||||
this.isAuthenticated = false;
|
||||
this.currentUser = null;
|
||||
this.updateUI();
|
||||
this.showNotification('登录已过期,请重新登录', 'warning');
|
||||
}
|
||||
}
|
||||
}, 5 * 60 * 1000); // 5分钟
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要登录才能使用某些功能
|
||||
* @param {Function} callback - 需要登录后执行的回调函数
|
||||
* @param {string} redirectUrl - 登录后的回跳地址
|
||||
*/
|
||||
requireAuth(callback, redirectUrl = null) {
|
||||
if (this.isAuthenticated) {
|
||||
callback();
|
||||
} else {
|
||||
// 未登录,跳转到登录页面
|
||||
const loginUrl = redirectUrl
|
||||
? `auth/pages/login.html?redirect=${encodeURIComponent(redirectUrl)}`
|
||||
: 'auth/pages/login.html';
|
||||
|
||||
window.location.href = loginUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
* @returns {object|null} 用户信息
|
||||
*/
|
||||
getCurrentUser() {
|
||||
return this.currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已登录
|
||||
* @returns {boolean} 是否已登录
|
||||
*/
|
||||
isLoggedIn() {
|
||||
return this.isAuthenticated;
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.homeAuth = new HomeAuthIntegration();
|
||||
});
|
||||
|
||||
// 导出供其他模块使用
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = HomeAuthIntegration;
|
||||
}
|
||||
2
AIEC-server/js/auth-integration.jsZone.Identifier
Normal file
2
AIEC-server/js/auth-integration.jsZone.Identifier
Normal file
@ -0,0 +1,2 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
71
AIEC-server/js/chat-input-height-observer.js
Normal file
71
AIEC-server/js/chat-input-height-observer.js
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 动态监测聊天输入框高度变化
|
||||
* 当输入框高度变化时(如多行文本扩展),自动更新CSS变量
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 获取聊天输入框容器
|
||||
const chatInputContainer = document.getElementById('chatModeInput');
|
||||
|
||||
if (!chatInputContainer) {
|
||||
console.log('Chat input container not found, will retry when chat mode is activated');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建 ResizeObserver 监测高度变化
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
for (let entry of entries) {
|
||||
const height = entry.contentRect.height;
|
||||
|
||||
// 更新CSS变量
|
||||
document.documentElement.style.setProperty('--chat-input-height', height + 'px');
|
||||
|
||||
// 调试信息
|
||||
console.log('Chat input height updated:', height + 'px');
|
||||
}
|
||||
});
|
||||
|
||||
// 开始观察
|
||||
resizeObserver.observe(chatInputContainer);
|
||||
|
||||
// 初始设置高度
|
||||
const initialHeight = chatInputContainer.offsetHeight;
|
||||
if (initialHeight > 0) {
|
||||
document.documentElement.style.setProperty('--chat-input-height', initialHeight + 'px');
|
||||
}
|
||||
|
||||
// 监听聊天模式切换
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||
// 检查是否进入聊天模式
|
||||
if (document.body.classList.contains('chat-mode')) {
|
||||
// 重新计算高度
|
||||
setTimeout(() => {
|
||||
const chatInput = document.getElementById('chatModeInput');
|
||||
if (chatInput) {
|
||||
const height = chatInput.offsetHeight;
|
||||
document.documentElement.style.setProperty('--chat-input-height', height + 'px');
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 观察body的class变化
|
||||
observer.observe(document.body, { attributes: true });
|
||||
});
|
||||
|
||||
// 处理窗口大小变化
|
||||
window.addEventListener('resize', function() {
|
||||
const chatInputContainer = document.getElementById('chatModeInput');
|
||||
if (chatInputContainer && document.body.classList.contains('chat-mode')) {
|
||||
const height = chatInputContainer.offsetHeight;
|
||||
document.documentElement.style.setProperty('--chat-input-height', height + 'px');
|
||||
}
|
||||
});
|
||||
})();
|
||||
513
AIEC-server/js/chat-manager.js
Normal file
513
AIEC-server/js/chat-manager.js
Normal file
@ -0,0 +1,513 @@
|
||||
/**
|
||||
* 对话管理器 - 负责管理聊天会话的保存、加载和持久化
|
||||
*/
|
||||
class ChatManager {
|
||||
constructor() {
|
||||
this.currentChatId = null;
|
||||
this.chats = new Map(); // 存储所有聊天会话
|
||||
this.storageKey = 'yundage_chat_history';
|
||||
this.currentUserKey = 'yundage_current_user';
|
||||
this.initialized = false;
|
||||
|
||||
// 监听认证状态变化事件
|
||||
window.addEventListener('authStatusChecked', () => {
|
||||
if (!this.initialized) {
|
||||
console.log('ChatManager 收到认证状态检查完成事件,开始初始化');
|
||||
this.init();
|
||||
this.initialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 备用方案:如果3秒内没有收到认证事件,强制初始化
|
||||
setTimeout(() => {
|
||||
if (!this.initialized) {
|
||||
console.log('ChatManager 超时初始化');
|
||||
this.init();
|
||||
this.initialized = true;
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
init() {
|
||||
this.loadFromStorage();
|
||||
this.setupAutoSave();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地存储加载聊天记录
|
||||
*/
|
||||
loadFromStorage() {
|
||||
try {
|
||||
// 清空当前聊天记录
|
||||
this.chats.clear();
|
||||
this.currentChatId = null;
|
||||
|
||||
const currentUser = this.getCurrentUser();
|
||||
if (!currentUser) {
|
||||
console.log('没有登录用户,跳过加载');
|
||||
return;
|
||||
}
|
||||
|
||||
const storageKey = `${this.storageKey}_${currentUser.id}`;
|
||||
const currentChatKey = `${this.storageKey}_current_${currentUser.id}`;
|
||||
|
||||
console.log(`[ChatManager] 开始加载聊天记录...`);
|
||||
console.log(`[ChatManager] 存储键: ${storageKey}`);
|
||||
console.log(`[ChatManager] 当前聊天键: ${currentChatKey}`);
|
||||
|
||||
const savedChats = localStorage.getItem(storageKey);
|
||||
const savedCurrentChatId = localStorage.getItem(currentChatKey);
|
||||
|
||||
console.log(`[ChatManager] localStorage中保存的当前聊天ID: ${savedCurrentChatId}`);
|
||||
|
||||
if (savedChats) {
|
||||
const chatsData = JSON.parse(savedChats);
|
||||
this.chats = new Map(chatsData.map(chat => [chat.id, chat]));
|
||||
console.log(`[ChatManager] 成功加载了 ${this.chats.size} 个聊天会话`);
|
||||
console.log(`[ChatManager] 聊天ID列表:`, Array.from(this.chats.keys()));
|
||||
|
||||
// 恢复当前聊天ID
|
||||
if (savedCurrentChatId && this.chats.has(savedCurrentChatId)) {
|
||||
this.currentChatId = savedCurrentChatId;
|
||||
console.log(`[ChatManager] ✅ 成功恢复当前聊天: ${savedCurrentChatId}`);
|
||||
} else if (savedCurrentChatId) {
|
||||
console.log(`[ChatManager] ⚠️ 保存的聊天ID ${savedCurrentChatId} 不在聊天列表中`);
|
||||
} else {
|
||||
console.log(`[ChatManager] 没有保存的当前聊天ID`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[ChatManager] 未找到保存的聊天记录`);
|
||||
}
|
||||
|
||||
console.log(`[ChatManager] 加载完成,当前聊天ID: ${this.currentChatId}`);
|
||||
} catch (error) {
|
||||
console.error('[ChatManager] 加载聊天记录失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存到本地存储
|
||||
*/
|
||||
saveToStorage() {
|
||||
try {
|
||||
const currentUser = this.getCurrentUser();
|
||||
if (!currentUser) {
|
||||
console.log('没有登录用户,跳过保存');
|
||||
return;
|
||||
}
|
||||
|
||||
const storageKey = `${this.storageKey}_${currentUser.id}`;
|
||||
const chatsArray = Array.from(this.chats.values());
|
||||
console.log(`正在保存 ${chatsArray.length} 个聊天到 localStorage,键名: ${storageKey}`);
|
||||
localStorage.setItem(storageKey, JSON.stringify(chatsArray));
|
||||
console.log(`聊天记录已保存 (用户: ${currentUser.username}, ID: ${currentUser.id})`);
|
||||
} catch (error) {
|
||||
console.error('保存聊天记录失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户
|
||||
*/
|
||||
getCurrentUser() {
|
||||
const userStr = localStorage.getItem(this.currentUserKey);
|
||||
return userStr ? JSON.parse(userStr) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自动保存
|
||||
*/
|
||||
setupAutoSave() {
|
||||
// 每30秒自动保存一次
|
||||
setInterval(() => {
|
||||
if (this.currentChatId) {
|
||||
this.saveToStorage();
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
// 页面卸载时保存
|
||||
window.addEventListener('beforeunload', () => {
|
||||
this.saveToStorage();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的聊天会话
|
||||
* @param {string} firstMessage - 第一条消息,用于生成标题
|
||||
* @returns {string} 聊天ID
|
||||
*/
|
||||
createChat(firstMessage = '') {
|
||||
const chatId = `chat_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const chat = {
|
||||
id: chatId,
|
||||
title: this.generateTitle(firstMessage),
|
||||
messages: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
metadata: {
|
||||
messageCount: 0,
|
||||
lastActive: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
this.chats.set(chatId, chat);
|
||||
this.currentChatId = chatId;
|
||||
|
||||
// 保存当前聊天ID
|
||||
const currentUser = this.getCurrentUser();
|
||||
if (currentUser) {
|
||||
const currentChatKey = `${this.storageKey}_current_${currentUser.id}`;
|
||||
localStorage.setItem(currentChatKey, chatId);
|
||||
}
|
||||
|
||||
this.saveToStorage();
|
||||
|
||||
return chatId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成聊天标题
|
||||
* @param {string} firstMessage - 第一条消息
|
||||
* @returns {string} 标题
|
||||
*/
|
||||
generateTitle(firstMessage) {
|
||||
if (!firstMessage) return '新对话';
|
||||
|
||||
// 移除多余的空格和换行
|
||||
const cleaned = firstMessage.trim().replace(/\s+/g, ' ');
|
||||
|
||||
// 如果消息太长,截取前30个字符
|
||||
if (cleaned.length > 30) {
|
||||
return cleaned.substring(0, 30) + '...';
|
||||
}
|
||||
|
||||
return cleaned || '新对话';
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息到当前聊天
|
||||
* @param {string} content - 消息内容
|
||||
* @param {string} type - 消息类型 ('user' 或 'ai')
|
||||
* @param {object} metadata - 额外的元数据
|
||||
*/
|
||||
addMessage(content, type, metadata = {}) {
|
||||
if (!this.currentChatId) {
|
||||
console.error('没有活动的聊天会话');
|
||||
return;
|
||||
}
|
||||
|
||||
const chat = this.chats.get(this.currentChatId);
|
||||
if (!chat) {
|
||||
console.error('聊天会话不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
const message = {
|
||||
id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
content,
|
||||
contentType: metadata.contentType || 'markdown', // 默认使用markdown
|
||||
type,
|
||||
timestamp: new Date().toISOString(),
|
||||
status: type === 'user' ? 'sent' : 'complete',
|
||||
metadata: {
|
||||
deviceInfo: navigator.userAgent,
|
||||
...metadata
|
||||
}
|
||||
};
|
||||
|
||||
chat.messages.push(message);
|
||||
chat.updatedAt = new Date().toISOString();
|
||||
chat.metadata.messageCount = chat.messages.length;
|
||||
chat.metadata.lastActive = new Date().toISOString();
|
||||
|
||||
// 如果是第一条消息,更新标题
|
||||
if (chat.messages.length === 1 && type === 'user') {
|
||||
chat.title = this.generateTitle(content);
|
||||
}
|
||||
|
||||
this.chats.set(this.currentChatId, chat);
|
||||
|
||||
// 触发消息添加事件
|
||||
this.dispatchEvent('messageAdded', { chatId: this.currentChatId, message });
|
||||
|
||||
// 保存到本地存储
|
||||
this.saveToStorage();
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天会话
|
||||
* @param {string} chatId - 聊天ID
|
||||
* @returns {object|null} 聊天会话
|
||||
*/
|
||||
getChat(chatId) {
|
||||
return this.chats.get(chatId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前聊天会话
|
||||
* @returns {object|null} 当前聊天会话
|
||||
*/
|
||||
getCurrentChat() {
|
||||
return this.currentChatId ? this.getChat(this.currentChatId) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到指定聊天
|
||||
* @param {string} chatId - 聊天ID
|
||||
*/
|
||||
switchToChat(chatId) {
|
||||
if (this.chats.has(chatId)) {
|
||||
this.currentChatId = chatId;
|
||||
const chat = this.chats.get(chatId);
|
||||
chat.metadata.lastActive = new Date().toISOString();
|
||||
|
||||
// 保存当前聊天ID
|
||||
const currentUser = this.getCurrentUser();
|
||||
if (currentUser) {
|
||||
const currentChatKey = `${this.storageKey}_current_${currentUser.id}`;
|
||||
localStorage.setItem(currentChatKey, chatId);
|
||||
console.log(`保存当前聊天ID: ${chatId}`);
|
||||
}
|
||||
|
||||
// 触发聊天切换事件
|
||||
this.dispatchEvent('chatSwitched', { chatId, chat });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除聊天会话
|
||||
* @param {string} chatId - 聊天ID
|
||||
*/
|
||||
deleteChat(chatId) {
|
||||
if (this.chats.has(chatId)) {
|
||||
this.chats.delete(chatId);
|
||||
|
||||
// 如果删除的是当前聊天,清空当前聊天ID
|
||||
if (this.currentChatId === chatId) {
|
||||
this.currentChatId = null;
|
||||
|
||||
// 清除保存的当前聊天ID
|
||||
const currentUser = this.getCurrentUser();
|
||||
if (currentUser) {
|
||||
const currentChatKey = `${this.storageKey}_current_${currentUser.id}`;
|
||||
localStorage.removeItem(currentChatKey);
|
||||
}
|
||||
}
|
||||
|
||||
this.saveToStorage();
|
||||
|
||||
// 触发聊天删除事件
|
||||
this.dispatchEvent('chatDeleted', { chatId });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新聊天标题
|
||||
* @param {string} chatId - 聊天ID
|
||||
* @param {string} newTitle - 新标题
|
||||
*/
|
||||
updateChatTitle(chatId, newTitle) {
|
||||
const chat = this.chats.get(chatId);
|
||||
if (chat) {
|
||||
chat.title = newTitle;
|
||||
chat.updatedAt = new Date().toISOString();
|
||||
this.chats.set(chatId, chat);
|
||||
this.saveToStorage();
|
||||
|
||||
// 触发标题更新事件
|
||||
this.dispatchEvent('chatTitleUpdated', { chatId, title: newTitle });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有聊天列表
|
||||
* @param {object} options - 选项
|
||||
* @returns {array} 聊天列表
|
||||
*/
|
||||
getAllChats(options = {}) {
|
||||
const { sortBy = 'updatedAt', order = 'desc', limit = null } = options;
|
||||
|
||||
let chatsArray = Array.from(this.chats.values());
|
||||
|
||||
// 排序
|
||||
chatsArray.sort((a, b) => {
|
||||
const aValue = a[sortBy];
|
||||
const bValue = b[sortBy];
|
||||
|
||||
if (order === 'desc') {
|
||||
return bValue > aValue ? 1 : -1;
|
||||
} else {
|
||||
return aValue > bValue ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
// 限制数量
|
||||
if (limit) {
|
||||
chatsArray = chatsArray.slice(0, limit);
|
||||
}
|
||||
|
||||
return chatsArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索聊天记录
|
||||
* @param {string} query - 搜索关键词
|
||||
* @returns {array} 匹配的聊天列表
|
||||
*/
|
||||
searchChats(query) {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const results = [];
|
||||
|
||||
for (const chat of this.chats.values()) {
|
||||
// 搜索标题
|
||||
if (chat.title.toLowerCase().includes(lowerQuery)) {
|
||||
results.push({
|
||||
chat,
|
||||
matchType: 'title'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 搜索消息内容
|
||||
const matchedMessages = chat.messages.filter(msg =>
|
||||
msg.content.toLowerCase().includes(lowerQuery)
|
||||
);
|
||||
|
||||
if (matchedMessages.length > 0) {
|
||||
results.push({
|
||||
chat,
|
||||
matchType: 'content',
|
||||
matchedMessages
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有聊天记录
|
||||
*/
|
||||
clearAllChats() {
|
||||
this.chats.clear();
|
||||
this.currentChatId = null;
|
||||
this.saveToStorage();
|
||||
|
||||
// 触发清空事件
|
||||
this.dispatchEvent('allChatsCleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出聊天记录
|
||||
* @param {string} chatId - 聊天ID,如果不提供则导出所有
|
||||
* @returns {string} JSON格式的聊天记录
|
||||
*/
|
||||
exportChats(chatId = null) {
|
||||
if (chatId) {
|
||||
const chat = this.chats.get(chatId);
|
||||
return chat ? JSON.stringify(chat, null, 2) : null;
|
||||
} else {
|
||||
const allChats = Array.from(this.chats.values());
|
||||
return JSON.stringify(allChats, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入聊天记录
|
||||
* @param {string} jsonData - JSON格式的聊天记录
|
||||
*/
|
||||
importChats(jsonData) {
|
||||
try {
|
||||
const data = JSON.parse(jsonData);
|
||||
const chatsToImport = Array.isArray(data) ? data : [data];
|
||||
|
||||
for (const chat of chatsToImport) {
|
||||
if (chat.id && chat.messages) {
|
||||
this.chats.set(chat.id, chat);
|
||||
}
|
||||
}
|
||||
|
||||
this.saveToStorage();
|
||||
|
||||
// 触发导入事件
|
||||
this.dispatchEvent('chatsImported', { count: chatsToImport.length });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('导入聊天记录失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天统计信息
|
||||
*/
|
||||
getStatistics() {
|
||||
const totalChats = this.chats.size;
|
||||
let totalMessages = 0;
|
||||
let userMessages = 0;
|
||||
let aiMessages = 0;
|
||||
|
||||
for (const chat of this.chats.values()) {
|
||||
totalMessages += chat.messages.length;
|
||||
userMessages += chat.messages.filter(m => m.type === 'user').length;
|
||||
aiMessages += chat.messages.filter(m => m.type === 'ai').length;
|
||||
}
|
||||
|
||||
return {
|
||||
totalChats,
|
||||
totalMessages,
|
||||
userMessages,
|
||||
aiMessages,
|
||||
averageMessagesPerChat: totalChats > 0 ? Math.round(totalMessages / totalChats) : 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发自定义事件
|
||||
* @param {string} eventName - 事件名称
|
||||
* @param {object} detail - 事件详情
|
||||
*/
|
||||
dispatchEvent(eventName, detail = {}) {
|
||||
const event = new CustomEvent(`chatManager:${eventName}`, { detail });
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始新的聊天会话
|
||||
*/
|
||||
startNewChat() {
|
||||
this.currentChatId = null;
|
||||
|
||||
// 清除保存的当前聊天ID
|
||||
const currentUser = this.getCurrentUser();
|
||||
if (currentUser) {
|
||||
const currentChatKey = `${this.storageKey}_current_${currentUser.id}`;
|
||||
localStorage.removeItem(currentChatKey);
|
||||
}
|
||||
|
||||
this.dispatchEvent('newChatStarted');
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录后重新加载聊天记录
|
||||
*/
|
||||
reloadForUser() {
|
||||
console.log('用户登录状态变化,重新加载聊天记录');
|
||||
this.loadFromStorage();
|
||||
this.dispatchEvent('userChanged');
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局实例
|
||||
window.chatManager = new ChatManager();
|
||||
2
AIEC-server/js/chat-manager.jsZone.Identifier
Normal file
2
AIEC-server/js/chat-manager.jsZone.Identifier
Normal file
@ -0,0 +1,2 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
468
AIEC-server/js/chat-mode-optimizer.js
Normal file
468
AIEC-server/js/chat-mode-optimizer.js
Normal file
@ -0,0 +1,468 @@
|
||||
/**
|
||||
* 聊天模式性能优化
|
||||
* 在聊天模式下禁用轮播和动画以节省资源
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
let animationsPaused = false;
|
||||
let carouselInterval = null;
|
||||
let originalAnimations = new Map();
|
||||
|
||||
/**
|
||||
* 暂停所有CSS动画
|
||||
*/
|
||||
function pauseCSSAnimations() {
|
||||
// 获取所有有动画的元素
|
||||
const animatedElements = document.querySelectorAll('*');
|
||||
|
||||
animatedElements.forEach(element => {
|
||||
const computedStyle = window.getComputedStyle(element);
|
||||
const animationName = computedStyle.animationName;
|
||||
const animationDuration = computedStyle.animationDuration;
|
||||
|
||||
if (animationName && animationName !== 'none') {
|
||||
// 保存原始动画设置
|
||||
originalAnimations.set(element, {
|
||||
animationName: animationName,
|
||||
animationDuration: animationDuration,
|
||||
animationPlayState: computedStyle.animationPlayState
|
||||
});
|
||||
|
||||
// 暂停动画
|
||||
element.style.animationPlayState = 'paused';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复所有CSS动画
|
||||
*/
|
||||
function resumeCSSAnimations() {
|
||||
originalAnimations.forEach((styles, element) => {
|
||||
element.style.animationPlayState = styles.animationPlayState || 'running';
|
||||
});
|
||||
originalAnimations.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止轮播
|
||||
*/
|
||||
function stopCarousel() {
|
||||
// 停止ticker轮播
|
||||
const tickerCarousel = document.getElementById('tickerCarousel');
|
||||
if (tickerCarousel) {
|
||||
tickerCarousel.style.animationPlayState = 'paused';
|
||||
console.log('已暂停ticker轮播');
|
||||
}
|
||||
|
||||
// 停止所有自定义轮播动画
|
||||
const carouselElements = document.querySelectorAll('.ticker-carousel, .news-carousel');
|
||||
carouselElements.forEach(el => {
|
||||
el.style.animationPlayState = 'paused';
|
||||
});
|
||||
|
||||
// 查找并停止所有轮播相关的定时器
|
||||
if (window.tickerInterval) {
|
||||
clearInterval(window.tickerInterval);
|
||||
window.tickerInterval = null;
|
||||
}
|
||||
|
||||
// 停止新闻轮播定时器
|
||||
if (window.newsRotationInterval) {
|
||||
clearInterval(window.newsRotationInterval);
|
||||
window.newsRotationInterval = null;
|
||||
console.log('已停止新闻轮播定时器');
|
||||
}
|
||||
|
||||
// 保存所有活动的定时器ID以便恢复
|
||||
window.pausedIntervals = [];
|
||||
|
||||
// 暴力停止所有定时器(作为后备方案)
|
||||
const highestId = window.setTimeout(function() {
|
||||
for (let i = highestId; i >= 0; i--) {
|
||||
window.clearInterval(i);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// 停止Bootstrap轮播
|
||||
const carousels = document.querySelectorAll('.carousel');
|
||||
carousels.forEach(carousel => {
|
||||
if (typeof $ !== 'undefined' && $(carousel).carousel) {
|
||||
$(carousel).carousel('pause');
|
||||
}
|
||||
});
|
||||
|
||||
// 停止Swiper轮播
|
||||
if (window.Swiper) {
|
||||
const swipers = document.querySelectorAll('.swiper-container');
|
||||
swipers.forEach(swiperEl => {
|
||||
if (swiperEl.swiper) {
|
||||
swiperEl.swiper.autoplay.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复轮播
|
||||
*/
|
||||
function startCarousel() {
|
||||
// 恢复Bootstrap轮播
|
||||
const carousels = document.querySelectorAll('.carousel');
|
||||
carousels.forEach(carousel => {
|
||||
if ($(carousel).carousel) {
|
||||
$(carousel).carousel('cycle');
|
||||
}
|
||||
});
|
||||
|
||||
// 恢复Swiper轮播
|
||||
if (window.Swiper) {
|
||||
const swipers = document.querySelectorAll('.swiper-container');
|
||||
swipers.forEach(swiperEl => {
|
||||
if (swiperEl.swiper) {
|
||||
swiperEl.swiper.autoplay.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用背景视频
|
||||
*/
|
||||
function pauseBackgroundVideos() {
|
||||
const videos = document.querySelectorAll('video');
|
||||
videos.forEach(video => {
|
||||
if (!video.paused) {
|
||||
video.pause();
|
||||
video.dataset.wasPaused = 'false';
|
||||
} else {
|
||||
video.dataset.wasPaused = 'true';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复背景视频
|
||||
*/
|
||||
function resumeBackgroundVideos() {
|
||||
const videos = document.querySelectorAll('video');
|
||||
videos.forEach(video => {
|
||||
if (video.dataset.wasPaused === 'false') {
|
||||
video.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入聊天模式时的优化
|
||||
*/
|
||||
function optimizeForChatMode() {
|
||||
if (animationsPaused) return;
|
||||
|
||||
console.log('进入聊天模式,暂停动画和轮播');
|
||||
|
||||
// 添加聊天模式激活类
|
||||
document.body.classList.add('chat-mode-active');
|
||||
|
||||
// 冻结背景渐变但保持可见
|
||||
const animatedGradient = document.querySelector('.animated-gradient');
|
||||
if (animatedGradient) {
|
||||
// 获取当前背景并冻结
|
||||
const currentStyle = window.getComputedStyle(animatedGradient);
|
||||
const currentBackground = currentStyle.backgroundImage || currentStyle.background;
|
||||
|
||||
// 保存原始样式
|
||||
animatedGradient.dataset.originalBackground = animatedGradient.style.background;
|
||||
animatedGradient.dataset.originalTransition = animatedGradient.style.transition;
|
||||
|
||||
// 设置固定背景
|
||||
animatedGradient.style.background = currentBackground;
|
||||
animatedGradient.style.transition = 'none';
|
||||
|
||||
// 停止requestAnimationFrame动画
|
||||
// 设置标志位阻止动画继续
|
||||
window.pauseBackgroundAnimation = true;
|
||||
|
||||
console.log('已冻结背景渐变动画,保持当前颜色');
|
||||
}
|
||||
|
||||
// 暂停CSS动画但不包括背景和功能性元素
|
||||
const animatedElements = document.querySelectorAll('[style*="animation"]');
|
||||
animatedElements.forEach(el => {
|
||||
// 不处理背景渐变、加载动画和工具按钮
|
||||
if (!el.classList.contains('animated-gradient') &&
|
||||
!el.classList.contains('loading-spinner') &&
|
||||
el.id !== 'chatModeLoadingSpinner' &&
|
||||
el.id !== 'loadingSpinner' &&
|
||||
!el.closest('#toolsMenuBtn')) {
|
||||
el.style.animationPlayState = 'paused';
|
||||
}
|
||||
});
|
||||
|
||||
// 停止轮播
|
||||
stopCarousel();
|
||||
|
||||
// 暂停背景视频
|
||||
pauseBackgroundVideos();
|
||||
|
||||
// 只隐藏3D动画元素
|
||||
const tickerElements = document.querySelectorAll(
|
||||
'.ticker-pulse-ring, .ticker-carousel, .news-carousel, .ticker-flip-card'
|
||||
);
|
||||
tickerElements.forEach(el => {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
|
||||
// 隐藏整个ticker区域
|
||||
const tickerSection = document.querySelector('.ticker-section');
|
||||
if (tickerSection) {
|
||||
tickerSection.style.display = 'none';
|
||||
console.log('已隐藏3D ticker区域');
|
||||
}
|
||||
|
||||
animationsPaused = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出聊天模式时恢复
|
||||
*/
|
||||
function restoreFromChatMode() {
|
||||
if (!animationsPaused) return;
|
||||
|
||||
console.log('退出聊天模式,恢复动画和轮播');
|
||||
|
||||
// 移除聊天模式激活类
|
||||
document.body.classList.remove('chat-mode-active');
|
||||
|
||||
// 恢复背景渐变动画
|
||||
const animatedGradient = document.querySelector('.animated-gradient');
|
||||
if (animatedGradient) {
|
||||
// 恢复原始样式
|
||||
animatedGradient.style.background = animatedGradient.dataset.originalBackground || '';
|
||||
animatedGradient.style.transition = animatedGradient.dataset.originalTransition || '';
|
||||
delete animatedGradient.dataset.originalBackground;
|
||||
delete animatedGradient.dataset.originalTransition;
|
||||
|
||||
// 恢复背景动画
|
||||
window.pauseBackgroundAnimation = false;
|
||||
|
||||
// 重新启动背景动画(如果有initBackground函数)
|
||||
if (window.initBackground && typeof window.initBackground === 'function') {
|
||||
window.initBackground();
|
||||
}
|
||||
|
||||
console.log('已恢复背景渐变动画');
|
||||
}
|
||||
|
||||
// 恢复CSS动画
|
||||
const animatedElements = document.querySelectorAll('[style*="animation"]');
|
||||
animatedElements.forEach(el => {
|
||||
if (el.id !== 'chatModeLoadingSpinner' && el.id !== 'loadingSpinner') {
|
||||
el.style.animationPlayState = '';
|
||||
}
|
||||
});
|
||||
|
||||
// 恢复轮播
|
||||
startCarousel();
|
||||
|
||||
// 恢复背景视频
|
||||
resumeBackgroundVideos();
|
||||
|
||||
// 恢复3D动画元素
|
||||
const tickerElements = document.querySelectorAll(
|
||||
'.ticker-pulse-ring, .ticker-carousel, .news-carousel, .ticker-flip-card'
|
||||
);
|
||||
tickerElements.forEach(el => {
|
||||
el.style.display = '';
|
||||
});
|
||||
|
||||
// 恢复ticker区域
|
||||
const tickerSection = document.querySelector('.ticker-section');
|
||||
if (tickerSection) {
|
||||
tickerSection.style.display = '';
|
||||
console.log('已恢复3D ticker区域');
|
||||
}
|
||||
|
||||
// 重新启动新闻轮播
|
||||
if (window.initTicker3D && typeof window.initTicker3D === 'function') {
|
||||
window.initTicker3D();
|
||||
}
|
||||
|
||||
animationsPaused = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测聊天模式状态
|
||||
*/
|
||||
function checkChatModeStatus() {
|
||||
const chatContainer = document.getElementById('chatContainer');
|
||||
const welcomeScreen = document.getElementById('welcomeScreen');
|
||||
|
||||
// 判断是否在聊天模式
|
||||
const isInChatMode = chatContainer && chatContainer.style.display !== 'none' &&
|
||||
(!welcomeScreen || welcomeScreen.style.display === 'none');
|
||||
|
||||
if (isInChatMode) {
|
||||
optimizeForChatMode();
|
||||
} else {
|
||||
restoreFromChatMode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听模式切换
|
||||
*/
|
||||
function setupModeObserver() {
|
||||
// 监听聊天容器的显示状态变化
|
||||
const chatContainer = document.getElementById('chatContainer');
|
||||
const welcomeScreen = document.getElementById('welcomeScreen');
|
||||
|
||||
if (!chatContainer) {
|
||||
setTimeout(setupModeObserver, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建MutationObserver监听display变化
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes' &&
|
||||
(mutation.attributeName === 'style' || mutation.attributeName === 'class')) {
|
||||
checkChatModeStatus();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 开始观察
|
||||
if (chatContainer) {
|
||||
observer.observe(chatContainer, {
|
||||
attributes: true,
|
||||
attributeFilter: ['style', 'class']
|
||||
});
|
||||
}
|
||||
|
||||
if (welcomeScreen) {
|
||||
observer.observe(welcomeScreen, {
|
||||
attributes: true,
|
||||
attributeFilter: ['style', 'class']
|
||||
});
|
||||
}
|
||||
|
||||
// 初始检查
|
||||
checkChatModeStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加全局样式优化
|
||||
*/
|
||||
function addOptimizationStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'chat-mode-optimization';
|
||||
style.textContent = `
|
||||
/* 保持背景但停止过渡动画 */
|
||||
.chat-mode-active .animated-gradient {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* 隐藏3D ticker区域 */
|
||||
.chat-mode-active .ticker-section {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 隐藏脉冲环动画 */
|
||||
.chat-mode-active .ticker-pulse-ring {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 隐藏3D卡片和轮播 */
|
||||
.chat-mode-active .ticker-carousel,
|
||||
.chat-mode-active .ticker-flip-card,
|
||||
.chat-mode-active .news-carousel {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 聊天模式下隐藏轮播控制器 */
|
||||
.chat-mode-active .carousel-control-prev,
|
||||
.chat-mode-active .carousel-control-next,
|
||||
.chat-mode-active .carousel-indicators {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 确保spin动画关键帧存在 */
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 保持加载动画正常运行 */
|
||||
.chat-mode-active .loading-spinner,
|
||||
.chat-mode-active #loadingSpinner,
|
||||
.chat-mode-active #chatModeLoadingSpinner {
|
||||
animation: spin 1s linear infinite !important;
|
||||
animation-play-state: running !important;
|
||||
}
|
||||
|
||||
/* 只停止装饰性动画,不影响功能性动画 */
|
||||
.chat-mode-active .ticker-pulse-ring,
|
||||
.chat-mode-active .wave-animation {
|
||||
animation-play-state: paused !important;
|
||||
}
|
||||
|
||||
/* 减少GPU使用 */
|
||||
.chat-mode-active .ticker-3d,
|
||||
.chat-mode-active .particles-js,
|
||||
.chat-mode-active .wave-animation {
|
||||
transform: none !important;
|
||||
will-change: auto !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
function init() {
|
||||
// 添加优化样式
|
||||
addOptimizationStyles();
|
||||
|
||||
// DOM加载完成后设置监听
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', setupModeObserver);
|
||||
} else {
|
||||
setupModeObserver();
|
||||
}
|
||||
|
||||
// 监听页面可见性变化
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
// 页面不可见时暂停所有动画
|
||||
optimizeForChatMode();
|
||||
} else {
|
||||
// 页面可见时根据模式恢复
|
||||
checkChatModeStatus();
|
||||
}
|
||||
});
|
||||
|
||||
// 为body添加聊天模式类
|
||||
const originalSendMessage = window.sendMessage;
|
||||
if (originalSendMessage) {
|
||||
window.sendMessage = function() {
|
||||
document.body.classList.add('chat-mode-active');
|
||||
return originalSendMessage.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 启动
|
||||
init();
|
||||
|
||||
// 导出API供外部调用
|
||||
window.chatModeOptimizer = {
|
||||
pause: optimizeForChatMode,
|
||||
resume: restoreFromChatMode,
|
||||
check: checkChatModeStatus
|
||||
};
|
||||
|
||||
})();
|
||||
155
AIEC-server/js/force-text-wrap.js
Normal file
155
AIEC-server/js/force-text-wrap.js
Normal file
@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 强制文本换行处理
|
||||
* 解决列表项中文本不换行的问题
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 处理列表项的换行
|
||||
*/
|
||||
function processListItems() {
|
||||
// 获取所有消息中的列表项
|
||||
const listItems = document.querySelectorAll(
|
||||
'.ai-message li, .user-message li, .message li, #chatMessages li'
|
||||
);
|
||||
|
||||
listItems.forEach(li => {
|
||||
// 获取列表项的计算样式
|
||||
const computedStyle = window.getComputedStyle(li);
|
||||
const parentWidth = li.parentElement.offsetWidth;
|
||||
|
||||
// 如果列表项内容超出父容器宽度,强制设置样式
|
||||
if (li.scrollWidth > parentWidth) {
|
||||
li.style.wordBreak = 'break-all';
|
||||
li.style.overflowWrap = 'anywhere';
|
||||
li.style.whiteSpace = 'normal';
|
||||
li.style.maxWidth = '100%';
|
||||
}
|
||||
|
||||
// 处理列表项中的所有文本节点
|
||||
const walker = document.createTreeWalker(
|
||||
li,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
let textNode;
|
||||
while (textNode = walker.nextNode()) {
|
||||
const text = textNode.textContent;
|
||||
// 检查是否包含长连续字符串(超过30个字符没有空格)
|
||||
if (text && /\S{30,}/.test(text)) {
|
||||
// 在长字符串中插入零宽空格以允许换行
|
||||
const newText = text.replace(/(\S{20})/g, '$1\u200B');
|
||||
if (newText !== text) {
|
||||
textNode.textContent = newText;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置全局样式
|
||||
*/
|
||||
function setGlobalStyles() {
|
||||
// 检查是否已经添加了样式
|
||||
if (document.getElementById('force-wrap-styles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建style元素
|
||||
const style = document.createElement('style');
|
||||
style.id = 'force-wrap-styles';
|
||||
style.textContent = `
|
||||
/* 强制列表项换行 */
|
||||
.ai-message li,
|
||||
.user-message li,
|
||||
.message li,
|
||||
#chatMessages li {
|
||||
word-break: break-all !important;
|
||||
overflow-wrap: anywhere !important;
|
||||
white-space: normal !important;
|
||||
max-width: 100% !important;
|
||||
line-break: anywhere !important;
|
||||
}
|
||||
|
||||
/* 列表容器设置 */
|
||||
.ai-message ul,
|
||||
.ai-message ol,
|
||||
.user-message ul,
|
||||
.user-message ol,
|
||||
.message ul,
|
||||
.message ol,
|
||||
#chatMessages ul,
|
||||
#chatMessages ol {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
`;
|
||||
|
||||
// 添加到head
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听DOM变化
|
||||
*/
|
||||
function setupObserver() {
|
||||
const chatContainer = document.getElementById('chatMessages');
|
||||
if (!chatContainer) {
|
||||
setTimeout(setupObserver, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建观察器
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
// 当有新消息添加时处理
|
||||
let hasNewMessages = false;
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.addedNodes.length > 0) {
|
||||
hasNewMessages = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasNewMessages) {
|
||||
// 延迟处理,等待渲染完成
|
||||
setTimeout(processListItems, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// 开始观察
|
||||
observer.observe(chatContainer, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
function init() {
|
||||
// 设置全局样式
|
||||
setGlobalStyles();
|
||||
|
||||
// DOM加载完成后执行
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
processListItems();
|
||||
setupObserver();
|
||||
});
|
||||
} else {
|
||||
processListItems();
|
||||
setupObserver();
|
||||
}
|
||||
|
||||
// 定期检查(后备方案)
|
||||
setInterval(processListItems, 3000);
|
||||
}
|
||||
|
||||
// 启动
|
||||
init();
|
||||
|
||||
})();
|
||||
2
AIEC-server/js/main-fixed.jsZone.Identifier
Normal file
2
AIEC-server/js/main-fixed.jsZone.Identifier
Normal file
@ -0,0 +1,2 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
@ -0,0 +1,2 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
2852
AIEC-server/js/main.js
Normal file
2852
AIEC-server/js/main.js
Normal file
File diff suppressed because it is too large
Load Diff
2766
AIEC-server/js/main.js.backup
Normal file
2766
AIEC-server/js/main.js.backup
Normal file
File diff suppressed because it is too large
Load Diff
2766
AIEC-server/js/main.js.modified
Normal file
2766
AIEC-server/js/main.js.modified
Normal file
File diff suppressed because it is too large
Load Diff
2
AIEC-server/js/main.jsZone.Identifier
Normal file
2
AIEC-server/js/main.jsZone.Identifier
Normal file
@ -0,0 +1,2 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
2
AIEC-server/js/main.jsZone.IdentifierZone.Identifier
Normal file
2
AIEC-server/js/main.jsZone.IdentifierZone.Identifier
Normal file
@ -0,0 +1,2 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
418
AIEC-server/js/markdown-renderer.js
Normal file
418
AIEC-server/js/markdown-renderer.js
Normal file
@ -0,0 +1,418 @@
|
||||
/**
|
||||
* Markdown渲染器 - 处理消息的Markdown格式化显示
|
||||
*/
|
||||
class MarkdownRenderer {
|
||||
constructor() {
|
||||
this.isMarkdownMode = true; // 默认开启Markdown模式
|
||||
this.codeBlockId = 0; // 用于生成唯一的代码块ID
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 加载用户偏好设置
|
||||
const savedMode = localStorage.getItem('yundage_markdown_mode');
|
||||
if (savedMode !== null) {
|
||||
this.isMarkdownMode = savedMode === 'true';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换Markdown模式
|
||||
*/
|
||||
toggleMarkdownMode() {
|
||||
this.isMarkdownMode = !this.isMarkdownMode;
|
||||
localStorage.setItem('yundage_markdown_mode', this.isMarkdownMode);
|
||||
return this.isMarkdownMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染Markdown内容
|
||||
* @param {string} content - 原始Markdown内容
|
||||
* @param {object} options - 渲染选项
|
||||
* @returns {string} HTML内容
|
||||
*/
|
||||
render(content, options = {}) {
|
||||
if (!this.isMarkdownMode) {
|
||||
// 纯文本模式,只转换换行
|
||||
return this.escapeHtml(content).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
// 先强制替换所有数字序列为符号(有序列表)
|
||||
content = content.replace(/^\d+\.\s+/gm, '▶ ');
|
||||
|
||||
// Markdown模式渲染
|
||||
let html = content;
|
||||
|
||||
// 1. 处理代码块(```语言\n代码\n```)
|
||||
html = this.renderCodeBlocks(html);
|
||||
|
||||
// 2. 处理行内代码(`代码`)
|
||||
html = this.renderInlineCode(html);
|
||||
|
||||
// 3. 处理标题(# ## ### 等)
|
||||
html = this.renderHeaders(html);
|
||||
|
||||
// 4. 处理粗体(**文字** 或 __文字__)
|
||||
html = this.renderBold(html);
|
||||
|
||||
// 5. 处理斜体(*文字* 或 _文字_)
|
||||
html = this.renderItalic(html);
|
||||
|
||||
// 6. 处理链接([文字](链接))
|
||||
html = this.renderLinks(html);
|
||||
|
||||
// 7. 处理图片()
|
||||
html = this.renderImages(html);
|
||||
|
||||
// 8. 处理列表
|
||||
html = this.renderLists(html);
|
||||
|
||||
// 9. 处理引用(> 引用内容)
|
||||
html = this.renderBlockquotes(html);
|
||||
|
||||
// 10. 处理表格
|
||||
html = this.renderTables(html);
|
||||
|
||||
// 11. 处理分隔线(--- 或 ***)
|
||||
html = this.renderHorizontalRules(html);
|
||||
|
||||
// 12. 处理换行
|
||||
html = this.renderLineBreaks(html);
|
||||
|
||||
// 13. 最后强制处理所有数字序列,替换为符号
|
||||
html = html.replace(/^\d+\.\s+/gm, '▸ ');
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义HTML特殊字符
|
||||
*/
|
||||
escapeHtml(text) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染代码块
|
||||
*/
|
||||
renderCodeBlocks(text) {
|
||||
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
|
||||
return text.replace(codeBlockRegex, (match, language, code) => {
|
||||
const id = `code-block-${this.codeBlockId++}`;
|
||||
const escapedCode = this.escapeHtml(code.trim());
|
||||
const lang = language || 'plaintext';
|
||||
|
||||
return `
|
||||
<div class="code-block-wrapper mb-4">
|
||||
<div class="code-header flex justify-between items-center bg-gray-700 text-gray-200 px-3 py-1 rounded-t text-sm">
|
||||
<span class="code-language">${lang}</span>
|
||||
<button class="copy-code-btn hover:bg-gray-600 px-2 py-1 rounded transition-colors" data-code-id="${id}">
|
||||
<i class="fas fa-copy"></i> 复制
|
||||
</button>
|
||||
</div>
|
||||
<pre class="code-block bg-gray-800 text-gray-100 p-4 rounded-b overflow-x-auto" id="${id}"><code class="language-${lang}">${escapedCode}</code></pre>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染行内代码
|
||||
*/
|
||||
renderInlineCode(text) {
|
||||
return text.replace(/`([^`]+)`/g, '<code class="inline-code bg-gray-200 text-red-600 px-1 py-0.5 rounded text-sm">$1</code>');
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染标题
|
||||
*/
|
||||
renderHeaders(text) {
|
||||
// H6 到 H1(从小到大,避免误匹配)
|
||||
for (let i = 6; i >= 1; i--) {
|
||||
const regex = new RegExp(`^${'#'.repeat(i)}\\s+(.+)$`, 'gm');
|
||||
text = text.replace(regex, `<h${i} class="text-${this.getHeaderSize(i)} font-bold mb-3 mt-4">$1</h${i}>`);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
getHeaderSize(level) {
|
||||
const sizes = ['3xl', '2xl', 'xl', 'lg', 'base', 'sm'];
|
||||
return sizes[level - 1] || 'base';
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染粗体
|
||||
*/
|
||||
renderBold(text) {
|
||||
return text
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong class="font-bold">$1</strong>')
|
||||
.replace(/__([^_]+)__/g, '<strong class="font-bold">$1</strong>');
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染斜体
|
||||
*/
|
||||
renderItalic(text) {
|
||||
return text
|
||||
.replace(/\*([^*]+)\*/g, '<em class="italic">$1</em>')
|
||||
.replace(/_([^_]+)_/g, '<em class="italic">$1</em>');
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染链接
|
||||
*/
|
||||
renderLinks(text) {
|
||||
return text.replace(/\[([^\]]+)\]\(([^)]+)\)/g,
|
||||
'<a href="$2" target="_blank" class="text-blue-600 hover:underline">$1</a>');
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染图片
|
||||
*/
|
||||
renderImages(text) {
|
||||
return text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g,
|
||||
'<img src="$2" alt="$1" class="max-w-full rounded shadow-md my-2">');
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染列表
|
||||
*/
|
||||
renderLists(text) {
|
||||
// 简化处理:直接将所有列表项包装为HTML,不添加额外符号
|
||||
|
||||
// 无序列表
|
||||
text = text.replace(/^[\*\-\+]\s+(.+)$/gm, '<li class="ml-4">$1</li>');
|
||||
|
||||
// 有序列表(已经在前面被替换为▶符号了)
|
||||
text = text.replace(/^▶\s+(.+)$/gm, '<li class="ml-4">$1</li>');
|
||||
|
||||
// 统一包装所有列表项
|
||||
text = text.replace(/(<li.*<\/li>\n?)+/g, match => {
|
||||
return `<ul class="list-none mb-3">${match}</ul>`;
|
||||
});
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染引用
|
||||
*/
|
||||
renderBlockquotes(text) {
|
||||
return text.replace(/^>\s+(.+)$/gm,
|
||||
'<blockquote class="border-l-4 border-gray-300 pl-4 py-2 my-2 text-gray-600">$1</blockquote>');
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染表格
|
||||
*/
|
||||
renderTables(text) {
|
||||
const tableRegex = /\|(.+)\|\n\|[\s\-\|]+\|\n((?:\|.+\|\n?)+)/g;
|
||||
|
||||
return text.replace(tableRegex, (match, header, body) => {
|
||||
const headers = header.split('|').filter(h => h.trim());
|
||||
const rows = body.trim().split('\n').map(row =>
|
||||
row.split('|').filter(cell => cell.trim())
|
||||
);
|
||||
|
||||
let tableHtml = '<table class="min-w-full border-collapse border border-gray-300 my-4">';
|
||||
tableHtml += '<thead><tr class="bg-gray-100">';
|
||||
headers.forEach(h => {
|
||||
tableHtml += `<th class="border border-gray-300 px-4 py-2 text-left">${h.trim()}</th>`;
|
||||
});
|
||||
tableHtml += '</tr></thead><tbody>';
|
||||
|
||||
rows.forEach(row => {
|
||||
tableHtml += '<tr class="hover:bg-gray-50">';
|
||||
row.forEach(cell => {
|
||||
tableHtml += `<td class="border border-gray-300 px-4 py-2">${cell.trim()}</td>`;
|
||||
});
|
||||
tableHtml += '</tr>';
|
||||
});
|
||||
|
||||
tableHtml += '</tbody></table>';
|
||||
return tableHtml;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染分隔线
|
||||
*/
|
||||
renderHorizontalRules(text) {
|
||||
return text.replace(/^[\-\*]{3,}$/gm, '<hr class="my-4 border-gray-300">');
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染换行
|
||||
*/
|
||||
renderLineBreaks(text) {
|
||||
return text.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
/**
|
||||
* 为代码块添加复制功能
|
||||
*/
|
||||
initCodeCopyButtons() {
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.copy-code-btn')) {
|
||||
const btn = e.target.closest('.copy-code-btn');
|
||||
const codeId = btn.dataset.codeId;
|
||||
const codeBlock = document.getElementById(codeId);
|
||||
|
||||
if (codeBlock) {
|
||||
const code = codeBlock.textContent;
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
const originalHtml = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fas fa-check"></i> 已复制';
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = originalHtml;
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Markdown工具栏
|
||||
*/
|
||||
createToolbar() {
|
||||
return `
|
||||
<button class="toolbar-btn" data-action="bold" title="粗体">
|
||||
<i class="fas fa-bold"></i>
|
||||
</button>
|
||||
<button class="toolbar-btn" data-action="italic" title="斜体">
|
||||
<i class="fas fa-italic"></i>
|
||||
</button>
|
||||
<button class="toolbar-btn" data-action="code" title="代码">
|
||||
<i class="fas fa-code"></i>
|
||||
</button>
|
||||
<button class="toolbar-btn" data-action="link" title="链接">
|
||||
<i class="fas fa-link"></i>
|
||||
</button>
|
||||
<button class="toolbar-btn" data-action="list" title="列表">
|
||||
<i class="fas fa-list"></i>
|
||||
</button>
|
||||
<button class="toolbar-btn" data-action="quote" title="引用">
|
||||
<i class="fas fa-quote-right"></i>
|
||||
</button>
|
||||
<button class="toolbar-btn toggle-markdown-btn" data-action="toggle-markdown" title="切换Markdown模式" style="margin-left: auto;">
|
||||
<i class="fas fa-markdown"></i>
|
||||
<span class="text-xs" style="margin-left: 0.25rem;">${this.isMarkdownMode ? 'Markdown: ON' : 'Markdown: OFF'}</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理工具栏操作
|
||||
*/
|
||||
handleToolbarAction(action, textarea) {
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const text = textarea.value;
|
||||
const selectedText = text.substring(start, end);
|
||||
|
||||
let replacement = '';
|
||||
let cursorOffset = 0;
|
||||
|
||||
switch (action) {
|
||||
case 'bold':
|
||||
replacement = `**${selectedText || '粗体文字'}**`;
|
||||
cursorOffset = selectedText ? replacement.length : 2;
|
||||
break;
|
||||
case 'italic':
|
||||
replacement = `*${selectedText || '斜体文字'}*`;
|
||||
cursorOffset = selectedText ? replacement.length : 1;
|
||||
break;
|
||||
case 'code':
|
||||
if (selectedText.includes('\n')) {
|
||||
replacement = `\`\`\`\n${selectedText}\n\`\`\``;
|
||||
cursorOffset = 4;
|
||||
} else {
|
||||
replacement = `\`${selectedText || '代码'}\``;
|
||||
cursorOffset = selectedText ? replacement.length : 1;
|
||||
}
|
||||
break;
|
||||
case 'link':
|
||||
replacement = `[${selectedText || '链接文字'}](URL)`;
|
||||
cursorOffset = selectedText ? replacement.length - 4 : 1;
|
||||
break;
|
||||
case 'list':
|
||||
replacement = `- ${selectedText || '列表项'}`;
|
||||
cursorOffset = 2;
|
||||
break;
|
||||
case 'quote':
|
||||
replacement = `> ${selectedText || '引用文字'}`;
|
||||
cursorOffset = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
textarea.value = text.substring(0, start) + replacement + text.substring(end);
|
||||
textarea.selectionStart = start + cursorOffset;
|
||||
textarea.selectionEnd = start + cursorOffset;
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
// 新增:为工具栏绑定事件
|
||||
attachToolbarEvents(toolbar, textarea) {
|
||||
toolbar.addEventListener('click', (e) => {
|
||||
const button = e.target.closest('button[data-action]');
|
||||
if (button) {
|
||||
const action = button.dataset.action;
|
||||
if (action === 'toggle-markdown') {
|
||||
this.toggleMarkdownMode();
|
||||
button.textContent = this.isMarkdownMode ? 'Markdown: ON' : 'Markdown: OFF';
|
||||
} else {
|
||||
this.insertMarkdown(textarea, action);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局实例
|
||||
window.markdownRenderer = new MarkdownRenderer();
|
||||
|
||||
// 初始化工具栏函数
|
||||
function initMarkdownToolbars() {
|
||||
console.log('初始化Markdown工具栏...');
|
||||
|
||||
// 为欢迎模式输入框创建工具栏
|
||||
const welcomeToolbar = document.getElementById('markdownToolbar');
|
||||
const welcomeInput = document.getElementById('messageInput');
|
||||
if (welcomeToolbar && welcomeInput) {
|
||||
console.log('创建欢迎模式工具栏');
|
||||
welcomeToolbar.innerHTML = window.markdownRenderer.createToolbar();
|
||||
welcomeToolbar.style.display = 'flex';
|
||||
window.markdownRenderer.attachToolbarEvents(welcomeToolbar, welcomeInput);
|
||||
}
|
||||
|
||||
// 为聊天模式输入框创建工具栏
|
||||
const chatToolbar = document.getElementById('chatModeMarkdownToolbar');
|
||||
const chatInput = document.getElementById('chatModeMessageInput');
|
||||
if (chatToolbar && chatInput) {
|
||||
console.log('创建聊天模式工具栏');
|
||||
chatToolbar.innerHTML = window.markdownRenderer.createToolbar();
|
||||
chatToolbar.style.display = 'flex';
|
||||
window.markdownRenderer.attachToolbarEvents(chatToolbar, chatInput);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化工具栏
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initMarkdownToolbars();
|
||||
});
|
||||
|
||||
// 确保在所有资源加载完成后也初始化一次
|
||||
window.addEventListener('load', function() {
|
||||
setTimeout(initMarkdownToolbars, 100);
|
||||
});
|
||||
|
||||
// 导出初始化函数供其他模块使用
|
||||
window.initMarkdownToolbars = initMarkdownToolbars;
|
||||
2
AIEC-server/js/markdown-renderer.jsZone.Identifier
Normal file
2
AIEC-server/js/markdown-renderer.jsZone.Identifier
Normal file
@ -0,0 +1,2 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
143
AIEC-server/js/remove-br-tags.js
Normal file
143
AIEC-server/js/remove-br-tags.js
Normal file
@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 强制移除聊天消息中的所有br标签
|
||||
* 用于修复列表项中的过多间距问题
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 移除指定元素内的所有br标签
|
||||
*/
|
||||
function removeBrTags(element) {
|
||||
if (!element) return;
|
||||
|
||||
// 查找所有br标签
|
||||
const brTags = element.querySelectorAll('br');
|
||||
brTags.forEach(br => {
|
||||
// 完全移除br标签
|
||||
br.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理聊天消息
|
||||
*/
|
||||
function processChatMessages() {
|
||||
// 获取聊天消息容器
|
||||
const chatContainer = document.getElementById('chatMessages');
|
||||
if (!chatContainer) return;
|
||||
|
||||
// 移除容器内的所有br标签
|
||||
removeBrTags(chatContainer);
|
||||
|
||||
// 针对特定的消息类
|
||||
const messageSelectors = [
|
||||
'.message',
|
||||
'.message-item',
|
||||
'.ai-message',
|
||||
'.user-message',
|
||||
'.message-content'
|
||||
];
|
||||
|
||||
messageSelectors.forEach(selector => {
|
||||
const elements = chatContainer.querySelectorAll(selector);
|
||||
elements.forEach(element => {
|
||||
removeBrTags(element);
|
||||
});
|
||||
});
|
||||
|
||||
// 特别处理列表项
|
||||
const listItems = chatContainer.querySelectorAll('li');
|
||||
listItems.forEach(li => {
|
||||
removeBrTags(li);
|
||||
|
||||
// 确保列表项内的段落不会产生额外间距
|
||||
const paragraphs = li.querySelectorAll('p');
|
||||
paragraphs.forEach(p => {
|
||||
// 如果段落只包含文本,将其内容直接放到li中
|
||||
if (p.childNodes.length === 1 && p.childNodes[0].nodeType === Node.TEXT_NODE) {
|
||||
const text = p.textContent;
|
||||
const textNode = document.createTextNode(text);
|
||||
p.parentNode.replaceChild(textNode, p);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建MutationObserver监听新消息
|
||||
*/
|
||||
function setupObserver() {
|
||||
const chatContainer = document.getElementById('chatMessages');
|
||||
if (!chatContainer) {
|
||||
// 如果容器还不存在,稍后重试
|
||||
setTimeout(setupObserver, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建观察器
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
// 检查是否有新节点添加
|
||||
if (mutation.addedNodes.length > 0) {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
// 处理新添加的元素
|
||||
removeBrTags(node);
|
||||
|
||||
// 如果是消息元素,进行额外处理
|
||||
if (node.classList && (
|
||||
node.classList.contains('message') ||
|
||||
node.classList.contains('message-item') ||
|
||||
node.classList.contains('ai-message') ||
|
||||
node.classList.contains('user-message')
|
||||
)) {
|
||||
// 处理其中的列表项
|
||||
const listItems = node.querySelectorAll('li');
|
||||
listItems.forEach(li => {
|
||||
removeBrTags(li);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 配置观察器
|
||||
const config = {
|
||||
childList: true,
|
||||
subtree: true
|
||||
};
|
||||
|
||||
// 开始观察
|
||||
observer.observe(chatContainer, config);
|
||||
|
||||
// 立即处理现有消息
|
||||
processChatMessages();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
function init() {
|
||||
// DOM加载完成后执行
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setupObserver();
|
||||
processChatMessages();
|
||||
});
|
||||
} else {
|
||||
setupObserver();
|
||||
processChatMessages();
|
||||
}
|
||||
|
||||
// 定期检查(作为后备方案)
|
||||
setInterval(processChatMessages, 2000);
|
||||
}
|
||||
|
||||
// 启动
|
||||
init();
|
||||
|
||||
})();
|
||||
468
AIEC-server/js/stream-status.js
Normal file
468
AIEC-server/js/stream-status.js
Normal file
@ -0,0 +1,468 @@
|
||||
/**
|
||||
* 流式状态显示组件
|
||||
* 用于显示RAG检索过程的实时状态
|
||||
*/
|
||||
class StreamStatusDisplay {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.statusElements = {};
|
||||
this.retrievalHistory = []; // 记录检索历史
|
||||
this.statusHistory = []; // 记录所有状态历史
|
||||
this.maxDisplayItems = 3; // 最多显示3条状态
|
||||
this.eventQueue = []; // 事件队列
|
||||
this.isProcessing = false; // 是否正在处理队列
|
||||
this.minDisplayTime = 500; // 每个状态最少显示500ms
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 创建状态显示区域
|
||||
this.statusArea = document.createElement('div');
|
||||
this.statusArea.className = 'stream-status-area';
|
||||
this.statusArea.style.cssText = `
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #f6f9fc 0%, #f0f4f8 100%);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
color: #475569;
|
||||
min-height: 48px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
// 添加动画背景
|
||||
const animBg = document.createElement('div');
|
||||
animBg.style.cssText = `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(99, 102, 241, 0.1), transparent);
|
||||
animation: shimmer 2s infinite;
|
||||
`;
|
||||
this.statusArea.appendChild(animBg);
|
||||
|
||||
// 添加CSS动画
|
||||
if (!document.getElementById('stream-status-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'stream-status-styles';
|
||||
style.textContent = `
|
||||
@keyframes shimmer {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.status-icon {
|
||||
font-size: 20px;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
.status-spinner {
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// 状态内容容器
|
||||
this.contentArea = document.createElement('div');
|
||||
this.contentArea.style.cssText = `
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
`;
|
||||
this.statusArea.appendChild(this.contentArea);
|
||||
|
||||
this.container.appendChild(this.statusArea);
|
||||
}
|
||||
|
||||
updateStatus(event) {
|
||||
// 清除淡出定时器
|
||||
if (this.fadeTimeout) {
|
||||
clearTimeout(this.fadeTimeout);
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case 'cache_hit':
|
||||
case 'cache':
|
||||
if (event.cached) {
|
||||
this.showCacheHit();
|
||||
} else {
|
||||
this.showCacheMiss();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'cache_miss':
|
||||
this.showCacheMiss();
|
||||
break;
|
||||
|
||||
case 'starting':
|
||||
this.showStatus('🔍 开始分析查询...', '#6366f1');
|
||||
break;
|
||||
|
||||
case 'complexity_check':
|
||||
const complexity = event.data;
|
||||
if (complexity) {
|
||||
if (complexity.is_complex) {
|
||||
this.showStatus(
|
||||
`📊 复杂查询 (${complexity.level || '高'}) - 置信度: ${(complexity.confidence * 100).toFixed(1)}%`,
|
||||
'#f59e0b'
|
||||
);
|
||||
} else {
|
||||
this.showStatus(
|
||||
`📊 简单查询 - 置信度: ${(complexity.confidence * 100).toFixed(1)}%`,
|
||||
'#10b981'
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'documents':
|
||||
const docs = event.data;
|
||||
if (docs) {
|
||||
// 记录检索历史
|
||||
this.retrievalHistory.push({
|
||||
count: docs.count,
|
||||
new_docs: docs.new_docs || 0,
|
||||
is_incremental: docs.is_incremental || false,
|
||||
retrieval_type: docs.retrieval_type || '检索',
|
||||
retrieval_reason: docs.retrieval_reason || ''
|
||||
});
|
||||
|
||||
// 根据是否是增量检索显示不同信息
|
||||
let message = '';
|
||||
if (docs.is_incremental) {
|
||||
// 增量检索(第二次及以后)
|
||||
const reason = docs.retrieval_reason || '继续检索';
|
||||
message = `📚 ${reason}:新增 ${docs.new_docs || 0} 篇文档(总计 ${docs.count} 篇)`;
|
||||
this.showStatus(message, '#8b5cf6'); // 紫色表示增量
|
||||
} else {
|
||||
// 初始检索
|
||||
message = `📚 已检索 ${docs.count} 篇文档`;
|
||||
if (docs.retrieval_type) {
|
||||
message = `📚 ${docs.retrieval_type}:${docs.count} 篇文档`;
|
||||
}
|
||||
this.showStatus(message, '#6366f1'); // 蓝色表示初始
|
||||
}
|
||||
|
||||
// 显示检索历史汇总
|
||||
if (this.retrievalHistory.length > 1) {
|
||||
this.showRetrievalSummary();
|
||||
}
|
||||
|
||||
// 显示来源文档
|
||||
if (docs.sources && docs.sources.length > 0) {
|
||||
this.addSources(docs.sources);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'sufficiency_check':
|
||||
const sufficient = event.data;
|
||||
if (sufficient) {
|
||||
// 置信度可能是数字或未定义
|
||||
const confidence = sufficient.confidence !== undefined ? sufficient.confidence : 0.5;
|
||||
if (sufficient.is_sufficient) {
|
||||
this.showStatus(
|
||||
`✅ 信息充分 (置信度: ${(confidence * 100).toFixed(1)}%)`,
|
||||
'#10b981'
|
||||
);
|
||||
} else {
|
||||
this.showStatus(
|
||||
`⚠️ 信息不足,继续检索... (置信度: ${(confidence * 100).toFixed(1)}%)`,
|
||||
'#f59e0b'
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'sub_queries':
|
||||
const queries = event.data;
|
||||
if (queries && queries.length > 0) {
|
||||
this.showStatus(`🔄 生成了 ${queries.length} 个子查询`, '#8b5cf6');
|
||||
this.showSubQueries(queries);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'iteration':
|
||||
const iter = event.data;
|
||||
if (iter) {
|
||||
// 只显示当前轮次,不显示最大轮次
|
||||
this.showStatus(`🔄 第 ${iter.current} 轮迭代`, '#6366f1');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'generating':
|
||||
// 不要在这里调用scheduleFadeOut
|
||||
this.showStatus('✨ 正在生成答案...', '#f59e0b'); // 使用黄色表示进行中
|
||||
break;
|
||||
|
||||
case 'cached':
|
||||
this.showStatus('💾 结果已缓存', '#10b981');
|
||||
this.scheduleFadeOut();
|
||||
break;
|
||||
|
||||
case 'complete':
|
||||
this.showStatus('✅ 答案生成完成', '#10b981');
|
||||
// 答案生成完成后,2秒后自动消失
|
||||
setTimeout(() => {
|
||||
this.fadeOutAndHide();
|
||||
}, 2000);
|
||||
break;
|
||||
|
||||
case 'cancelled':
|
||||
// 清除之前的所有状态
|
||||
this.statusHistory = [];
|
||||
// 显示取消状态
|
||||
this.showStatus('⚠️ 任务已停止', '#ef4444');
|
||||
// 1.5秒后淡出
|
||||
setTimeout(() => {
|
||||
this.fadeOutAndHide();
|
||||
}, 1500);
|
||||
break;
|
||||
|
||||
default:
|
||||
if (event.message) {
|
||||
this.showStatus(event.message, '#6366f1');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showStatus(message, color = '#6366f1') {
|
||||
// 添加到历史记录
|
||||
this.statusHistory.push({
|
||||
message: message,
|
||||
color: color,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 使用独立的渲染方法
|
||||
this.renderStatuses();
|
||||
}
|
||||
|
||||
showCacheHit() {
|
||||
this.statusArea.style.background = 'linear-gradient(135deg, #dcfce7 0%, #d1fae5 100%)';
|
||||
this.statusArea.style.borderLeft = '3px solid #10b981';
|
||||
this.contentArea.innerHTML = `
|
||||
<div class="status-icon" style="color: #10b981;">📦</div>
|
||||
<div class="status-message" style="color: #065f46; font-weight: 500;">
|
||||
使用缓存结果(伪流式输出)
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
showCacheMiss() {
|
||||
this.statusArea.style.background = 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)';
|
||||
this.statusArea.style.borderLeft = '3px solid #f59e0b';
|
||||
this.contentArea.innerHTML = `
|
||||
<div class="status-icon status-spinner" style="color: #f59e0b;">🔍</div>
|
||||
<div class="status-message" style="color: #78350f; font-weight: 500;">
|
||||
开始实时检索...
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
showSubQueries(queries) {
|
||||
const maxDisplay = 3;
|
||||
const displayQueries = queries.slice(0, maxDisplay);
|
||||
const remaining = queries.length - maxDisplay;
|
||||
|
||||
// 构建子查询列表HTML
|
||||
const queriesList = displayQueries.map((q, i) =>
|
||||
`${i + 1}. ${q.length > 30 ? q.substring(0, 30) + '...' : q}`
|
||||
).join('<br>');
|
||||
|
||||
const moreText = remaining > 0 ? `<br>... 还有 ${remaining} 个` : '';
|
||||
|
||||
// 更新最后一个状态的消息,添加子查询列表
|
||||
if (this.statusHistory.length > 0) {
|
||||
const lastStatus = this.statusHistory[this.statusHistory.length - 1];
|
||||
// 如果消息中还没有子查询列表,就添加
|
||||
if (!lastStatus.message.includes('<br>')) {
|
||||
lastStatus.message += `<br><span style="color: #4f46e5; font-size: 12px; margin-left: 32px;">${queriesList}${moreText}</span>`;
|
||||
// 重新渲染
|
||||
this.renderStatuses();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showRetrievalSummary() {
|
||||
// 显示检索历史汇总
|
||||
const totalRounds = this.retrievalHistory.length;
|
||||
const totalDocs = this.retrievalHistory[this.retrievalHistory.length - 1].count;
|
||||
|
||||
// 创建一个小的汇总显示
|
||||
const summaryHtml = `
|
||||
<span style="
|
||||
display: inline-block;
|
||||
margin-left: 12px;
|
||||
padding: 2px 8px;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #6366f1;
|
||||
">共 ${totalRounds} 轮检索</span>
|
||||
`;
|
||||
|
||||
// 添加到当前状态显示中
|
||||
if (!this.contentArea.querySelector('.retrieval-summary')) {
|
||||
const summarySpan = document.createElement('span');
|
||||
summarySpan.className = 'retrieval-summary';
|
||||
summarySpan.innerHTML = summaryHtml;
|
||||
this.contentArea.appendChild(summarySpan);
|
||||
} else {
|
||||
this.contentArea.querySelector('.retrieval-summary').innerHTML = summaryHtml;
|
||||
}
|
||||
}
|
||||
|
||||
addSources(sources) {
|
||||
// 不单独添加,而是将来源信息合并到最新的状态消息中
|
||||
const maxDisplay = 3;
|
||||
const displaySources = sources.slice(0, maxDisplay);
|
||||
const sourceText = displaySources.join(', ');
|
||||
const moreText = sources.length > maxDisplay ? ` 等${sources.length}个` : '';
|
||||
|
||||
// 更新最后一个状态的消息,添加来源信息
|
||||
if (this.statusHistory.length > 0) {
|
||||
const lastStatus = this.statusHistory[this.statusHistory.length - 1];
|
||||
// 如果消息中还没有来源信息,就添加
|
||||
if (!lastStatus.message.includes('来源:')) {
|
||||
lastStatus.message += `<br><span style="color: #64748b; font-size: 12px; margin-left: 32px;">来源: ${sourceText}${moreText}</span>`;
|
||||
// 重新渲染
|
||||
this.renderStatuses();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderStatuses() {
|
||||
// 抽取渲染逻辑为独立方法
|
||||
const recentStatuses = this.statusHistory.slice(-this.maxDisplayItems);
|
||||
|
||||
this.contentArea.innerHTML = `
|
||||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||
${recentStatuses.map((status, index) => {
|
||||
const isLatest = index === recentStatuses.length - 1;
|
||||
const opacity = isLatest ? 1 : 0.6;
|
||||
return `
|
||||
<div style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
opacity: ${opacity};
|
||||
padding: ${isLatest ? '8px 0' : '4px 0'};
|
||||
border-bottom: ${!isLatest ? '1px solid rgba(226, 232, 240, 0.5)' : 'none'};
|
||||
">
|
||||
<div class="status-icon" style="color: ${status.color}; font-size: ${isLatest ? '20px' : '16px'};">⚡</div>
|
||||
<div class="status-message" style="
|
||||
color: #334155;
|
||||
font-weight: ${isLatest ? '500' : '400'};
|
||||
font-size: ${isLatest ? '14px' : '12px'};
|
||||
">
|
||||
${status.message}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 更新背景色
|
||||
if (recentStatuses.length > 0) {
|
||||
const latestStatus = recentStatuses[recentStatuses.length - 1];
|
||||
this.statusArea.style.background = `linear-gradient(135deg, ${this.hexToRgba(latestStatus.color, 0.05)} 0%, ${this.hexToRgba(latestStatus.color, 0.1)} 100%)`;
|
||||
this.statusArea.style.borderLeft = `3px solid ${latestStatus.color}`;
|
||||
}
|
||||
}
|
||||
|
||||
scheduleFadeOut() {
|
||||
// 清除之前的定时器
|
||||
if (this.fadeTimeout) {
|
||||
clearTimeout(this.fadeTimeout);
|
||||
}
|
||||
// 5秒后淡出
|
||||
this.fadeTimeout = setTimeout(() => {
|
||||
this.fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
fadeOut() {
|
||||
this.statusArea.style.opacity = '0.3';
|
||||
this.statusArea.style.transform = 'scale(0.98)';
|
||||
// 不完全隐藏,保留淡化状态
|
||||
// setTimeout(() => {
|
||||
// this.statusArea.style.display = 'none';
|
||||
// }, 300);
|
||||
}
|
||||
|
||||
fadeOutAndHide() {
|
||||
// 添加过渡动画
|
||||
this.statusArea.style.transition = 'all 0.5s ease-out';
|
||||
this.statusArea.style.opacity = '0';
|
||||
this.statusArea.style.transform = 'scale(0.95)';
|
||||
this.statusArea.style.maxHeight = this.statusArea.offsetHeight + 'px';
|
||||
|
||||
// 动画结束后完全隐藏
|
||||
setTimeout(() => {
|
||||
this.statusArea.style.maxHeight = '0';
|
||||
this.statusArea.style.padding = '0';
|
||||
this.statusArea.style.margin = '0';
|
||||
this.statusArea.style.overflow = 'hidden';
|
||||
|
||||
// 再过渡一段时间后完全移除显示
|
||||
setTimeout(() => {
|
||||
this.statusArea.style.display = 'none';
|
||||
}, 500);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (this.fadeTimeout) {
|
||||
clearTimeout(this.fadeTimeout);
|
||||
}
|
||||
this.contentArea.innerHTML = '';
|
||||
this.statusArea.style.opacity = '1';
|
||||
this.statusArea.style.transform = 'scale(1)';
|
||||
this.statusArea.style.display = 'flex';
|
||||
this.statusArea.style.background = 'linear-gradient(135deg, #f6f9fc 0%, #f0f4f8 100%)';
|
||||
this.statusArea.style.borderLeft = 'none';
|
||||
// 重置历史记录
|
||||
this.retrievalHistory = [];
|
||||
this.statusHistory = [];
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.fadeTimeout) {
|
||||
clearTimeout(this.fadeTimeout);
|
||||
}
|
||||
if (this.statusArea && this.statusArea.parentNode) {
|
||||
this.statusArea.parentNode.removeChild(this.statusArea);
|
||||
}
|
||||
}
|
||||
|
||||
hexToRgba(hex, alpha) {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出
|
||||
window.StreamStatusDisplay = StreamStatusDisplay;
|
||||
console.log('[StreamStatusDisplay] 组件已加载');
|
||||
154
AIEC-server/js/switch-toggle-bridge.js
Normal file
154
AIEC-server/js/switch-toggle-bridge.js
Normal file
@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 开关按钮与原有逻辑的桥接
|
||||
* 保持原有的事件处理逻辑不变
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// DOM加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 获取欢迎模式开关元素
|
||||
const deepResearchToggle = document.getElementById('deepResearchToggle');
|
||||
const showThinkingToggle = document.getElementById('showThinkingToggle');
|
||||
|
||||
// 获取聊天模式开关元素
|
||||
const chatModeDeepResearchToggle = document.getElementById('chatModeDeepResearchToggle');
|
||||
const chatModeShowThinkingToggle = document.getElementById('chatModeShowThinkingToggle');
|
||||
|
||||
// 获取原有的菜单项元素
|
||||
const deepResearchItem = document.getElementById('deepResearchItem');
|
||||
const showThinkingItem = document.getElementById('showThinkingItem');
|
||||
const chatModeDeepResearchItem = document.getElementById('chatModeDeepResearchItem');
|
||||
const chatModeShowThinkingItem = document.getElementById('chatModeShowThinkingItem');
|
||||
|
||||
// 获取状态显示元素
|
||||
const deepResearchStatus = document.getElementById('deepResearchStatus');
|
||||
const showThinkingStatus = document.getElementById('showThinkingStatus');
|
||||
const chatModeDeepResearchStatus = document.getElementById('chatModeDeepResearchStatus');
|
||||
const chatModeShowThinkingStatus = document.getElementById('chatModeShowThinkingStatus');
|
||||
|
||||
// 初始化开关状态为关闭
|
||||
if (deepResearchToggle) {
|
||||
deepResearchToggle.checked = false;
|
||||
}
|
||||
if (showThinkingToggle) {
|
||||
showThinkingToggle.checked = false;
|
||||
}
|
||||
if (chatModeDeepResearchToggle) {
|
||||
chatModeDeepResearchToggle.checked = false;
|
||||
}
|
||||
if (chatModeShowThinkingToggle) {
|
||||
chatModeShowThinkingToggle.checked = false;
|
||||
}
|
||||
|
||||
// 深度研究开关处理
|
||||
if (deepResearchToggle && deepResearchItem) {
|
||||
// 标记避免循环触发
|
||||
let isUpdatingFromToggle = false;
|
||||
|
||||
deepResearchToggle.addEventListener('change', function() {
|
||||
if (isUpdatingFromToggle) return;
|
||||
|
||||
// 设置标记
|
||||
isUpdatingFromToggle = true;
|
||||
|
||||
// 触发原有的点击事件
|
||||
deepResearchItem.click();
|
||||
|
||||
// 延迟重置标记
|
||||
setTimeout(() => {
|
||||
isUpdatingFromToggle = false;
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// 监听原有逻辑的状态变化
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'childList' || mutation.type === 'characterData') {
|
||||
const status = deepResearchStatus ? deepResearchStatus.textContent : '关';
|
||||
deepResearchToggle.checked = status === '开';
|
||||
// 同步到聊天模式的开关
|
||||
if (chatModeDeepResearchToggle) {
|
||||
chatModeDeepResearchToggle.checked = status === '开';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (deepResearchStatus) {
|
||||
observer.observe(deepResearchStatus, { childList: true, characterData: true, subtree: true });
|
||||
}
|
||||
}
|
||||
|
||||
// 显示思考开关处理 - 已禁用
|
||||
if (showThinkingToggle) {
|
||||
// 禁用开关,不添加任何事件监听
|
||||
showThinkingToggle.disabled = true;
|
||||
showThinkingToggle.checked = false; // 始终保持关闭状态
|
||||
}
|
||||
|
||||
// 聊天模式深度研究开关处理
|
||||
if (chatModeDeepResearchToggle && chatModeDeepResearchItem) {
|
||||
// 标记避免循环触发
|
||||
let isUpdatingFromToggle = false;
|
||||
|
||||
chatModeDeepResearchToggle.addEventListener('change', function() {
|
||||
if (isUpdatingFromToggle) return;
|
||||
|
||||
// 设置标记
|
||||
isUpdatingFromToggle = true;
|
||||
|
||||
// 触发原有的点击事件
|
||||
chatModeDeepResearchItem.click();
|
||||
|
||||
// 延迟重置标记
|
||||
setTimeout(() => {
|
||||
isUpdatingFromToggle = false;
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// 监听原有逻辑的状态变化
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'childList' || mutation.type === 'characterData') {
|
||||
const status = chatModeDeepResearchStatus ? chatModeDeepResearchStatus.textContent : '关';
|
||||
chatModeDeepResearchToggle.checked = status === '开';
|
||||
// 同步两个模式的开关状态
|
||||
if (deepResearchToggle) {
|
||||
deepResearchToggle.checked = status === '开';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (chatModeDeepResearchStatus) {
|
||||
observer.observe(chatModeDeepResearchStatus, { childList: true, characterData: true, subtree: true });
|
||||
}
|
||||
}
|
||||
|
||||
// 聊天模式显示思考开关处理 - 已禁用
|
||||
if (chatModeShowThinkingToggle) {
|
||||
// 禁用开关,不添加任何事件监听
|
||||
chatModeShowThinkingToggle.disabled = true;
|
||||
chatModeShowThinkingToggle.checked = false; // 始终保持关闭状态
|
||||
}
|
||||
|
||||
// 阻止菜单项本身的点击事件冒泡到开关
|
||||
[deepResearchItem, showThinkingItem, chatModeDeepResearchItem, chatModeShowThinkingItem].forEach(item => {
|
||||
if (item) {
|
||||
const originalClick = item.onclick;
|
||||
item.onclick = function(e) {
|
||||
// 如果点击的是开关,不执行原有逻辑
|
||||
if (e.target.closest('.switch')) {
|
||||
return;
|
||||
}
|
||||
// 否则执行原有逻辑
|
||||
if (originalClick) {
|
||||
originalClick.call(this, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
373
AIEC-server/js/viewport-handler.js
Normal file
373
AIEC-server/js/viewport-handler.js
Normal file
@ -0,0 +1,373 @@
|
||||
/**
|
||||
* 视口和缩放处理器
|
||||
* 处理页面缩放、视口变化和响应式布局
|
||||
*/
|
||||
class ViewportHandler {
|
||||
constructor() {
|
||||
this.currentZoom = 1;
|
||||
this.breakpoints = {
|
||||
xs: 320,
|
||||
sm: 640,
|
||||
md: 768,
|
||||
lg: 1024,
|
||||
xl: 1280,
|
||||
'2xl': 1536
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 检测初始缩放级别
|
||||
this.detectZoomLevel();
|
||||
|
||||
// 设置事件监听器
|
||||
this.setupEventListeners();
|
||||
|
||||
// 应用初始视口设置
|
||||
this.applyViewportSettings();
|
||||
|
||||
// 处理初始布局
|
||||
this.handleViewportChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测浏览器缩放级别
|
||||
*/
|
||||
detectZoomLevel() {
|
||||
// 方法1:使用 window.devicePixelRatio
|
||||
const pixelRatio = window.devicePixelRatio || 1;
|
||||
|
||||
// 方法2:使用 outerWidth 和 innerWidth 比较
|
||||
const zoomLevel = Math.round((window.outerWidth / window.innerWidth) * 100) / 100;
|
||||
|
||||
// 方法3:使用媒体查询检测
|
||||
const mqString = `(resolution: ${window.devicePixelRatio}dppx)`;
|
||||
const mq = window.matchMedia(mqString);
|
||||
|
||||
this.currentZoom = pixelRatio;
|
||||
this.updateZoomClasses();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件监听器
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// 监听窗口大小变化
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
this.handleViewportChange();
|
||||
this.detectZoomLevel();
|
||||
}, 250);
|
||||
});
|
||||
|
||||
// 监听缩放变化
|
||||
window.addEventListener('wheel', (e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
setTimeout(() => {
|
||||
this.detectZoomLevel();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听方向变化(移动设备)
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setTimeout(() => {
|
||||
this.handleViewportChange();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// 监听媒体查询变化
|
||||
this.setupMediaQueryListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置媒体查询监听器
|
||||
*/
|
||||
setupMediaQueryListeners() {
|
||||
// 监听不同断点
|
||||
Object.entries(this.breakpoints).forEach(([name, width]) => {
|
||||
const mq = window.matchMedia(`(min-width: ${width}px)`);
|
||||
mq.addListener((e) => {
|
||||
if (e.matches) {
|
||||
this.onBreakpointChange(name, width);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 监听高DPI屏幕
|
||||
const highDpiQuery = window.matchMedia('(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)');
|
||||
highDpiQuery.addListener((e) => {
|
||||
if (e.matches) {
|
||||
document.body.classList.add('high-dpi');
|
||||
} else {
|
||||
document.body.classList.remove('high-dpi');
|
||||
}
|
||||
});
|
||||
|
||||
// 监听触摸设备
|
||||
const touchQuery = window.matchMedia('(hover: none) and (pointer: coarse)');
|
||||
touchQuery.addListener((e) => {
|
||||
if (e.matches) {
|
||||
document.body.classList.add('touch-device');
|
||||
} else {
|
||||
document.body.classList.remove('touch-device');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理视口变化
|
||||
*/
|
||||
handleViewportChange() {
|
||||
const viewport = {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
orientation: window.innerWidth > window.innerHeight ? 'landscape' : 'portrait',
|
||||
zoom: this.currentZoom
|
||||
};
|
||||
|
||||
// 更新CSS变量
|
||||
this.updateCSSVariables(viewport);
|
||||
|
||||
// 调整布局
|
||||
this.adjustLayout(viewport);
|
||||
|
||||
// 优化字体大小
|
||||
this.optimizeFontSize(viewport);
|
||||
|
||||
// 处理特殊组件
|
||||
this.handleSpecialComponents(viewport);
|
||||
|
||||
// 触发自定义事件
|
||||
window.dispatchEvent(new CustomEvent('viewportChanged', { detail: viewport }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新CSS变量
|
||||
*/
|
||||
updateCSSVariables(viewport) {
|
||||
const root = document.documentElement;
|
||||
|
||||
// 视口尺寸
|
||||
root.style.setProperty('--vw', `${viewport.width * 0.01}px`);
|
||||
root.style.setProperty('--vh', `${viewport.height * 0.01}px`);
|
||||
root.style.setProperty('--vmin', `${Math.min(viewport.width, viewport.height) * 0.01}px`);
|
||||
root.style.setProperty('--vmax', `${Math.max(viewport.width, viewport.height) * 0.01}px`);
|
||||
|
||||
// 缩放相关
|
||||
root.style.setProperty('--zoom-level', this.currentZoom);
|
||||
root.style.setProperty('--base-font-size', `${16 / this.currentZoom}px`);
|
||||
|
||||
// 响应式间距
|
||||
const spacingUnit = Math.max(4, Math.min(8, viewport.width / 200));
|
||||
root.style.setProperty('--spacing-unit', `${spacingUnit}px`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新缩放相关的CSS类
|
||||
*/
|
||||
updateZoomClasses() {
|
||||
const body = document.body;
|
||||
|
||||
// 移除旧的缩放类
|
||||
body.classList.remove('zoom-50', 'zoom-75', 'zoom-90', 'zoom-100', 'zoom-110', 'zoom-125', 'zoom-150', 'zoom-200');
|
||||
|
||||
// 添加新的缩放类
|
||||
if (this.currentZoom <= 0.5) {
|
||||
body.classList.add('zoom-50');
|
||||
} else if (this.currentZoom <= 0.75) {
|
||||
body.classList.add('zoom-75');
|
||||
} else if (this.currentZoom <= 0.9) {
|
||||
body.classList.add('zoom-90');
|
||||
} else if (this.currentZoom <= 1.1) {
|
||||
body.classList.add('zoom-100');
|
||||
} else if (this.currentZoom <= 1.25) {
|
||||
body.classList.add('zoom-110');
|
||||
} else if (this.currentZoom <= 1.5) {
|
||||
body.classList.add('zoom-125');
|
||||
} else if (this.currentZoom <= 2) {
|
||||
body.classList.add('zoom-150');
|
||||
} else {
|
||||
body.classList.add('zoom-200');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整布局
|
||||
*/
|
||||
adjustLayout(viewport) {
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
|
||||
// 小屏幕自动折叠侧边栏
|
||||
if (viewport.width < 768 && sidebar && !sidebar.classList.contains('collapsed')) {
|
||||
// 触发侧边栏折叠
|
||||
const toggleBtn = document.getElementById('toggleSidebar');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
// 调整聊天消息容器高度
|
||||
const chatMessages = document.getElementById('chatMessages');
|
||||
if (chatMessages) {
|
||||
const headerHeight = document.querySelector('header')?.offsetHeight || 0;
|
||||
const inputHeight = document.getElementById('chatModeInput')?.offsetHeight || 0;
|
||||
const availableHeight = viewport.height - headerHeight - inputHeight - 40; // 40px for padding
|
||||
chatMessages.style.maxHeight = `${availableHeight}px`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化字体大小
|
||||
*/
|
||||
optimizeFontSize(viewport) {
|
||||
// 根据视口宽度计算基础字体大小
|
||||
let baseFontSize = 16;
|
||||
|
||||
if (viewport.width < 360) {
|
||||
baseFontSize = 14;
|
||||
} else if (viewport.width < 768) {
|
||||
baseFontSize = 15;
|
||||
} else if (viewport.width > 1920) {
|
||||
baseFontSize = 18;
|
||||
}
|
||||
|
||||
// 应用缩放调整
|
||||
baseFontSize = baseFontSize / this.currentZoom;
|
||||
|
||||
// 设置根字体大小
|
||||
document.documentElement.style.fontSize = `${baseFontSize}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理特殊组件
|
||||
*/
|
||||
handleSpecialComponents(viewport) {
|
||||
// 3D新闻卡片调整
|
||||
const ticker3D = document.querySelector('.ticker-3d-container');
|
||||
const tickerSection = document.querySelector('.ticker-section');
|
||||
|
||||
if (ticker3D) {
|
||||
// 根据缩放级别调整3D卡片
|
||||
if (this.currentZoom > 1.5) {
|
||||
// 高缩放时隐藏
|
||||
if (tickerSection) tickerSection.style.display = 'none';
|
||||
} else if (this.currentZoom > 1.25) {
|
||||
// 中等缩放时缩小
|
||||
ticker3D.style.transform = 'scale(0.7)';
|
||||
ticker3D.style.maxHeight = '80px';
|
||||
if (tickerSection) tickerSection.style.display = 'block';
|
||||
} else if (viewport.width < 768) {
|
||||
ticker3D.style.transform = 'scale(0.8)';
|
||||
if (tickerSection) tickerSection.style.display = 'block';
|
||||
} else if (viewport.width < 1024) {
|
||||
ticker3D.style.transform = 'scale(0.9)';
|
||||
if (tickerSection) tickerSection.style.display = 'block';
|
||||
} else {
|
||||
ticker3D.style.transform = 'scale(1)';
|
||||
ticker3D.style.maxHeight = '';
|
||||
if (tickerSection) tickerSection.style.display = 'block';
|
||||
}
|
||||
|
||||
// 3D卡片现在固定在底部,不需要检测重叠
|
||||
}
|
||||
|
||||
// 文字轮播调整
|
||||
const rotatingWord = document.querySelector('.rotating-word-3d');
|
||||
if (rotatingWord) {
|
||||
if (viewport.width < 480) {
|
||||
rotatingWord.style.fontSize = '1rem';
|
||||
} else if (viewport.width < 768) {
|
||||
rotatingWord.style.fontSize = '1.25rem';
|
||||
} else {
|
||||
rotatingWord.style.fontSize = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用视口设置
|
||||
*/
|
||||
applyViewportSettings() {
|
||||
// 设置视口meta标签
|
||||
let viewportMeta = document.querySelector('meta[name="viewport"]');
|
||||
if (!viewportMeta) {
|
||||
viewportMeta = document.createElement('meta');
|
||||
viewportMeta.name = 'viewport';
|
||||
document.head.appendChild(viewportMeta);
|
||||
}
|
||||
|
||||
// 根据设备类型设置不同的视口配置
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
|
||||
if (isMobile) {
|
||||
viewportMeta.content = 'width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes';
|
||||
} else {
|
||||
viewportMeta.content = 'width=device-width, initial-scale=1.0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断点变化处理
|
||||
*/
|
||||
onBreakpointChange(name, width) {
|
||||
console.log(`Breakpoint changed to: ${name} (${width}px)`);
|
||||
|
||||
// 触发自定义事件
|
||||
window.dispatchEvent(new CustomEvent('breakpointChanged', {
|
||||
detail: { name, width }
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前断点
|
||||
*/
|
||||
getCurrentBreakpoint() {
|
||||
const width = window.innerWidth;
|
||||
let current = 'xs';
|
||||
|
||||
for (const [name, breakpoint] of Object.entries(this.breakpoints)) {
|
||||
if (width >= breakpoint) {
|
||||
current = name;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为移动设备
|
||||
*/
|
||||
isMobile() {
|
||||
return window.innerWidth < this.breakpoints.md ||
|
||||
('ontouchstart' in window) ||
|
||||
(navigator.maxTouchPoints > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为高DPI屏幕
|
||||
*/
|
||||
isHighDPI() {
|
||||
return window.devicePixelRatio > 1 ||
|
||||
(window.matchMedia && window.matchMedia('(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)').matches);
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制重新计算布局
|
||||
*/
|
||||
forceRecalculate() {
|
||||
this.detectZoomLevel();
|
||||
this.handleViewportChange();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局实例
|
||||
window.viewportHandler = new ViewportHandler();
|
||||
|
||||
// 导出给其他模块使用
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ViewportHandler;
|
||||
}
|
||||
Reference in New Issue
Block a user