110 lines
3.6 KiB
Text
Executable file
110 lines
3.6 KiB
Text
Executable file
// chat.js —— 光湖桥聊天主逻辑(只依赖 window.GHB,与壳无关)
|
||
(() => {
|
||
const $ = (id) => document.getElementById(id);
|
||
const stream = $('stream');
|
||
const statusLine = $('status-line');
|
||
const peerInput = $('peer');
|
||
const textInput = $('text');
|
||
const sendBtn = $('send');
|
||
const connDot = $('conn');
|
||
const nodeIdEl = $('node-id');
|
||
|
||
const fmtTime = (ts) => new Date(ts || Date.now()).toLocaleTimeString('zh-CN', { hour12: false });
|
||
|
||
function sysMsg(text) {
|
||
const d = document.createElement('div');
|
||
d.className = 'msg sys';
|
||
d.textContent = text;
|
||
stream.appendChild(d);
|
||
stream.scrollTop = stream.scrollHeight;
|
||
}
|
||
|
||
function bubble({ who, from, text, msgId }) {
|
||
const d = document.createElement('div');
|
||
d.className = `msg ${who}`;
|
||
if (msgId) d.dataset.msgId = msgId;
|
||
const meta = document.createElement('span');
|
||
meta.className = 'meta';
|
||
meta.textContent = who === 'me' ? `我 · ${fmtTime()}` : `${from} · ${fmtTime()}`;
|
||
const body = document.createElement('span');
|
||
body.textContent = text;
|
||
d.appendChild(meta);
|
||
d.appendChild(body);
|
||
if (who === 'me') {
|
||
const tick = document.createElement('span');
|
||
tick.className = 'tick';
|
||
tick.textContent = '…';
|
||
d.appendChild(tick);
|
||
}
|
||
stream.appendChild(d);
|
||
stream.scrollTop = stream.scrollHeight;
|
||
return d;
|
||
}
|
||
|
||
function setStatus(text, isErr) {
|
||
statusLine.textContent = text || '';
|
||
statusLine.classList.toggle('err', !!isErr);
|
||
}
|
||
|
||
function markReceipt(msgId, verified) {
|
||
const d = stream.querySelector(`[data-msg-id="${msgId}"]`);
|
||
if (!d) return;
|
||
const tick = d.querySelector('.tick');
|
||
if (!tick) return;
|
||
tick.textContent = verified ? '✓ 已送达·已验签' : '✓ 已送达';
|
||
tick.classList.toggle('ok', verified);
|
||
}
|
||
|
||
function doSend() {
|
||
const to = peerInput.value.trim();
|
||
const text = textInput.value.trim();
|
||
if (!to) { setStatus('请先填对方节点号', true); return; }
|
||
if (!text) return;
|
||
bubble({ who: 'me', text, msgId: null }); // 占位,sent 事件后补 msg_id
|
||
pendingSend = { text };
|
||
window.GHB.send(to, text);
|
||
textInput.value = '';
|
||
setStatus(`正在发送 → ${to} …`);
|
||
}
|
||
|
||
let pendingSend = null;
|
||
|
||
// ── 事件绑定 ──
|
||
window.GHB.on('connected', (ev) => {
|
||
nodeIdEl.textContent = `我的节点号:${ev.node_id}`;
|
||
connDot.className = 'conn on';
|
||
sysMsg(`已接入光湖桥,节点号 ${ev.node_id}(宿主:${window.GHB.kind})`);
|
||
});
|
||
|
||
window.GHB.on('message', (ev) => {
|
||
bubble({ who: 'peer', from: ev.from, text: ev.text, msgId: ev.msg_id });
|
||
peerInput.placeholder = ev.from; // 记住最近来信的对端
|
||
setStatus('');
|
||
});
|
||
|
||
window.GHB.on('sent', (ev) => {
|
||
// 给最后一个"我"的气泡补上 msg_id 以便回执对账
|
||
const mine = stream.querySelectorAll('.msg.me');
|
||
const last = mine[mine.length - 1];
|
||
if (last && !last.dataset.msgId) last.dataset.msgId = ev.msg_id;
|
||
setStatus(`已交给光湖桥 → ${ev.to}`);
|
||
pendingSend = null;
|
||
});
|
||
|
||
window.GHB.on('receipt', (ev) => {
|
||
markReceipt(ev.msg_id, ev.verified);
|
||
setStatus(ev.verified ? '对方已确认,回执验签通过' : '对方已确认');
|
||
});
|
||
|
||
window.GHB.on('error', (ev) => setStatus(`错误:${ev.message}`, true));
|
||
|
||
// 壳层自检钩子:自动填入并发送,让 UI 真实渲染完整链路
|
||
window.GHB.on('__autosend', (ev) => {
|
||
peerInput.value = ev.to;
|
||
textInput.value = ev.text;
|
||
doSend();
|
||
});
|
||
|
||
sendBtn.addEventListener('click', doSend);
|
||
textInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') doSend(); });
|
||
})();
|