// 会话管理模块
import {getCurrentSessionId} from './dom-utils.js';
import {customAlert, customConfirm} from './modal.js';
import {highlightCodeBlocks} from './highlight.js';
import {scrollToBottom} from './dom-utils.js';
let pendingSessionElement = null;
export function getPendingSessionElement() {
return pendingSessionElement;
}
export async function createNewSession(sessionList, sessionSidebar, sidebarOverlay, switchSession) {
const existingPending = document.querySelector('.session-item.pending');
if (existingPending) {
await customAlert('已有待创建的新会话,请先发送消息完成创建', '提示');
document.querySelectorAll('.session-item').forEach(item => item.classList.remove('active'));
existingPending.classList.add('active');
return;
}
try {
// 尝试切换到空会话,但不阻止创建临时会话项(即使失败也继续)
try {
await switchSession('');
} catch (err) {
console.warn('切换空会话时出错,但继续创建新会话:', err);
}
document.querySelectorAll('.session-item').forEach(item => item.classList.remove('active'));
const tempSessionId = 'temp_' + Date.now();
const tempSession = document.createElement('div');
tempSession.className = 'session-item pending active';
tempSession.dataset.sessionId = tempSessionId;
tempSession.dataset.isNewSession = 'true';
tempSession.innerHTML = `
<div class="session-info">
<div class="session-name">新建会话...</div>
<div class="session-meta">
<span class="session-time">${new Date().toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})}</span>
<span class="session-count">0 条</span>
</div>
</div>
`;
tempSession.querySelector('.session-info').addEventListener('click', async () => {
if (tempSession.dataset.isNewSession === 'true') {
const currentId = getCurrentSessionId() || '';
if (currentId !== '') {
try {
await switchSession('');
document.querySelectorAll('.session-item').forEach(i => i.classList.remove('active'));
tempSession.classList.add('active');
} catch (err) {
console.warn('切换回新会话时出错:', err);
}
}
}
});
if (sessionList) {
sessionList.insertBefore(tempSession, sessionList.firstChild);
}
pendingSessionElement = tempSession;
// 确保 currentSessionId 隐藏域被清空
const currentSessionIdEl = document.getElementById('currentSessionId');
if (currentSessionIdEl) {
currentSessionIdEl.value = '';
}
if (window.innerWidth <= 600) {
sessionSidebar.classList.remove('show');
if (sidebarOverlay) sidebarOverlay.classList.remove('show');
}
} catch (err) {
console.error('❌ 创建会话异常:', err);
await customAlert('创建新会话失败,请重试');
}
}
export async function deleteSession(sessionId, switchSessionCallback, refreshSessionListCallback) {
if (!await customConfirm('确定要删除这个会话吗?')) {
return;
}
try {
const res = await fetch(`/AiChat/DeleteSession?sessionId=${encodeURIComponent(sessionId)}`, {method: 'DELETE'});
const result = await res.json();
if (result.success) {
const isCurrent = (sessionId === getCurrentSessionId());
// 调用刷新会话列表的回调(传递所有必要的参数)
if (refreshSessionListCallback && typeof refreshSessionListCallback === 'function') {
if (refreshSessionListCallback.length === 0) {
await refreshSessionListCallback();
} else {
await refreshSessionListCallback();
}
}
if (isCurrent) {
const first = document.querySelector('.session-item');
if (first && first.dataset.sessionId) {
const firstSessionId = first.dataset.sessionId;
// switchSessionCallback 是一个包装函数,只需要传递 sessionId
if (switchSessionCallback && typeof switchSessionCallback === 'function') {
await switchSessionCallback(firstSessionId);
}
} else {
// 如果没有其他会话,创建新会话
await createNewSession(
document.getElementById('sessionList'),
document.getElementById('sessionSidebar'),
document.getElementById('sidebarOverlay'),
switchSessionCallback
);
}
}
} else {
await customAlert(result["msg"] || result.message || '删除失败');
}
} catch (err) {
console.error('删除会话失败:', err);
await customAlert('删除失败: ' + (err.message || err));
}
}
export async function switchSession(sessionId, chatMain, updateDomReferences, bindChatEvents, setConnectionStatus, isConnected, updateInputState) {
// 确保参数有效
if (!chatMain) {
chatMain = document.getElementById('chatMain');
}
if (!updateDomReferences || typeof updateDomReferences !== 'function') {
console.error('switchSession: updateDomReferences 参数无效');
await customAlert('切换会话失败:参数错误');
return;
}
if (!bindChatEvents || typeof bindChatEvents !== 'function') {
console.error('switchSession: bindChatEvents 参数无效');
await customAlert('切换会话失败:参数错误');
return;
}
const currentSessionId = getCurrentSessionId();
// 处理空字符串的情况:如果都是空字符串,也认为是相同会话
const normalizedSessionId = sessionId || '';
const normalizedCurrentId = currentSessionId || '';
if (normalizedSessionId === normalizedCurrentId) {
console.log('⚠️ 已经在当前会话中,无需切换');
if (window.innerWidth <= 600) {
const sessionSidebar = document.getElementById('sessionSidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');
if (sessionSidebar) sessionSidebar.classList.remove('show');
if (sidebarOverlay) sidebarOverlay.classList.remove('show');
}
return;
}
console.log('🔄 切换会话:', normalizedSessionId || '(新会话)');
try {
const res = await fetch(`/AiChat/ChatMessages?sessionId=${encodeURIComponent(normalizedSessionId)}`);
if (!res.ok) {
const errorText = await res.text().catch(() => '');
console.error('切换会话请求失败:', res.status, errorText);
// 如果是创建新会话(空 sessionId),允许继续,不显示错误
if (normalizedSessionId === '') {
console.warn('新会话请求失败,但允许继续创建临时会话');
return;
} else {
await customAlert(`切换会话失败: ${res.status} ${res.statusText}`);
return;
}
}
const html = await res.text();
if (chatMain) {
chatMain.innerHTML = html;
// 使用传入的 updateDomReferences 函数获取 DOM 引用
const domRefs = updateDomReferences && typeof updateDomReferences === 'function'
? updateDomReferences()
: null;
if (!domRefs) {
console.error('无法获取 DOM 引用');
await customAlert('切换会话失败:无法获取 DOM 元素');
return;
}
// 确保参数有效
if (bindChatEvents && typeof bindChatEvents === 'function') {
bindChatEvents(domRefs);
}
if (domRefs.statusEl && setConnectionStatus && typeof setConnectionStatus === 'function') {
const connectionStatus = typeof isConnected === 'function' ? isConnected() : isConnected;
// 创建一个更新输入状态的函数,使用当前的 domRefs
const updateInputStateForSwitch = () => {
if (updateInputState && typeof updateInputState === 'function') {
updateInputState(domRefs);
}
};
setConnectionStatus(connectionStatus, domRefs.statusEl, updateInputStateForSwitch);
} else if (updateInputState && typeof updateInputState === 'function') {
// 即使没有 statusEl,也要更新输入状态
updateInputState(domRefs);
}
if (domRefs.chatArea) {
highlightCodeBlocks(domRefs.chatArea).catch(err => {
console.warn('代码高亮失败:', err);
});
scrollToBottom(domRefs.chatArea);
}
}
// 更新会话列表的选中状态(包括空会话的情况)
if (normalizedSessionId === '') {
// 对于新会话,清除所有选中状态
document.querySelectorAll('.session-item').forEach(item => {
item.classList.remove('active');
});
} else {
document.querySelectorAll('.session-item').forEach(item => {
item.classList.toggle('active', item.dataset.sessionId === normalizedSessionId);
});
}
} catch (err) {
console.error('切换会话失败:', err);
// 如果是创建新会话(空 sessionId),允许继续,不显示错误
if (normalizedSessionId === '') {
console.warn('新会话切换失败,但允许继续创建临时会话:', err);
} else {
await customAlert(`切换会话失败: ${err.message || err}`);
}
}
if (window.innerWidth <= 600) {
const sessionSidebar = document.getElementById('sessionSidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');
if (sessionSidebar) sessionSidebar.classList.remove('show');
if (sidebarOverlay) sidebarOverlay.classList.remove('show');
}
}
export async function refreshSessionList(switchSessionCallback, updateDomReferencesCallback, bindChatEventsCallback, setConnectionStatusCallback, getIsConnectedCallback, updateInputStateCallback) {
try {
const response = await fetch('/AiChat/Sessions');
const result = await response.json();
if (result.success && result.data) {
renderSessionList(result.data, switchSessionCallback, updateDomReferencesCallback, bindChatEventsCallback, setConnectionStatusCallback, getIsConnectedCallback, updateInputStateCallback);
}
} catch (err) {
console.error('刷新会话列表失败:', err);
}
}
/**
* 更新会话元素的辅助函数
*/
function applySessionUpdate(element, sessionId, name, time, count, switchSessionCallback, refreshSessionListCallback) {
const nameEl = element.querySelector('.session-name');
const timeEl = element.querySelector('.session-time');
const countEl = element.querySelector('.session-count');
if (nameEl) {
nameEl.textContent = name;
nameEl.title = name;
nameEl.style.fontStyle = 'normal';
}
if (timeEl && time) {
timeEl.textContent = new Date(time).toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
if (countEl && count !== undefined) {
countEl.textContent = `${count} 条`;
}
element.classList.remove('pending');
// 更新点击事件
const sessionInfo = element.querySelector('.session-info');
if (sessionInfo) {
const newSessionInfo = sessionInfo.cloneNode(true);
sessionInfo.parentNode.replaceChild(newSessionInfo, sessionInfo);
newSessionInfo.addEventListener('click', async () => {
if (switchSessionCallback && typeof switchSessionCallback === 'function') {
await switchSessionCallback(sessionId);
}
});
}
// 确保有删除按钮
if (!element.querySelector('.btn-delete-session')) {
const btn = document.createElement('button');
btn.className = 'btn-delete-session';
btn.textContent = '🗑️';
btn.title = '删除会话';
btn.onclick = async (e) => {
e.stopPropagation();
await deleteSession(sessionId, switchSessionCallback, refreshSessionListCallback);
};
element.appendChild(btn);
}
}
/**
* 更新指定会话的标题
* @param {string} sessionId - 会话ID
* @param {Function} switchSessionCallback - 切换会话的回调
* @param {Function} refreshSessionListCallback - 刷新会话列表的回调(可选)
* @param {string} [sessionName] - 会话名称(可选,如果有则直接更新不请求接口)
*/
export async function updateSessionTitle(sessionId, switchSessionCallback, refreshSessionListCallback, sessionName) {
if (!sessionId || !pendingSessionElement) {
return false;
}
// 如果 pending 元素的 sessionId 不匹配,说明不是要更新的会话
if (pendingSessionElement.dataset.sessionId !== sessionId) {
return false;
}
if (sessionName) {
console.log('✅ 使用 SignalR 提供的会话名称更新:', sessionId, sessionName);
applySessionUpdate(
pendingSessionElement,
sessionId,
sessionName,
new Date(),
2,
switchSessionCallback,
refreshSessionListCallback
);
return true;
}
try {
// 获取会话列表,查找对应的会话
const response = await fetch('/AiChat/Sessions');
const result = await response.json();
if (result.success && result.data) {
const matched = result.data.find(s => s.sessionId === sessionId);
if (matched) {
console.log('✅ 找到匹配的会话,更新标题:', sessionId, matched['sessionName']);
applySessionUpdate(
pendingSessionElement,
sessionId,
matched['sessionName'] || '未命名会话',
matched['lastUpdateTime'],
matched['messageCount'],
switchSessionCallback,
refreshSessionListCallback
);
return true;
} else {
console.log('⚠️ 未找到匹配的会话,SessionId:', sessionId);
}
}
} catch (err) {
console.error('更新会话标题失败:', err);
}
return false;
}
export function renderSessionList(sessions, switchSessionCallback, updateDomReferencesCallback, bindChatEventsCallback, setConnectionStatusCallback, getIsConnectedCallback, updateInputStateCallback) {
const sessionList = document.getElementById('sessionList');
if (!sessionList) return;
const pending = pendingSessionElement;
sessionList.innerHTML = '';
if (pending) {
// 如果 pending 有真实的 sessionId(不是 temp_ 开头),尝试从服务器数据中更新
if (pending.dataset.sessionId && !pending.dataset.sessionId.startsWith('temp_')) {
const realId = pending.dataset.sessionId;
const matched = sessions.find(s => s.sessionId === realId);
if (matched) {
console.log('✅ 找到匹配的会话,更新标题:', realId, matched['sessionName']);
// 更新 pending 会话项的信息
const nameEl = pending.querySelector('.session-name');
const timeEl = pending.querySelector('.session-time');
const countEl = pending.querySelector('.session-count');
if (nameEl) {
const sessionName = matched['sessionName'] || '未命名会话';
nameEl.textContent = sessionName;
nameEl.title = sessionName;
nameEl.style.fontStyle = 'normal';
}
if (timeEl) {
const updateTime = matched['lastUpdateTime'];
if (updateTime) {
timeEl.textContent = new Date(updateTime).toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
}
if (countEl) {
const messageCount = matched['messageCount'] !== undefined ? matched['messageCount'] : 0;
countEl.textContent = `${messageCount} 条`;
}
pending.classList.remove('pending');
// 更新点击事件
const sessionInfo = pending.querySelector('.session-info');
if (sessionInfo) {
// 移除旧的事件监听器
const newSessionInfo = sessionInfo.cloneNode(true);
sessionInfo.parentNode.replaceChild(newSessionInfo, sessionInfo);
newSessionInfo.addEventListener('click', async () => {
// switchSessionCallback 是包装函数,只需要传递 sessionId
if (switchSessionCallback && typeof switchSessionCallback === 'function') {
await switchSessionCallback(realId);
}
});
// 更新删除按钮的事件
const deleteBtn = pending.querySelector('.btn-delete-session');
if (deleteBtn) {
const newDeleteBtn = deleteBtn.cloneNode(true);
deleteBtn.parentNode.replaceChild(newDeleteBtn, deleteBtn);
newDeleteBtn.onclick = async (e) => {
e.stopPropagation();
await deleteSession(realId, switchSessionCallback, (switchCallback, updateCallback, bindCallback, statusCallback, connectedCallback, inputStateCallback) => {
refreshSessionList(switchCallback, updateCallback, bindCallback, statusCallback, connectedCallback, inputStateCallback);
});
};
}
}
// 确保有删除按钮
if (!pending.querySelector('.btn-delete-session')) {
const btn = document.createElement('button');
btn.className = 'btn-delete-session';
btn.textContent = '🗑️';
btn.title = '删除会话';
btn.onclick = async (e) => {
e.stopPropagation();
await deleteSession(realId, switchSessionCallback, (switchCallback, updateCallback, bindCallback, statusCallback, connectedCallback, inputStateCallback) => {
refreshSessionList(switchCallback, updateCallback, bindCallback, statusCallback, connectedCallback, inputStateCallback);
});
};
pending.appendChild(btn);
}
// 从 sessions 数组中移除已匹配的项
sessions = sessions.filter(s => s.sessionId !== realId);
} else {
console.log('⚠️ 未找到匹配的会话,SessionId:', realId, '服务器返回的会话列表:', sessions.map(s => s.sessionId));
}
}
// 将 pending 元素插入到列表开头
sessionList.appendChild(pending);
}
sessions.forEach(session => {
const div = document.createElement('div');
div.className = `session-item ${session.sessionId === getCurrentSessionId() ? 'active' : ''}`;
div.dataset.sessionId = session.sessionId;
div.innerHTML = `
<div class="session-info">
<div class="session-name" title="${session['sessionName']}">${session['sessionName']}</div>
<div class="session-meta">
<span class="session-time">${new Date(session['lastUpdateTime']).toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})}</span>
<span class="session-count">${session['messageCount']} 条</span>
</div>
</div>
<button class="btn-delete-session" title="删除会话">🗑️</button>
`;
div.querySelector('.session-info').onclick = async () => {
// switchSessionCallback 是包装函数,只需要传递 sessionId
if (switchSessionCallback && typeof switchSessionCallback === 'function') {
await switchSessionCallback(session.sessionId);
}
};
div.querySelector('.btn-delete-session').onclick = async (e) => {
e.stopPropagation();
await deleteSession(session.sessionId, switchSessionCallback, () => {
refreshSessionList(switchSessionCallback, updateDomReferencesCallback, bindChatEventsCallback, setConnectionStatusCallback, getIsConnectedCallback, updateInputStateCallback);
});
};
sessionList.appendChild(div);
});
}
export function toggleSidebar(forceClose = false) {
const sessionSidebar = document.getElementById('sessionSidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');
const isMobile = window.innerWidth <= 600;
if (forceClose) {
if (isMobile) {
if (sessionSidebar) sessionSidebar.classList.remove('show');
if (sidebarOverlay) sidebarOverlay.classList.remove('show');
} else {
if (sessionSidebar) sessionSidebar.classList.add('collapsed');
}
return;
}
if (isMobile) {
if (sessionSidebar) sessionSidebar.classList.toggle('show');
if (sidebarOverlay) sidebarOverlay.classList.toggle('show');
} else {
if (sessionSidebar) {
sessionSidebar.classList.toggle('collapsed');
sessionSidebar.classList.toggle('show');
}
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
下面把这段“会话管理模块”代码的结构和功能要点分块讲清楚,便于理解每个导出函数的职责、交互流程和重要细节。
总体说明
- 这是一个负责管理会话列表与会话切换的前端模块(纯前端 DOM 操作 + 与后端的若干接口交互)。
- 依赖的外部模块/函数:
- getCurrentSessionId、scrollToBottom(来自 dom-utils.js)
- customAlert、customConfirm(来自 modal.js,用于提示/确认)
- highlightCodeBlocks(来自 highlight.js,用于代码高亮)
- 与后端接口约定(已在代码中使用):
- GET /AiChat/Sessions — 获取会话列表(返回 JSON)
- DELETE /AiChat/DeleteSession?sessionId=... — 删除会话(返回 JSON)
- GET /AiChat/ChatMessages?sessionId=... — 获取某个会话的聊天界面(返回 HTML)
模块级变量
- pendingSessionElement:模块级变量,保存当前“待创建/临时”会话 DOM 元素(可能是用户刚创建但尚未被后端正式确认的会话)。导出 getPendingSessionElement() 用于读取它。
主要导出函数说明(按重要性排序)
- createNewSession(sessionList, sessionSidebar, sidebarOverlay, switchSession)
- 目的:在 UI 上创建一个“待创建的新会话”条目(临时会话),并尽量把界面切换到空会话(用于输入新消息)。
- 主要逻辑:
- 若已有一个 class 包含 .session-item.pending 的待创建项,则阻止重复创建并提醒用户先发送消息完成创建,同时把该 pending 项设为 active。
- 尝试调用 switchSession('') 切换到空会话(空字符串代表新会话/未保存会话),如果切换失败仍继续创建临时项(catch 仅记录警告)。
- 在 sessionList(传入的 DOM 节点)最前方插入一个临时会话项(data-session-id 为 temp_{timestamp};并标记 data-is-new-session = 'true')。
- 临时项的点击行为:如果是新会话并且当前页面有已存在的 currentId,会尝试再次 switchSession('') 并将该临时项置为 active(有 try/catch)。
- 清空隐藏域 currentSessionId(如存在),便于前端表示没有当前会话。
- 在窄屏(<=600px)上会自动关闭侧栏(移动端 UX)。
- 错误处理:发生异常时调用 customAlert 提示“创建新会话失败”。
- deleteSession(sessionId, switchSessionCallback, refreshSessionListCallback)
- 目的:删除指定会话并在必要时更新 UI(切换到其他会话或创建新会话)。
- 主要逻辑:
- 先用 customConfirm 确认删除。
- 发起 DELETE 请求到 /AiChat/DeleteSession?sessionId=... 并解析 JSON 返回(result.success)。
- 如果删除成功:
- 调用 refreshSessionListCallback 刷新会话列表(如果提供)。
- 如果删除的是当前正在打开的会话(isCurrent),尝试切换到会话列表中第一个会话(如果存在),否则调用 createNewSession 创建临时会话。
- 如果删除失败或请求异常,显示 customAlert 错误提示。
- 注意:switchSessionCallback、refreshSessionListCallback 都以回调形式传入,代码会检查并调用。
- switchSession(sessionId, chatMain, updateDomReferences, bindChatEvents, setConnectionStatus, isConnected, updateInputState)
- 目的:切换到指定会话(sessionId 可为空字符串,表示新/空会话),加载对应聊天界面并绑定事件等。
- 参数职责:
- chatMain:外部容器元素(默认 document.getElementById('chatMain'))
- updateDomReferences:必须的函数,返回当前 chat 界面相关的 DOM 引用集合(供后续使用)
- bindChatEvents:必须的函数,用来给新加载的 chat 界面绑定事件(比如发送按钮、输入框等)
- setConnectionStatus:可选,用来显示/设置连接状态的函数
- isConnected:表示是否连接(可以是函数或布尔)
- updateInputState:用于根据连接状态更新输入框可用性
- 主要逻辑:
- 如果尝试切换到与当前会话相同的 sessionId,则直接返回(仅在移动端关闭侧栏)。
- 发起 GET /AiChat/ChatMessages?sessionId=... 获取聊天 HTML;如果 status 非 ok,非空 session 时显示错误;若是空 session(新会话),在失败情况下容忍(允许继续创建临时会话)。
- 将返回的 html 置入 chatMain.innerHTML。
- 通过 updateDomReferences() 获取新的 DOM 引用(若无法获取,则提示失败)。
- 调用 bindChatEvents(domRefs) 绑定各种事件(若提供)。
- 如果提供了 setConnectionStatus,会调用它并传入连接状态、statusEl 和用于更新输入的回调;否则即使没有 statusEl 也会尝试调用 updateInputState(domRefs) 来更新输入框状态。
- 对 chatArea 调用代码高亮(highlightCodeBlocks)并滚动到底部(scrollToBottom)。
- 更新会话列表中的选中状态:非空 sessionId 时将对应 .session-item 加 active;空 sessionId 则清除所有 active(表示新会话)。
- 在移动端同样会收起侧栏。
- 错误处理:对网络或解析错误会显示提示,空 sessionId 的失败会被容忍并打印警告。
- refreshSessionList(switchSessionCallback, updateDomReferencesCallback, bindChatEventsCallback, setConnectionStatusCallback, getIsConnectedCallback, updateInputStateCallback)
- 目的:从后端获取会话列表并渲染(调用 renderSessionList)。
- 实现:GET /AiChat/Sessions -> 若返回 result.success && result.data 则调用 renderSessionList 并把回调参数传下去。
- updateSessionTitle(sessionId, switchSessionCallback, refreshSessionListCallback, sessionName)
- 目的:用于把 pendingSessionElement(临时会话)更新为正式会话的显示信息(名称、时间、消息数),通常在后端确认或通过 SignalR 收到名称后调用。
- 主要逻辑:
- 仅当 pendingSessionElement 存在且其 dataset.sessionId 与传入 sessionId 匹配时才尝试更新。
- 如果调用时传入了 sessionName(例如通过 SignalR 主动推送),直接使用并调用 applySessionUpdate 更新 pending DOM。
- 否则会调用 /AiChat/Sessions 获取列表,在列表中查找匹配的 sessionId 并用服务器数据更新 DOM(包括名字、时间、消息数)。
- 返回是否成功更新(布尔)。
- renderSessionList(sessions, switchSessionCallback, updateDomReferencesCallback, bindChatEventsCallback, setConnectionStatusCallback, getIsConnectedCallback, updateInputStateCallback)
- 目的:基于传入的会话数组渲染侧栏会话列表,并把 pending(临时)项插入到列表顶部(如果存在)。
- 细节:
- 清空 sessionList(DOM id 为 sessionList)的 innerHTML 后重建。
- 若有 pendingSessionElement:
- 如果 pending 的 data-session-id 不是 temp_*(说明后端已经返回真实 id 并被写入),尝试在传入的 sessions 中匹配该真实 id:
- 若匹配到,则把 pending 的显示信息(名称、时间、消息数)替换为服务器值,并移除 pending 的 pending class,确保它成为正式条目;同时为其添加删除按钮与点击切换事件。
- 从 sessions 数组中移除已匹配的项(避免重复渲染)。
- 把 pending 元素插入到列表顶部(appendChild)。
- 如果 pending 的 data-session-id 不是 temp_*(说明后端已经返回真实 id 并被写入),尝试在传入的 sessions 中匹配该真实 id:
- 遍历剩余 sessions,逐个构造 .session-item DOM,设置 .session-info 点击事件(调用 switchSessionCallback(sessionId)),删除按钮会调用 deleteSession 并刷新会话列表。
- 注意:render 时会把当前会话 id 跟 getCurrentSessionId() 比对决定 active 类。
辅助函数 applySessionUpdate(element, sessionId, name, time, count, switchSessionCallback, refreshSessionListCallback)
- 功能:把给定 element 的名称、时间、消息数和点击行为更新为正式会话状态,并确保存在删除按钮(绑定 deleteSession)。
- 用在 updateSessionTitle 与 renderSessionList 内部以复用更新逻辑。
toggleSidebar(forceClose = false)
- UI 工具函数:控制侧栏显示/收起行为,支持移动端(<=600px)和桌面两种样式(collapsed、show)。
- 如果 forceClose 为 true,则强制关闭侧栏(移动端)或折叠(桌面)。
重要设计/行为说明与注意点
- pending 概念:模块允许创建“临时会话”项(temp_xxx)并把它保存在 pendingSessionElement。后续当服务器返回正式会话信息(通过刷新列表或 SignalR 推送)时,会把 pending 转为正式会话项。
- 空 sessionId 的语义:空字符串被视为“新会话 / 未存储的会话”,对切换空会话(switchSession(''))的失败有容忍逻辑(不会阻止 UI 创建临时会话)。
- 回调驱动:switchSession、renderSessionList、updateSessionTitle 等函数都依赖外部传入的回调(updateDomReferences、bindChatEvents、setConnectionStatus)完成 DOM 绑定和状态设置,这使得模块与具体的聊天 UI 实现解耦。
- 移动端 UX:多处在 window.innerWidth <= 600 条件下自动收起侧栏,优化移动端交互。
- 错误提示:网络或逻辑错误大多使用 customAlert 提示;删除通过 customConfirm 确认。
- 安全性与健壮性:对许多回调都做了类型检查(typeof === 'function'),对 fetch 返回的 res.ok 做了处理;但某些地方仍假定服务端返回符合约定的 JSON/HTML 格式。
- 多次刷新/并发:代码中会替换节点和 cloneNode 来移除旧事件监听器,这是为了避免重复绑定;但并未显示处理并发创建多个 pending(createNewSession 只会阻止已有 pending 的创建)。
总结
- 该模块是一个完整的前端会话列表与切换管理实现,负责:
- 在 UI 中创建临时新会话、把临时会话升级为正式会话;
- 从后端拉取会话列表并渲染;
- 切换会话(加载聊天 HTML、绑定事件、设置连接与输入状态、代码高亮、滚动到底);
- 删除会话并处理切换或创建后续会话;
- 支持移动端与桌面不同的侧栏行为。
- 模块将 UI 操作(DOM 构造/事件)、网络请求和外部回调整合起来,保持了和聊天具体实现(DOM refs、事件绑定、SignalR 推送等)的解耦。
如果需要,我可以:
- 画出函数调用与流程图(文字或图示),
- 指出可能的 bug/改进点(例如并发/边界条件、错误信息更友好、API 返回校验等),
- 或者把部分逻辑重构为更小、更易测试的函数。要哪个请告诉我。
评论加载中...