var CONFIG = {
baseUrl: '', // API基础路径,留空则自动获取
chatUrl: 'https://aiyy.ccut.edu.cn/ai/h5/chat/wszs', // 非流式接口
streamUrl: 'https://aiyy.ccut.edu.cn/ai/h5/stream/wszs', // 流式接口
healthUrl: 'https://aiyy.ccut.edu.cn/ai/health', // 健康检查
timeout: 30000, // 请求超时(ms)
streamTimeout: 120000, // 流式超时(ms)
maxRetry: 2, // 最大重试次数
retryDelay: 1000 // 重试延迟基数(ms)
};
(function(global) {
'use strict';
// ==================== 配置 ====================
var CFG = (global.EnrollFloatConfig || {});
var CONFIG = {
robotImg: CFG.robotImg || 'https://aiyy.ccut.edu.cn/fdai/robot.png',
botName: CFG.botName || '网数小助手',
title: CFG.title || '信息化小助手',
placeholder: CFG.placeholder || '请输入您想咨询的问题...',
welcome: CFG.welcome || JSON.stringify("您好,我是信息化小助手,您在使用校园信息化服务遇到问题可以咨询我。\n").slice(1, -1)
};
// 如果robotImg为空,尝试自动推断
if (!CONFIG.robotImg) {
var scriptPath = (document.currentScript && document.currentScript.src) || '';
if (scriptPath) {
var base = scriptPath.substring(0, scriptPath.lastIndexOf('/') + 1);
CONFIG.robotImg = base.replace(/\/js\//, '/') + 'robot.png';
} else {
CONFIG.robotImg = 'enrollment/robot.png';
}
}
var IMG_REGEX = /(https?:\/\/[^\s"')]+\.(?:jpg|jpeg|png|gif|webp|bmp|svg)(?:\?[^\s"')]*)?)/gi;
// ==================== DOM ====================
var root = null;
var trigger = null;
var win = null;
var messagesEl = null;
var textarea = null;
var sendBtn = null;
// ==================== 状态 ====================
var state = {
open: false,
expanded: false,
sending: false,
aiBubble: null,
aiRawText: '',
greetingIdx: 0
};
var GREETINGS = [
];
// ==================== 初始化 ====================
function init() {
buildDOM();
bindEvents();
appendWelcome();
console.log('[EnrollFloat] 悬浮组件已初始化');
}
function buildDOM() {
root = document.createElement('div');
root.className = 'enroll-float-root';
root.innerHTML =
// ---- 触发器(小机器人) ----
'
' +
'
点我咨询
' +
'
' +
'
 + ')
' +
'
' +
'
' +
// ---- 对话窗口 ----
'' +
' ' +
' ' +
' ' +
'
' +
' ' +
'
' +
'
';
document.body.appendChild(root);
trigger = document.getElementById('efTrigger');
win = document.getElementById('efWindow');
messagesEl = document.getElementById('efMessages');
textarea = document.getElementById('efTextarea');
sendBtn = document.getElementById('efSendBtn');
}
// ==================== 事件绑定 ====================
function bindEvents() {
// 点击触发器 → 打开窗口
trigger.addEventListener('click', openWindow);
// 关闭按钮 → 关闭窗口
document.getElementById('efBtnClose').addEventListener('click', closeWindow);
// 展开按钮 → 展开/还原
document.getElementById('efBtnExpand').addEventListener('click', toggleExpand);
// 输入框
textarea.addEventListener('input', onInput);
textarea.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); doSend(); }
});
// 发送按钮
sendBtn.addEventListener('click', doSend);
}
function onInput() {
sendBtn.disabled = !textarea.value.trim() || state.sending;
autoResize();
}
function autoResize() {
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 90) + 'px';
}
// ==================== 窗口控制 ====================
function openWindow() {
state.open = true;
trigger.classList.add('hidden');
win.classList.add('show');
void win.offsetWidth; // 强制重排
win.classList.add('visible');
setTimeout(function() { textarea.focus(); }, 280);
}
function closeWindow() {
state.open = false; state.expanded = false;
win.classList.remove('visible', 'expanded');
setTimeout(function() {
win.classList.remove('show');
trigger.classList.remove('hidden');
}, 260);
}
function toggleExpand() {
state.expanded = !state.expanded;
win.classList.toggle('expanded', state.expanded);
// 展开后聚焦消息区方便滚动查看
if (state.expanded) messagesEl.scrollTop = messagesEl.scrollHeight;
}
// ==================== 欢迎消息 ====================
function appendWelcome() {
var text = CONFIG.welcome ||
('');
// text = text.replace(/\\\\n/g, "\n");
text = text.replace(/\\n/g, '
');
var el = document.createElement('div');
el.className = 'ef-welcome';
el.innerHTML = '' + text + '
';
messagesEl.appendChild(el);
}
// ==================== 发送 & 接收 ====================
function doSend() {
var msg = textarea.value.trim();
if (!msg || state.sending) return;
appendMsg(msg, 'user');
textarea.value = ''; onInput();
state.sending = true;
state.aiBubble = null; state.aiRawText = '';
showTyping();
AiApi.chatStream(msg, {
onMessage: function(text, isOver) {
if (isOver) { removeTyping(); finishSending(); }
else { appendAiChunk(text); }
},
onSyncData: function(sid) { console.log('[EnrollFloat] Session:', sid); },
onError: function(err) {
removeTyping();
showError((err && err.message) || '出错了,请重试');
finishSending();
}
}).catch(function(e) {
removeTyping();
showError(((e && e.error) || '网络连接异常'));
finishSending();
});
}
function finishSending() {
state.sending = false;
sendBtn.disabled = !textarea.value.trim();
}
// ==================== 消息渲染 ====================
/** 滚动到最新 */
function scrollToBottom() {
requestAnimationFrame(function() {
requestAnimationFrame(function() {
messagesEl.scrollTo({ top: messagesEl.scrollHeight, behavior: 'smooth' });
});
});
}
function appendMsg(text, role) {
var row = document.createElement('div');
row.className = 'ef-msg-row' + (role === 'user' ? ' user' : '');
var isUser = role === 'user';
if (isUser) {
row.innerHTML =
'👤
' +
'' + escHtml(text) + '
';
} else {
row.innerHTML =
'' +
'';
}
messagesEl.appendChild(row);
scrollToBottom();
return row;
}
function appendAiChunk(text) {
if (!state.aiBubble) {
var row = appendMsg('', 'ai');
var bubble = row.querySelector('.ef-bubble');
bubble.innerHTML = '';
state.aiBubble = bubble.querySelector('.ef-md-body');
state.aiRawText = '';
}
state.aiRawText += text;
try {
state.aiBubble.innerHTML = Marked.parse(state.aiRawText) || '';
processImages(state.aiBubble);
} catch (e) {
state.aiBubble.textContent = state.aiRawText;
}
scrollToBottom();
}
function showTyping() {
var row = document.createElement('div');
row.className = 'ef-msg-row'; row.id = 'efTypingRow';
row.innerHTML =
'' +
'';
messagesEl.appendChild(row);
scrollToBottom();
}
function removeTyping() {
var el = document.getElementById('efTypingRow');
if (el) el.remove();
}
function showError(msg) {
var row = document.createElement('div');
row.className = 'ef-msg-row';
row.innerHTML =
'⚠️
' +
'❌ ' + escHtml(msg) + '
';
messagesEl.appendChild(row);
scrollToBottom();
state.aiBubble = null;
}
// 打招呼(点击机器人头像时)
function sayHi() {
var g = GREETINGS[state.greetingIdx % GREETINGS.length];
state.greetingIdx++;
if (!state.open) openWindow();
setTimeout(function() { appendMsg(g, 'ai'); }, 100);
}
// ==================== 图片处理 ====================
function processImages(containerEl) {
if (!containerEl) return;
// 裸URL图片 → img标签
var walker = document.createTreeWalker(containerEl, NodeFilter.SHOW_TEXT, null, false);
var nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
nodes.forEach(function(node) {
var txt = node.textContent;
if (!IMG_REGEX.test(txt)) return;
var frag = document.createDocumentFragment(), lastIdx = 0;
txt.replace(IMG_REGEX, function(m, url, off) {
if (off > lastIdx) frag.appendChild(document.createTextNode(txt.substring(lastIdx, off)));
frag.appendChild(createImg(url.trim()));
lastIdx = off + m.length; return m;
});
if (lastIdx < txt.length) frag.appendChild(document.createTextNode(txt.substring(lastIdx)));
node.parentNode.replaceChild(frag, node);
});
// → 还原为
containerEl.querySelectorAll('a').forEach(function(a) {
var h = (a.getAttribute('href') || '').trim();
if (h && IMG_REGEX.test(h)) {
var img = createImg(h); img.alt = a.textContent.trim() || '图片';
a.parentNode.replaceChild(img, a);
}
});
// 已有
统一样式
containerEl.querySelectorAll('img').forEach(function(img) {
if (img.classList.contains('ef-img')) return;
styleImg(img);
});
}
function createImg(src) {
var img = document.createElement('img'); img.src = src; img.alt = '图片';
img.loading = 'lazy'; styleImg(img); return img;
}
function styleImg(img) {
img.className = 'ef-img';
img.onerror = function() {
this.style.cssText = 'min-height:60px;background:#f1f5f9;display:flex;align-items:center;';
this.alt = '图片加载失败';
};
img.onclick = function(e) { e.preventDefault(); preview(this.src); };
}
function preview(src) {
hidePreview();
var ov = document.createElement('div'); ov.className = 'ef-img-overlay';
ov.id = 'efImgOv';
var img = document.createElement('img'); img.src = src; ov.appendChild(img);
ov.onclick = hidePreview; document.body.appendChild(ov);
}
function hidePreview() {
var el = document.getElementById('efImgOv');
if (el) el.remove();
}
// ==================== 工具函数 ====================
function escHtml(t) { var d=document.createElement('div'); d.textContent=t; return d.innerHTML; }
function escAttr(t) { return String(t).replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,''').replace(//g,'>'); }
// ==================== 启动 ====================
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})(window);