514 lines
23 KiB
JavaScript
514 lines
23 KiB
JavaScript
/* ============================================================
|
||
* 微信工作台 — 聊天 / 消息 / 会话模块
|
||
* 依赖:core.js(window.Core)、wx.js(window.WxN)
|
||
* ============================================================ */
|
||
(function (global) {
|
||
"use strict";
|
||
|
||
var Core = global.Core;
|
||
var WxN = global.WxN;
|
||
var $ = function (sel, root) { return (root || document).querySelector(sel); };
|
||
|
||
function state() { return WxN.state; }
|
||
|
||
/* ==================== brief 缓存辅助 ==================== */
|
||
function loadBriefCache() { return Core.store(Core.KEYS.wxBrief) || {}; }
|
||
function saveBriefCache() {
|
||
var s = state();
|
||
var all = loadBriefCache();
|
||
var appId = s.app && s.app.appId;
|
||
if (!appId) return;
|
||
all[appId] = s.brief;
|
||
Core.store(Core.KEYS.wxBrief, all);
|
||
}
|
||
|
||
// 相对时间显示
|
||
function timeAgo(ts) {
|
||
var now = Date.now();
|
||
var diff = Math.floor((now - ts) / 1000);
|
||
if (diff < 60) return "刚刚";
|
||
if (diff < 3600) return Math.floor(diff / 60) + "分钟前";
|
||
if (diff < 86400) return Math.floor(diff / 3600) + "小时前";
|
||
if (diff < 604800) return Math.floor(diff / 86400) + "天前";
|
||
var d = new Date(ts);
|
||
return (d.getMonth() + 1) + "/" + d.getDate();
|
||
}
|
||
|
||
// 格式化消息时间戳
|
||
function fmtMsgTime(ts) {
|
||
if (!ts) return "";
|
||
var d = typeof ts === "number" && ts < 1e12 ? new Date(ts * 1000) : new Date(ts);
|
||
if (isNaN(d.getTime())) return "";
|
||
var h = d.getHours(), m = d.getMinutes();
|
||
var pad = function (n) { return n < 10 ? "0" + n : "" + n; };
|
||
var now = new Date();
|
||
if (d.toDateString() === now.toDateString()) return pad(h) + ":" + pad(m);
|
||
return (d.getMonth() + 1) + "/" + d.getDate() + " " + pad(h) + ":" + pad(m);
|
||
}
|
||
|
||
/* ==================== 会话列表(从 MongoDB 聚合) ==================== */
|
||
function loadConversations() {
|
||
var s = state();
|
||
if (!s.app || !s.app.appId) return;
|
||
var params = { page_size: 50 };
|
||
if (s.app.wxid) params.selfWxid = s.app.wxid;
|
||
Core.api("wx/conversation/list", params)
|
||
.then(function (list) {
|
||
s.conversations = list || [];
|
||
if (s.tab === "conversations") WxN.renderContacts();
|
||
fetchMissingChatroomBriefs();
|
||
// 初始化时自动加载第一条会话的聊天记录
|
||
if (!s.active && s.conversations.length > 0) {
|
||
console.log("[loadConversations] auto-opening first conversation: " + s.conversations[0].wxid);
|
||
openConversationChat(s.conversations[0].wxid);
|
||
}
|
||
})
|
||
.catch(function () {});
|
||
}
|
||
|
||
function fetchMissingChatroomBriefs() {
|
||
var s = state();
|
||
var missingChatrooms = [];
|
||
s.conversations.forEach(function (c) {
|
||
if (c.isGroup && !s.brief[c.wxid]) missingChatrooms.push(c.wxid);
|
||
});
|
||
if (!missingChatrooms.length) { fixMissingChatroomAvatars(); return; }
|
||
var batches = [];
|
||
for (var i = 0; i < missingChatrooms.length; i += 20) batches.push(missingChatrooms.slice(i, i + 20));
|
||
batches.forEach(function (batch) {
|
||
Core.wxApi("contacts/getBriefInfo", { appId: s.app.appId, wxids: batch })
|
||
.then(function (list) {
|
||
(list || []).forEach(function (info) { if (info && info.userName) s.brief[info.userName] = info; });
|
||
saveBriefCache();
|
||
if (s.tab === "conversations") WxN.renderContacts();
|
||
fixMissingChatroomAvatars();
|
||
})
|
||
.catch(function () {});
|
||
});
|
||
}
|
||
|
||
function fixMissingChatroomAvatars() {
|
||
var s = state();
|
||
s.conversations.forEach(function (c) {
|
||
if (!c.isGroup) return;
|
||
var info = s.brief[c.wxid] || {};
|
||
if (info.bigHeadImgUrl || info.smallHeadImgUrl) return;
|
||
Core.wxApi("group/getChatroomInfo", { appId: s.app.appId, chatroomId: c.wxid })
|
||
.then(function (data) {
|
||
if (!data || !data.chatroomId) return;
|
||
var existing = s.brief[c.wxid] || {};
|
||
existing.smallHeadImgUrl = existing.smallHeadImgUrl || data.smallHeadImgUrl || "";
|
||
existing.nickName = existing.nickName || data.nickName || "";
|
||
if (data.remark) existing.remark = existing.remark || data.remark;
|
||
s.brief[c.wxid] = existing;
|
||
saveBriefCache();
|
||
if (s.active && s.active.wxid === c.wxid) {
|
||
s.active.avatar = existing.bigHeadImgUrl || existing.smallHeadImgUrl || "";
|
||
s.active.name = existing.remark || existing.nickName || s.active.name;
|
||
WxN.renderChatHead();
|
||
}
|
||
if (s.tab === "conversations") WxN.renderContacts();
|
||
})
|
||
.catch(function () {});
|
||
});
|
||
}
|
||
|
||
function openConversationChat(wxid) {
|
||
var s = state();
|
||
var d = conversationDisplay(wxid);
|
||
var isRoom = wxid.indexOf("@chatroom") >= 0;
|
||
s.active = { wxid: wxid, name: d.name, avatar: d.avatar, type: isRoom ? "chatroom" : "friend", fromConv: true };
|
||
WxN.renderContacts();
|
||
var box = $("#wxMsgs", s.container);
|
||
box.innerHTML = '<div class="empty" style="margin:auto"><span class="spin"></span><div class="mt-1 fs-12">加载聊天记录…</div></div>';
|
||
var msgParams = { wxid: wxid, page_size: 200 };
|
||
if (s.app.wxid) msgParams.selfWxid = s.app.wxid;
|
||
Core.api("wx/conversation/msgs", msgParams)
|
||
.then(function (list) {
|
||
s.convMsgs[wxid] = list || [];
|
||
WxN.renderChat();
|
||
markConvAsRead(wxid);
|
||
if (isRoom) fetchGroupMemberList(wxid);
|
||
})
|
||
.catch(function () { s.convMsgs[wxid] = []; WxN.renderChat(); });
|
||
$("#wxText", s.container).focus();
|
||
}
|
||
|
||
function fetchGroupMemberList(chatroomId) {
|
||
var s = state();
|
||
if (!s.app || !s.app.appId) return;
|
||
Core.wxApi("group/getChatroomMemberList", { appId: s.app.appId, chatroomId: chatroomId })
|
||
.then(function (data) {
|
||
var members = (data && data.memberList) || [];
|
||
var map = {};
|
||
members.forEach(function (m) {
|
||
if (m && m.wxid) {
|
||
map[m.wxid] = {
|
||
nickName: m.nickName || m.displayName || m.wxid,
|
||
bigHeadImgUrl: m.bigHeadImgUrl || "",
|
||
smallHeadImgUrl: m.smallHeadImgUrl || ""
|
||
};
|
||
}
|
||
});
|
||
s.groupMembers[chatroomId] = map;
|
||
if (s.active && s.active.wxid === chatroomId && s.active.fromConv) WxN.renderChat();
|
||
})
|
||
.catch(function () {});
|
||
}
|
||
|
||
function markConvAsRead(wxid) {
|
||
var s = state();
|
||
if (!s.app || !s.app.appId) return;
|
||
var params = { wxid: wxid };
|
||
if (s.app.wxid) params.selfWxid = s.app.wxid;
|
||
Core.api("wx/conversation/read", params)
|
||
.then(function () {
|
||
s.conversations.forEach(function (c) { if (c.wxid === wxid) c.unreadCount = 0; });
|
||
if (s.tab === "conversations") WxN.renderContacts();
|
||
})
|
||
.catch(function () {});
|
||
}
|
||
|
||
function conversationDisplay(wxid) {
|
||
var s = state();
|
||
var info = s.brief[wxid] || {};
|
||
return { name: info.remark || info.nickName || wxid, avatar: info.bigHeadImgUrl || info.smallHeadImgUrl || "" };
|
||
}
|
||
|
||
/* ==================== 联系人详情悬浮气泡 ==================== */
|
||
var _detailCache = {};
|
||
var _detailTimer = null;
|
||
var _detailPopup = null;
|
||
|
||
function showContactDetail(wxid, el, e) {
|
||
clearTimeout(_detailTimer);
|
||
hideContactDetail();
|
||
_detailTimer = setTimeout(function () {
|
||
var rect = el.getBoundingClientRect();
|
||
var popup = document.createElement('div');
|
||
popup.className = 'wx-detail-popup';
|
||
popup.innerHTML = '<div class="wx-detail-loading"><span class="spin"></span> 加载中…</div>';
|
||
popup.style.position = 'fixed';
|
||
popup.style.left = (rect.right + 8) + 'px';
|
||
popup.style.top = rect.top + 'px';
|
||
popup.style.zIndex = '9999';
|
||
document.body.appendChild(popup);
|
||
_detailPopup = popup;
|
||
if (_detailCache[wxid]) { renderDetailPopup(_detailCache[wxid]); return; }
|
||
var s = state();
|
||
Core.wxApi("contacts/getDetailInfo", { appId: s.app.appId, wxids: [wxid] })
|
||
.then(function (list) {
|
||
if (!list || !list.length) { renderDetailPopup(null); return; }
|
||
_detailCache[wxid] = list[0];
|
||
renderDetailPopup(list[0]);
|
||
})
|
||
.catch(function () { renderDetailPopup(null); });
|
||
}, 400);
|
||
}
|
||
|
||
function renderDetailPopup(info) {
|
||
if (!_detailPopup) return;
|
||
if (!info) { _detailPopup.innerHTML = '<div class="wx-detail-empty">暂无详情</div>'; return; }
|
||
var sex = info.sex === 1 ? '♂ 男' : info.sex === 2 ? '♀ 女' : '未知';
|
||
var sexClass = info.sex === 1 ? 'male' : info.sex === 2 ? 'female' : '';
|
||
var html = '<div class="wx-detail-card">';
|
||
html += '<div class="wx-detail-head">';
|
||
if (info.bigHeadImgUrl || info.smallHeadImgUrl) {
|
||
html += '<img class="wx-detail-avatar" src="' + Core.esc(info.bigHeadImgUrl || info.smallHeadImgUrl) + '" alt="" onerror="this.style.display=\'none\'">';
|
||
} else {
|
||
html += '<div class="wx-detail-avatar wx-detail-avatar-text">' + Core.esc(Core.initial(info.nickName || info.userName)) + '</div>';
|
||
}
|
||
html += '<div class="wx-detail-info">';
|
||
html += '<div class="wx-detail-name">' + Core.esc(info.nickName || info.userName) + '</div>';
|
||
html += '<div class="wx-detail-wxid">' + Core.esc(info.userName) + '</div>';
|
||
html += '</div>';
|
||
html += '<span class="wx-detail-sex ' + sexClass + '">' + sex + '</span>';
|
||
html += '</div>';
|
||
html += '<div class="wx-detail-body">';
|
||
if (info.remark) html += '<div class="wx-detail-row"><span class="wx-detail-label">备注</span><span class="wx-detail-value">' + Core.esc(info.remark) + '</span></div>';
|
||
if (info.alias) html += '<div class="wx-detail-row"><span class="wx-detail-label">微信号</span><span class="wx-detail-value">' + Core.esc(info.alias) + '</span></div>';
|
||
if (info.signature) html += '<div class="wx-detail-row"><span class="wx-detail-label">签名</span><span class="wx-detail-value wx-detail-signature">' + Core.esc(info.signature) + '</span></div>';
|
||
if (info.province || info.city) html += '<div class="wx-detail-row"><span class="wx-detail-label">地区</span><span class="wx-detail-value">' + Core.esc((info.province || '') + ' ' + (info.city || '')) + '</span></div>';
|
||
if (info.phoneNumList) html += '<div class="wx-detail-row"><span class="wx-detail-label">手机</span><span class="wx-detail-value">' + Core.esc(info.phoneNumList) + '</span></div>';
|
||
if (info.description) html += '<div class="wx-detail-row"><span class="wx-detail-label">描述</span><span class="wx-detail-value wx-detail-signature">' + Core.esc(info.description) + '</span></div>';
|
||
html += '</div></div>';
|
||
_detailPopup.innerHTML = html;
|
||
}
|
||
|
||
function hideContactDetail() {
|
||
clearTimeout(_detailTimer);
|
||
if (_detailPopup && _detailPopup.parentNode) _detailPopup.parentNode.removeChild(_detailPopup);
|
||
_detailPopup = null;
|
||
}
|
||
|
||
function clearDetailCache() { _detailCache = {}; }
|
||
|
||
/* ==================== 聊天 ==================== */
|
||
function openChat(wxid) {
|
||
var s = state();
|
||
var d = WxN.contactDisplay(wxid);
|
||
var isRoom = s.contacts.chatrooms.indexOf(wxid) >= 0;
|
||
s.active = { wxid: wxid, name: d.name, avatar: d.avatar, type: isRoom ? "chatroom" : "friend" };
|
||
if (!s.msgs[wxid]) s.msgs[wxid] = [];
|
||
WxN.renderContacts();
|
||
WxN.renderChat();
|
||
$("#wxText", s.container).focus();
|
||
}
|
||
|
||
function renderChatHead() {
|
||
var s = state();
|
||
var box = $("#wxChatHead", s.container);
|
||
var a = s.active;
|
||
if (!a) {
|
||
box.innerHTML = '<div class="wx-title c-dim">未选择会话</div><span class="spacer"></span>' +
|
||
'<span class="badge ' + (s.ai.sessionId ? "green" : "gray") + '">' + (s.ai.sessionId ? "AI 会话就绪" : "AI 未注册") + "</span>" +
|
||
'<button class="btn ghost sm" id="wxAiConfig">⚙ AI 助手设置</button>';
|
||
} else {
|
||
box.innerHTML =
|
||
(a.avatar ? '<img class="wx-avatar" src="' + Core.esc(a.avatar) + '" alt="">' : '<div class="wx-avatar">' + Core.esc(Core.initial(a.name)) + "</div>") +
|
||
"<div><div class=\"wx-title\">" + Core.esc(a.name) + "</div>" +
|
||
'<div class="wx-sub mono">' + Core.esc(a.wxid) + (a.type === "chatroom" ? " · 群聊" : "") + "</div></div>" +
|
||
'<span class="spacer"></span>' +
|
||
'<span class="badge ' + (s.ai.sessionId ? "green" : "gray") + '">' + (s.ai.sessionId ? "AI 会话就绪" : "AI 未注册") + "</span>" +
|
||
'<button class="btn ghost sm" id="wxAiConfig">⚙ AI 助手设置</button>';
|
||
}
|
||
$("#wxAiConfig", box).onclick = WxN.openAiConfig;
|
||
}
|
||
|
||
function renderChat() {
|
||
renderChatHead();
|
||
var s = state();
|
||
var box = $("#wxMsgs", s.container);
|
||
var a = s.active;
|
||
if (!a) {
|
||
box.innerHTML = '<div class="empty" style="margin:auto"><div class="big">◈</div>选择左侧联系人开始对话<br><span class="fs-12">发送内容将通过微信协议接口投递</span></div>';
|
||
return;
|
||
}
|
||
if (a.fromConv) {
|
||
var convMsgList = s.convMsgs[a.wxid] || [];
|
||
if (!convMsgList.length) {
|
||
box.innerHTML = '<div class="empty" style="margin:auto"><div class="big">✦</div>与「' + Core.esc(a.name) + '」暂无聊天记录<br><span class="fs-12">收到对方消息后将自动显示在这里</span></div>';
|
||
return;
|
||
}
|
||
box.innerHTML = renderConvMsgs(convMsgList);
|
||
box.scrollTop = box.scrollHeight;
|
||
return;
|
||
}
|
||
var msgs = s.msgs[a.wxid] || [];
|
||
if (!msgs.length) {
|
||
box.innerHTML = '<div class="empty" style="margin:auto"><div class="big">✦</div>与「' + Core.esc(a.name) + '」的会话已就绪<br><span class="fs-12">输入内容点击发送;也可粘贴客户消息后点击「AI 建议」获取话术</span></div>';
|
||
return;
|
||
}
|
||
box.innerHTML = msgs.map(function (m) {
|
||
var time = Core.esc(m.time || "");
|
||
if (m.role === "system") return '<div class="text-center fs-12 c-dim" style="width:100%">' + time + " · " + Core.esc(m.text) + "</div>";
|
||
if (m.role === "ai") return '<div class="msg-row"><div class="msg-avatar">✦</div><div><div class="msg-bubble" style="border-color:rgba(45,224,255,.4)">' + Core.esc(m.text) + '</div><div class="msg-time">' + time + " · AI 建议</div></div></div>";
|
||
return '<div class="msg-row self"><div class="msg-avatar">' + Core.esc(Core.initial((s.app && s.app.nickName) || "我")) + '</div><div><div class="msg-bubble' + (m.fail ? " fail" : "") + '">' + Core.esc(m.text) + '</div><div class="msg-time">' + time + (m.fail ? " · 发送失败" : "") + "</div></div></div>";
|
||
}).join("");
|
||
box.scrollTop = box.scrollHeight;
|
||
}
|
||
|
||
function renderConvMsgs(msgList) {
|
||
var s = state();
|
||
var selfName = (s.app && s.app.nickName) || "我";
|
||
var isGroupChat = s.active && s.active.type === "chatroom";
|
||
var members = (s.active && s.groupMembers && s.groupMembers[s.active.wxid]) || {};
|
||
var html = '';
|
||
var lastDate = '';
|
||
for (var i = 0; i < msgList.length; i++) {
|
||
var m = msgList[i];
|
||
var timeStr = fmtMsgTime(m.createAt);
|
||
var d = new Date(typeof m.createAt === "number" && m.createAt < 1e12 ? m.createAt * 1000 : m.createAt);
|
||
var dateKey = !isNaN(d.getTime()) ? d.toDateString() : "";
|
||
if (dateKey && dateKey !== lastDate) {
|
||
lastDate = dateKey;
|
||
html += '<div class="conv-date"><span>' + Core.esc(fmtMsgDate(d)) + '</span></div>';
|
||
}
|
||
if (m.msgType === "system" || m.msgType === "group_sys") {
|
||
html += '<div class="conv-sys">' + Core.esc(m.content || "系统消息") + '</div>';
|
||
continue;
|
||
}
|
||
var isSelf = m.direction === "self" || m.direction === "ai";
|
||
var senderName = '', senderAvatar = '';
|
||
if (isGroupChat && !isSelf) {
|
||
var senderWxid = m.senderWxid || '';
|
||
var memberInfo = members[senderWxid] || {};
|
||
senderName = memberInfo.nickName || senderWxid || '';
|
||
senderAvatar = memberInfo.bigHeadImgUrl || memberInfo.smallHeadImgUrl || '';
|
||
}
|
||
var avatarHtml;
|
||
if (m.direction === "ai") {
|
||
avatarHtml = '<div class="msg-avatar" style="background:rgba(45,224,255,0.15)">✦</div>';
|
||
} else if (isSelf) {
|
||
avatarHtml = '<div class="msg-avatar">' + Core.esc(Core.initial(selfName)) + '</div>';
|
||
} else if (isGroupChat && senderAvatar) {
|
||
avatarHtml = '<img class="msg-avatar" src="' + Core.esc(senderAvatar) + '" alt="" onerror="this.replaceWith(Object.assign(document.createElement(\'div\'),{className:\'msg-avatar\',textContent:\'' + Core.esc(Core.initial(senderName || '?')) + '\'}))">';
|
||
} else if (isGroupChat) {
|
||
avatarHtml = '<div class="msg-avatar">' + Core.esc(Core.initial(senderName || '?')) + '</div>';
|
||
} else {
|
||
var convAvatar = (s.active && s.active.avatar) || '';
|
||
if (convAvatar) {
|
||
avatarHtml = '<img class="msg-avatar" src="' + Core.esc(convAvatar) + '" alt="" onerror="this.replaceWith(Object.assign(document.createElement(\'div\'),{className:\'msg-avatar\',textContent:\'' + Core.esc(Core.initial((s.active && s.active.name) || '?')) + '\'}))">';
|
||
} else {
|
||
avatarHtml = '<div class="msg-avatar">' + Core.esc(Core.initial((s.active && s.active.name) || '?')) + '</div>';
|
||
}
|
||
}
|
||
var bubbleClass = isSelf ? 'msg-bubble conv-msg-self' : 'msg-bubble conv-msg-other';
|
||
var contentHtml = renderMsgContent(m.msgType, m.content, isSelf);
|
||
var senderNameHtml = (isGroupChat && !isSelf && senderName) ? '<div class="msg-sender-name">' + Core.esc(senderName) + '</div>' : '';
|
||
html += '<div class="msg-row' + (isSelf ? ' self' : '') + '">' + avatarHtml +
|
||
'<div>' + senderNameHtml +
|
||
'<div class="' + bubbleClass + '">' + contentHtml + '</div>' +
|
||
'<div class="msg-time">' + Core.esc(timeStr) + (m.direction === "ai" ? " · AI" : "") + "</div></div></div>";
|
||
}
|
||
return html;
|
||
}
|
||
|
||
function renderMsgContent(msgType, content, isSelf) {
|
||
if (!content) content = "";
|
||
switch (msgType) {
|
||
case "text": return Core.esc(content);
|
||
case "image":
|
||
var imgUrl = extractImgUrl(content);
|
||
if (imgUrl) return '<img class="conv-img" src="' + Core.esc(imgUrl) + '" alt="[图片]" onerror="this.outerHTML=\'<span>[图片]</span>\'">';
|
||
return '<span class="conv-media">[图片]</span>';
|
||
case "emoji": return '<span class="conv-media">😊 [表情]</span>';
|
||
case "voice": return '<span class="conv-media">🎤 [语音]</span>';
|
||
case "video": return '<span class="conv-media">🎬 [视频]</span>';
|
||
case "location": return '<span class="conv-media">📍 [位置]</span>';
|
||
case "namecard": return '<span class="conv-media">👤 [名片]</span>';
|
||
case "app_msg":
|
||
var title = extractAppMsgTitle(content);
|
||
return '<span class="conv-media">🔗 ' + Core.esc(title || "[链接/小程序]") + '</span>';
|
||
case "friend_req": return '<span class="conv-media">👥 [好友请求]</span>';
|
||
default: return '<span class="conv-media">[' + Core.esc(msgType || "消息") + ']</span>';
|
||
}
|
||
}
|
||
|
||
function extractImgUrl(xml) {
|
||
if (!xml) return "";
|
||
var m = xml.match(/cdnurl\s*=\s*"([^"]+)"/);
|
||
if (m && m[1]) return m[1];
|
||
m = xml.match(/aesurl\s*=\s*"([^"]+)"/);
|
||
if (m && m[1]) return m[1];
|
||
m = xml.match(/<url>\s*<!\[CDATA\[([^\]]+)\]/);
|
||
if (m && m[1]) return m[1];
|
||
if (xml.indexOf("http") === 0) return xml.trim();
|
||
return "";
|
||
}
|
||
|
||
function extractAppMsgTitle(xml) {
|
||
if (!xml) return "";
|
||
var m = xml.match(/<title>\s*<!\[CDATA\[([^\]]*)\]/);
|
||
if (m) return m[1];
|
||
m = xml.match(/<title>([^<]+)</);
|
||
if (m) return m[1];
|
||
return "";
|
||
}
|
||
|
||
function fmtMsgDate(d) {
|
||
var now = new Date();
|
||
if (d.toDateString() === now.toDateString()) return "今天";
|
||
var yesterday = new Date(now);
|
||
yesterday.setDate(now.getDate() - 1);
|
||
if (d.toDateString() === yesterday.toDateString()) return "昨天";
|
||
return (d.getMonth() + 1) + "月" + d.getDate() + "日";
|
||
}
|
||
|
||
/* ==================== 发送消息 ==================== */
|
||
function doSend() {
|
||
var s = state();
|
||
if (!s.active) { Core.toast("请先选择联系人", "err"); return; }
|
||
var text = $("#wxText", s.container).value.trim();
|
||
if (!text) { Core.toast("请输入消息内容", "err"); return; }
|
||
$("#wxText", s.container).value = "";
|
||
sendBubbles(s.active.wxid, text);
|
||
}
|
||
|
||
function sendText(wxid, text, opts) {
|
||
opts = opts || {};
|
||
var s = state();
|
||
var hint = $("#wxSendHint", s.container);
|
||
if (hint) hint.textContent = "发送中…";
|
||
return Core.wxApi("message/postText", { appId: s.app.appId, toWxid: wxid, content: text, ats: "" })
|
||
.then(function () {
|
||
if (s.msgs[wxid] === undefined) s.msgs[wxid] = [];
|
||
s.msgs[wxid].push({ role: "self", text: text, time: Core.nowTime() });
|
||
if (s.convMsgs[wxid]) {
|
||
s.convMsgs[wxid].push({ direction: "self", msgType: "text", content: text, createAt: Math.floor(Date.now() / 1000) });
|
||
}
|
||
if (hint) hint.textContent = "已发送";
|
||
renderChat();
|
||
loadConversations();
|
||
if (!opts.quiet) Core.toast("已发送", "ok");
|
||
})
|
||
.catch(function (e) {
|
||
if (s.msgs[wxid] === undefined) s.msgs[wxid] = [];
|
||
s.msgs[wxid].push({ role: "self", text: text, time: Core.nowTime(), fail: true });
|
||
if (hint) hint.textContent = "发送失败";
|
||
renderChat();
|
||
Core.toast("发送失败:" + e.message, "err");
|
||
});
|
||
}
|
||
|
||
function sendBubbles(wxid, text) {
|
||
var bubbles = Core.splitBubbles(text);
|
||
if (!bubbles.length) return Promise.resolve();
|
||
var hint = $("#wxSendHint", state().container);
|
||
var i = 0;
|
||
function next() {
|
||
if (i >= bubbles.length) {
|
||
if (hint) hint.textContent = bubbles.length > 1 ? "已发送 " + bubbles.length + " 条" : "已发送";
|
||
return Promise.resolve();
|
||
}
|
||
var b = bubbles[i++];
|
||
if (hint) hint.textContent = "发送中…" + (bubbles.length > 1 ? "(" + i + "/" + bubbles.length + ")" : "");
|
||
return sendText(wxid, b, { quiet: true }).then(function () {
|
||
if (i >= bubbles.length) return;
|
||
return Core.sleep(900 + Math.random() * 1600).then(next);
|
||
});
|
||
}
|
||
return next().then(function () {
|
||
Core.toast(bubbles.length > 1 ? "已发送(" + bubbles.length + " 条消息)" : "已发送", "ok");
|
||
});
|
||
}
|
||
|
||
/* ==================== WebSocket 新消息处理 ==================== */
|
||
function handleNewMessage(data) {
|
||
if (!data || !data.wxid) return;
|
||
var s = state();
|
||
var wxid = data.wxid;
|
||
if (!s.convMsgs[wxid]) s.convMsgs[wxid] = [];
|
||
s.convMsgs[wxid].push({
|
||
direction: data.direction || "customer", msgType: data.msgType || "text",
|
||
content: data.content || "", senderWxid: data.senderWxid || "",
|
||
createAt: data.createAt || Math.floor(Date.now() / 1000)
|
||
});
|
||
if (s.active && s.active.wxid === wxid && s.active.fromConv) {
|
||
renderChat();
|
||
markConvAsRead(wxid);
|
||
} else if (data.direction === "customer") {
|
||
s.conversations.forEach(function (c) { if (c.wxid === wxid) c.unreadCount = (c.unreadCount || 0) + 1; });
|
||
}
|
||
loadConversations();
|
||
}
|
||
|
||
/* ==================== 导出 ==================== */
|
||
WxN.timeAgo = timeAgo;
|
||
WxN.fmtMsgTime = fmtMsgTime;
|
||
WxN.loadConversations = loadConversations;
|
||
WxN.openConversationChat = openConversationChat;
|
||
WxN.markConvAsRead = markConvAsRead;
|
||
WxN.conversationDisplay = conversationDisplay;
|
||
WxN.showContactDetail = showContactDetail;
|
||
WxN.hideContactDetail = hideContactDetail;
|
||
WxN.clearDetailCache = clearDetailCache;
|
||
WxN.openChat = openChat;
|
||
WxN.renderChatHead = renderChatHead;
|
||
WxN.renderChat = renderChat;
|
||
WxN.doSend = doSend;
|
||
WxN.sendBubbles = sendBubbles;
|
||
WxN.handleNewMessage = handleNewMessage;
|
||
|
||
})(window);
|