console
let currentSelectedChatId = null;
let renderStartOffset = 0;
const PAGE_SIZE = 20;
let forceFullRenderMode = false;
export function renderUI(messageMap = {}) {
const overlay = document.getElementById('lineOverlay');
const contactsEl = document.getElementById('lineContacts');
const msgEl = document.getElementById('lineMessages');
const splitter = document.getElementById('lineSplitter');
const ball = document.getElementById('lineBall');
if (!overlay || !contactsEl || !msgEl) return;
enableDrag(overlay);
enableSplitter(splitter, contactsEl);
restoreState();
contactsEl.innerHTML = '';
msgEl.innerHTML = '点击左侧头像查看';
const chatIds = Object.keys(messageMap);
if (chatIds.length === 0) {
msgEl.innerHTML = '<p>暂无消息</p>';
currentSelectedChatId = null;
return;
}
chatIds.forEach((chatId, index) => {
const chat = messageMap[chatId];
const wrapper = document.createElement("div");
wrapper.className = "contact";
const avatar = document.createElement('img');
avatar.src = chat?.avatar || 'https://via.placeholder.com/40';
avatar.title = chat.name;
avatar.dataset.chatId = chatId;
const nameSpan = document.createElement('span');
nameSpan.className = 'name';
nameSpan.innerText = chat.name;
const badge = document.createElement("span");
badge.className = "badge";
badge.innerText = getUnreadCountFromDOM(chatId);
avatar.onclick = () => {
currentSelectedChatId = chatId;
forceFullRenderMode = false;
renderMessages(chatId, chat.name, chat.messages || [], msgEl, messageMap);
highlightCurrentAvatar(contactsEl, chatId);
};
wrapper.appendChild(avatar);
wrapper.appendChild(nameSpan);
wrapper.appendChild(badge);
contactsEl.appendChild(wrapper);
if (!currentSelectedChatId) {
avatar.click();
}
});
document.getElementById('markReadBtn').onclick = () => {
if (!currentSelectedChatId) return alert('⚠️ 未选择联系人');
document.querySelector(`img[data-chat-id="${currentSelectedChatId}"]`).nextSibling.nextSibling.innerText = '';
};
document.getElementById('lineRefresh').onclick = () => {
chrome.storage.local.get("messages", res => {
renderUI(res.messages || {});
});
};
document.getElementById("lineMinimize").onclick = () => {
overlay.style.display = "none";
ball.style.display = "flex";
saveState();
};
ball.onclick = () => {
overlay.style.display = "flex";
ball.style.display = "none";
restoreState();
};
}
export function appendMessagesToCurrentChat(latestMessages) {
const msgList = document.querySelector("#msgList");
if (!msgList || !currentSelectedChatId) return;
const displayedMessageIds = Array.from(msgList.querySelectorAll('[scroll-key]'))
.map(el => parseInt(el.getAttribute('scroll-key')));
const newMessages = latestMessages.filter(m => !displayedMessageIds.includes(m.id));
newMessages.forEach(m => {
msgList.appendChild(createMessageDOM(m));
});
if (newMessages.length > 0) {
msgList.scrollTop = msgList.scrollHeight;
console.log(`�� 新追加 ${newMessages.length} 条消息到当前窗口`);
}
}
export function renderContactSidebar(messageMap = {}) {
const contactsEl = document.getElementById('lineContacts');
if (!contactsEl) return;
contactsEl.innerHTML = '';
Object.keys(messageMap).forEach(chatId => {
const chat = messageMap[chatId];
const wrapper = document.createElement("div");
wrapper.className = "contact";
const avatar = document.createElement('img');
avatar.src = chat?.avatar || 'https://via.placeholder.com/40';
avatar.title = chat.name;
avatar.dataset.chatId = chatId;
const nameSpan = document.createElement('span');
nameSpan.className = 'name';
nameSpan.innerText = chat.name;
const badge = document.createElement("span");
badge.className = "badge";
badge.innerText = getUnreadCountFromDOM(chatId);
avatar.onclick = () => {
currentSelectedChatId = chatId;
forceFullRenderMode = false;
renderMessages(chatId, chat.name, chat.messages || [], document.getElementById('lineMessages'), messageMap);
highlightCurrentAvatar(contactsEl, chatId);
};
wrapper.appendChild(avatar);
wrapper.appendChild(nameSpan);
wrapper.appendChild(badge);
contactsEl.appendChild(wrapper);
});
highlightCurrentAvatar(contactsEl, currentSelectedChatId);
}
function renderMessages(chatId, chatName, messages = [], msgEl, messageMap) {
msgEl.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;">
<strong>${chatName}</strong>
<div>
<button id="fetchHistoryBtn" style="font-size:12px;margin-right:5px;">获取历史记录</button>
<button id="clearBtn" style="font-size:12px;">清空此人</button>
</div>
</div>
<div id="msgList" style="margin-top:10px;overflow-y:auto;height:calc(100% - 40px);position:relative;"></div>
`;
const msgList = msgEl.querySelector("#msgList");
msgList.innerHTML = '';
const chatData = messageMap[chatId];
const sorted = [...messages].sort((a, b) => a.id - b.id);
if (chatData?.tempMessages?.length) {
sorted.push(...chatData.tempMessages.sort((a, b) => a.id - b.id));
}
renderStartOffset = Math.max(0, sorted.length - PAGE_SIZE);
function renderPrevPage() {
const start = Math.max(0, renderStartOffset - PAGE_SIZE);
const end = renderStartOffset;
const prevMessages = sorted.slice(start, end);
const scrollAnchor = msgList.scrollHeight;
let lastDate = null;
prevMessages.reverse().forEach(m => {
const msgDate = m.date || (m.time.split(" ")[0]);
if (msgDate !== lastDate) {
msgList.insertBefore(createDateLine(msgDate), msgList.firstChild);
lastDate = msgDate;
}
msgList.insertBefore(createMessageDOM(m), msgList.firstChild);
});
msgList.scrollTop = msgList.scrollHeight - scrollAnchor;
renderStartOffset = start;
}
const initialMessages = sorted.slice(renderStartOffset, sorted.length);
let lastDate = null;
initialMessages.forEach(m => {
const msgDate = m.date || (m.time.split(" ")[0]);
if (msgDate !== lastDate) {
msgList.appendChild(createDateLine(msgDate));
lastDate = msgDate;
}
msgList.appendChild(createMessageDOM(m));
});
msgList.scrollTop = msgList.scrollHeight;
msgList.onscroll = () => {
if (msgList.scrollTop <= 5 && renderStartOffset > 0) {
renderPrevPage();
}
};
document.getElementById("clearBtn").onclick = () => {
chrome.storage.local.get("messages", res => {
const store = res.messages || {};
delete store[chatId];
chrome.storage.local.set({ messages: store }, () => {
currentSelectedChatId = null;
document.getElementById('lineMessages').innerHTML = '<p>请选择联系人</p>';
window.postMessage({
source: 'line_chat_monitor',
action: 'restart_observers'
}, '*');
renderUI(store);
});
});
};
document.getElementById("fetchHistoryBtn").onclick = () => {
forceFullRenderMode = true;
window.dispatchEvent(new CustomEvent("force-scan-line", { detail: { targetChatId: chatId } }));
};
}
export function renderMessagesForceRefresh(chatId, messages) {
const msgEl = document.getElementById('lineMessages');
if (!msgEl) return;
const chatName = (document.querySelector(`img[data-chat-id="${chatId}"]`)?.title) || '未知';
const messageMap = {};
messageMap[chatId] = { messages };
renderMessages(chatId, chatName, messages, msgEl, messageMap);
}
function createMessageDOM(m) {
const wrapper = document.createElement("div");
wrapper.style.display = 'flex';
wrapper.style.justifyContent = m.from === 'self' ? 'flex-end' : 'flex-start';
const container = document.createElement("div");
container.className = "msg-outer-container";
container.style.textAlign = m.from === 'self' ? 'left' : 'right';
const statusRow = document.createElement("div");
statusRow.className = "msg-status-row";
statusRow.style.fontSize = "11px";
statusRow.style.color = "#999";
if (m.from === 'self' && m.isRead) {
const readStatus = document.createElement('span');
readStatus.className = "msg-read-status";
readStatus.innerText = m.readStatusText || "已讀";
statusRow.appendChild(readStatus);
statusRow.appendChild(document.createElement("br"));
}
const timeEl = document.createElement("span");
timeEl.className = "msg-time";
timeEl.innerText = m.time;
statusRow.appendChild(timeEl);
container.appendChild(statusRow);
const msgClass = m.from === 'self' ? 'msg_rgt msg_wrap' : 'msg_lft msg_wrap';
const outerDiv = document.createElement("div");
outerDiv.className = msgClass;
outerDiv.setAttribute('scroll-key', m.id);
const bubble = document.createElement("div");
bubble.className = "msg-block";
bubble.innerText = m.text;
bubble.style.background = m.from === 'self' ? '#d0f0c0' : '#f0f0f0';
outerDiv.appendChild(bubble);
container.appendChild(outerDiv);
wrapper.appendChild(container);
return wrapper;
}
function createDateLine(dateText) {
const informDate = document.createElement("div");
informDate.className = "inform_date";
informDate.style.textAlign = "center";
informDate.style.margin = "10px 0";
const span = document.createElement("span");
span.className = "date";
span.innerText = dateText;
informDate.appendChild(span);
return informDate;
}
function getUnreadCountFromDOM(chatId) {
const li = document.querySelector(`li[data-key="${chatId}"]`);
if (!li) return '';
const span = li.querySelector("span.new");
return span ? parseInt(span.innerText.trim()) || '' : '';
}
function highlightCurrentAvatar(contactsEl, chatId) {
document.querySelectorAll('#lineContacts img').forEach(el => el.classList.remove('selected'));
const avatar = contactsEl.querySelector(`img[data-chat-id="${chatId}"]`);
if (avatar) avatar.classList.add('selected');
}
function enableDrag(el) {
const bar = el.querySelector(".title-bar");
if (!bar) return;
let isDragging = false;
let offsetX = 0, offsetY = 0;
bar.addEventListener("mousedown", (e) => {
isDragging = true;
offsetX = e.clientX - el.offsetLeft;
offsetY = e.clientY - el.offsetTop;
});
document.addEventListener("mousemove", (e) => {
if (isDragging) {
el.style.left = `${e.clientX - offsetX}px`;
el.style.top = `${e.clientY - offsetY}px`;
}
});
document.addEventListener("mouseup", () => {
isDragging = false;
saveState();
});
}
function enableSplitter(splitter, leftPane) {
if (!splitter || !leftPane) return;
splitter.onmousedown = function (e) {
e.preventDefault();
const startX = e.clientX;
const startWidth = leftPane.offsetWidth;
function onMouseMove(e) {
const newWidth = Math.min(300, Math.max(80, startWidth + e.clientX - startX));
leftPane.style.width = `${newWidth}px`;
}
function onMouseUp() {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
saveState();
}
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
};
}
function saveState() {
const overlay = document.getElementById('lineOverlay');
if (!overlay) return;
const state = {
left: overlay.offsetLeft,
top: overlay.offsetTop,
width: overlay.offsetWidth,
height: overlay.offsetHeight,
contactsWidth: document.getElementById('lineContacts')?.offsetWidth || 120
};
localStorage.setItem('line_chat_state', JSON.stringify(state));
}
function restoreState() {
const data = localStorage.getItem('line_chat_state');
if (!data) return;
const overlay = document.getElementById('lineOverlay');
if (!overlay) return;
const { left, top, width, height, contactsWidth } = JSON.parse(data);
if (left) overlay.style.left = `${left}px`;
if (top) overlay.style.top = `${top}px`;
if (width) overlay.style.width = `${width}px`;
if (height) overlay.style.height = `${height}px`;
if (contactsWidth) document.getElementById('lineContacts').style.width = `${contactsWidth}px`;
}
window.appendMessagesToCurrentChat = appendMessagesToCurrentChat;
window.renderContactSidebar = renderContactSidebar;
window.appendMessagesToCurrentChat = appendMessagesToCurrentChat;
window.renderContactSidebar = renderContactSidebar;
window.renderMessagesForceRefresh = renderMessagesForceRefresh;
window.addEventListener('message', (event) => {
if (event.data?.source !== 'line_chat_monitor') return;
if (event.data.action === 'update_chat') {
const { chatId, messages } = event.data;
if (chatId === currentSelectedChatId) {
renderMessagesForceRefresh(chatId, messages);
}
}
});