357 lines
16 KiB
JavaScript
357 lines
16 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; }
|
||
|
||
// 登录地区列表
|
||
var REGION_LIST = [
|
||
{ id: "110000", name: "北京市" }, { id: "120000", name: "天津市" },
|
||
{ id: "130000", name: "河北省" }, { id: "140000", name: "山西省" },
|
||
{ id: "210000", name: "辽宁省" }, { id: "220000", name: "吉林省" },
|
||
{ id: "230000", name: "黑龙江省" }, { id: "310000", name: "上海市" },
|
||
{ id: "320000", name: "江苏省" }, { id: "330000", name: "浙江省" },
|
||
{ id: "340000", name: "安徽省" }, { id: "350000", name: "福建省" },
|
||
{ id: "360000", name: "江西省" }, { id: "370000", name: "山东省" },
|
||
{ id: "410000", name: "河南省" }, { id: "420000", name: "湖北省" },
|
||
{ id: "430000", name: "湖南省" }, { id: "440000", name: "广东省" },
|
||
{ id: "450000", name: "广西省" }, { id: "460000", name: "海南省" },
|
||
{ id: "500000", name: "重庆市" }, { id: "510000", name: "四川省" },
|
||
{ id: "520000", name: "贵州省" }, { id: "530000", name: "云南省" },
|
||
{ id: "540000", name: "西藏" }, { id: "610000", name: "陕西省" },
|
||
{ id: "620000", name: "甘肃省" }, { id: "630000", name: "青海省" },
|
||
{ id: "640000", name: "宁夏" }
|
||
];
|
||
|
||
/* ==================== 登录卡片 ==================== */
|
||
function renderLoginCard() {
|
||
var s = state();
|
||
var box = $("#wxLoginCard", s.container);
|
||
var app = s.app;
|
||
if ((!app || !app.appId) || s.qr) {
|
||
var qr = s.qr;
|
||
var qrImg = qr && qr.img
|
||
? '<img src="' + (qr.img.indexOf("data:") === 0 ? qr.img : "data:image/png;base64," + qr.img) + '" alt="微信登录二维码">'
|
||
: '<div class="empty" style="padding:24px 0"><div class="big">▣</div>点击下方按钮获取登录二维码</div>';
|
||
var statusText = "等待获取二维码";
|
||
if (qr) {
|
||
statusText = qr.status === 0 ? "请使用微信扫码" : qr.status === 1 ? "已扫码,请在手机上确认登录" : "登录成功";
|
||
if (qr.expired) statusText = "二维码已过期,请重新获取";
|
||
}
|
||
box.innerHTML =
|
||
'<div class="wx-login-card">' +
|
||
'<div class="row mb-1"><span class="badge blue"><span class="dot live"></span>微信未登录</span></div>' +
|
||
'<div class="wx-qr-box">' + qrImg + '<div class="qrs">' + statusText + "</div></div>" +
|
||
'<button class="btn primary sm block" id="wxQrBtn">' + (qr ? "重新获取二维码" : "获取登录二维码") + "</button>" +
|
||
"</div>";
|
||
$("#wxQrBtn", box).onclick = getQrCode;
|
||
return;
|
||
}
|
||
box.innerHTML =
|
||
'<div class="wx-login-card">' +
|
||
'<div class="wx-user-row">' +
|
||
(app.headImgUrl
|
||
? '<img class="wx-avatar" src="' + Core.esc(app.headImgUrl) + '" alt="" onerror="this.replaceWith(Object.assign(document.createElement(\'div\'),{className:\'wx-avatar\',textContent:\'' + Core.esc(Core.initial(app.nickName || app.wxid)) + '\'}))">'
|
||
: '<div class="wx-avatar">' + Core.esc(Core.initial(app.nickName || app.wxid)) + "</div>") +
|
||
'<div style="min-width:0;flex:1">' +
|
||
'<div class="ci-name">' + Core.esc(app.nickName || "微信用户") + "</div>" +
|
||
'<div class="ci-sub mono">' + Core.esc(app.wxid || app.appId) + "</div>" +
|
||
"</div>" +
|
||
(s.online
|
||
? '<span class="badge green"><span class="dot live"></span>在线</span>'
|
||
: '<span class="badge red"><span class="dot"></span>离线</span>') +
|
||
"</div>" +
|
||
'<div class="row mt-1">' +
|
||
'<button class="btn ghost sm" id="wxOnlineBtn">检测在线</button>' +
|
||
'<button class="btn danger sm" id="wxLogoutBtn">退出登录</button>' +
|
||
"</div>" +
|
||
"</div>";
|
||
$("#wxOnlineBtn", box).onclick = function () { checkOnline().then(function () { renderLoginCard(); }); };
|
||
$("#wxLogoutBtn", box).onclick = doLogout;
|
||
}
|
||
|
||
/* ---------- 扫码登录 ---------- */
|
||
function showRegionDialog() {
|
||
return new Promise(function (resolve) {
|
||
var optionsHtml = REGION_LIST.map(function (r) {
|
||
return '<option value="' + r.id + '">' + Core.esc(r.name) + '</option>';
|
||
}).join('');
|
||
var m = Core.modal({
|
||
title: '选择登录地区',
|
||
body: '<div style="font-size:13px;line-height:1.8;margin-bottom:10px">请选择您当前所在的省市地区,用于微信登录接入。</div>' +
|
||
'<div class="field"><label class="label">登录地区</label>' +
|
||
'<select id="wx-region-select" class="select">' + optionsHtml + '</select></div>' +
|
||
'<div class="fs-12 c-dim" style="line-height:1.8">' +
|
||
'· 请选择最近的省市,若没有您的地区可自行采购 socks5 代理 IP<br>' +
|
||
'· 平台代理仅做测试,长用建议自己采购独占代理</div>' +
|
||
'<div id="wx-geo-hint" class="fs-12 c-dim" style="margin-top:6px"></div>',
|
||
footer: '<button class="btn ghost rg-cancel">取消</button><button class="btn primary rg-ok">确认</button>'
|
||
});
|
||
$('.rg-cancel', m.root).onclick = function () { m.close(); resolve(null); };
|
||
$('.rg-ok', m.root).onclick = function () {
|
||
var regionId = $('#wx-region-select', m.root).value;
|
||
m.close();
|
||
resolve(regionId);
|
||
};
|
||
var geoHint = $('#wx-geo-hint', m.root);
|
||
if (navigator.geolocation && geoHint) {
|
||
geoHint.textContent = '正在尝试定位…';
|
||
navigator.geolocation.getCurrentPosition(
|
||
function (pos) {
|
||
var lat = pos.coords.latitude;
|
||
var lng = pos.coords.longitude;
|
||
var guessed = guessRegionByCoords(lat, lng);
|
||
if (guessed) {
|
||
$('#wx-region-select', m.root).value = guessed;
|
||
geoHint.innerHTML = '<span class="c-success">已自动定位到 ' + Core.esc(REGION_LIST.filter(function(r){return r.id===guessed;})[0].name) + ',可手动修改</span>';
|
||
} else {
|
||
geoHint.textContent = '定位成功但无法匹配地区,请手动选择';
|
||
}
|
||
},
|
||
function () { geoHint.textContent = '无法获取定位,请手动选择地区'; },
|
||
{ timeout: 5000 }
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
function guessRegionByCoords(lat, lng) {
|
||
var coords = [
|
||
{ id: "110000", lat: 39.9, lng: 116.4 }, { id: "120000", lat: 39.1, lng: 117.2 },
|
||
{ id: "310000", lat: 31.2, lng: 121.5 }, { id: "440000", lat: 23.1, lng: 113.3 },
|
||
{ id: "330000", lat: 30.3, lng: 120.2 }, { id: "320000", lat: 32.1, lng: 118.8 },
|
||
{ id: "510000", lat: 30.6, lng: 104.1 }, { id: "420000", lat: 30.6, lng: 114.3 },
|
||
{ id: "430000", lat: 28.2, lng: 112.9 }, { id: "370000", lat: 36.7, lng: 117.0 },
|
||
{ id: "410000", lat: 34.8, lng: 113.7 }, { id: "130000", lat: 38.0, lng: 114.5 },
|
||
{ id: "210000", lat: 41.8, lng: 123.4 }, { id: "610000", lat: 34.3, lng: 108.9 },
|
||
{ id: "340000", lat: 31.8, lng: 117.3 }, { id: "350000", lat: 26.1, lng: 119.3 },
|
||
{ id: "360000", lat: 28.7, lng: 115.9 }, { id: "500000", lat: 29.6, lng: 106.5 },
|
||
{ id: "530000", lat: 25.0, lng: 102.7 }, { id: "520000", lat: 26.6, lng: 106.7 },
|
||
{ id: "450000", lat: 22.8, lng: 108.3 }, { id: "460000", lat: 20.0, lng: 110.3 },
|
||
{ id: "140000", lat: 37.9, lng: 112.5 }, { id: "220000", lat: 43.9, lng: 125.3 },
|
||
{ id: "230000", lat: 45.8, lng: 126.5 }, { id: "620000", lat: 36.1, lng: 103.8 },
|
||
{ id: "630000", lat: 36.6, lng: 101.8 }, { id: "640000", lat: 38.5, lng: 106.3 },
|
||
{ id: "540000", lat: 29.6, lng: 91.1 }
|
||
];
|
||
var minDist = Infinity, matched = null;
|
||
for (var i = 0; i < coords.length; i++) {
|
||
var d = Math.pow(lat - coords[i].lat, 2) + Math.pow(lng - coords[i].lng, 2);
|
||
if (d < minDist) { minDist = d; matched = coords[i].id; }
|
||
}
|
||
return matched;
|
||
}
|
||
|
||
function getQrCode() {
|
||
stopLoginPolling();
|
||
showRegionDialog().then(function (regionId) {
|
||
if (!regionId) return;
|
||
Core.loading(true, "正在获取登录二维码…");
|
||
Core.wxApi("login/getLoginQrCode", { appId: "", regionId: regionId, type: "mac" })
|
||
.then(function (data) {
|
||
Core.loading(false);
|
||
if (!data || !data.uuid) { Core.toast("获取二维码失败", "err"); return; }
|
||
state().qr = { uuid: data.uuid, img: data.qrImgBase64, status: 0, expired: false, appId: data.appId || "", regionId: regionId };
|
||
// 将接口返回的 appId 更新到销售的 wx_device_id
|
||
var hashMatch = (location.hash || '').match(/wx-advicer=(\d+)/);
|
||
var advicerId = hashMatch ? Number(hashMatch[1]) : 0;
|
||
if (data.appId && advicerId) {
|
||
Core.api("advicer/wx_device/update", { advicerId: advicerId, wxDeviceId: data.appId })
|
||
.then(function () { console.log("[getQrCode] wx_device_id updated: " + data.appId); })
|
||
.catch(function (e) { console.warn("[getQrCode] update wx_device_id failed:", e); });
|
||
}
|
||
renderLoginCard();
|
||
startLoginPolling();
|
||
})
|
||
.catch(function (e) { Core.loading(false); Core.toast("获取二维码失败:" + e.message, "err"); });
|
||
});
|
||
}
|
||
|
||
function startLoginPolling() {
|
||
stopLoginPolling();
|
||
var s = state();
|
||
s.loginTimer = setInterval(function () {
|
||
s = state();
|
||
if (!s || !s.qr) return;
|
||
Core.wxApi("login/checkLogin", {
|
||
appId: s.qr.appId || "", uuid: s.qr.uuid,
|
||
autoSliding: true, proxyIp: "", captchCode: ""
|
||
}).then(function (data) {
|
||
if (!data) return;
|
||
if (s.qr) {
|
||
s.qr.status = data.status;
|
||
if (typeof data.expiredTime === "number" && data.status !== 2 && data.expiredTime <= 0) {
|
||
s.qr.expired = true;
|
||
}
|
||
}
|
||
if (data.status === 2) {
|
||
stopLoginPolling();
|
||
var app = {
|
||
appId: s.qr.appId || "",
|
||
wxid: data.loginInfo ? data.loginInfo.wxid : "",
|
||
nickName: data.nickName || (data.loginInfo && data.loginInfo.nickName) || "",
|
||
headImgUrl: data.headImgUrl || ""
|
||
};
|
||
s.app = app;
|
||
Core.store(Core.KEYS.wxApp, app);
|
||
s.qr = null;
|
||
Core.toast("微信登录成功:" + (app.nickName || app.wxid), "ok");
|
||
renderLoginCard();
|
||
checkOnlineLoop();
|
||
// 登录成功后回写 wx_id 到销售记录
|
||
var hashMatch2 = (location.hash || '').match(/wx-advicer=(\d+)/);
|
||
var advicerId2 = hashMatch2 ? Number(hashMatch2[1]) : 0;
|
||
if (advicerId2 && app.wxid) {
|
||
Core.api("advicer/wx_id/update", { advicerId: advicerId2, wxId: app.wxid })
|
||
.then(function () { console.log("[login] wx_id updated: " + app.wxid); })
|
||
.catch(function (e) { console.warn("[login] update wx_id failed:", e); });
|
||
}
|
||
WxN.loadProfile().then(function () { WxN.loadConversations(); });
|
||
WxN.loadContacts();
|
||
WxN.loadLabels();
|
||
} else {
|
||
renderLoginCard();
|
||
}
|
||
}).catch(function () { });
|
||
}, 10000);
|
||
}
|
||
|
||
function stopLoginPolling() {
|
||
var s = state();
|
||
if (s && s.loginTimer) { clearInterval(s.loginTimer); s.loginTimer = null; }
|
||
}
|
||
|
||
function checkOnlineLoop() {
|
||
WxN.connectWs();
|
||
checkOnline();
|
||
}
|
||
|
||
function checkOnline() {
|
||
var s = state();
|
||
if (!s || !s.app || !s.app.appId) {
|
||
console.log("[checkOnline] skipped: state=" + !!s + ", app=" + (s && s.app ? s.app.appId : "null"));
|
||
return Promise.resolve(false);
|
||
}
|
||
console.log("[checkOnline] calling with appId=" + s.app.appId);
|
||
return Core.wxApi("login/checkOnline", { appId: s.app.appId })
|
||
.then(function (v) {
|
||
s.online = v === true;
|
||
renderLoginCard();
|
||
return s.online;
|
||
})
|
||
.catch(function () {
|
||
state().online = false;
|
||
renderLoginCard();
|
||
return false;
|
||
});
|
||
}
|
||
|
||
function resetToOffline() {
|
||
WxN.disconnectWs();
|
||
var s = state();
|
||
s.app = null;
|
||
s.online = false;
|
||
s.contacts = { friends: [], chatrooms: [], ghs: [] };
|
||
s.conversations = [];
|
||
s.convMsgs = {};
|
||
s.brief = {};
|
||
s.msgs = {};
|
||
s.active = null;
|
||
Core.store(Core.KEYS.wxApp, null);
|
||
if (location.hash) history.replaceState(null, '', location.pathname + location.search);
|
||
WxN.clearBriefCache();
|
||
renderLoginCard();
|
||
WxN.renderContacts();
|
||
WxN.renderChat();
|
||
}
|
||
|
||
/* ---------- 在线状态 WebSocket 事件 ---------- */
|
||
function handleOnlineStatus(data) {
|
||
if (!data) return;
|
||
var s = state();
|
||
var wasOnline = s.online;
|
||
s.online = data.online === true;
|
||
if (!s.online && wasOnline) {
|
||
Core.toast("微信已离线", "warn");
|
||
renderLoginCard();
|
||
} else if (s.online && !wasOnline) {
|
||
Core.toast("微信已恢复在线", "ok");
|
||
renderLoginCard();
|
||
} else {
|
||
renderLoginCard();
|
||
}
|
||
}
|
||
|
||
function doLogout() {
|
||
var s = state();
|
||
Core.confirm("确定退出微信登录?退出后需重新扫码。").then(function (ok) {
|
||
if (!ok) return;
|
||
Core.wxApi("login/logout", { appId: state().app.appId })
|
||
.then(function () {
|
||
Core.toast("已退出微信登录", "ok");
|
||
resetToOffline();
|
||
})
|
||
.catch(function (e) {
|
||
Core.wxApi("login/checkOnline", { appId: state().app.appId })
|
||
.then(function (online) {
|
||
if (online !== true) {
|
||
Core.toast("微信已离线,自动回到未登录状态", "warn");
|
||
resetToOffline();
|
||
} else {
|
||
Core.toast("退出登录失败:" + e.message, "err");
|
||
}
|
||
})
|
||
.catch(function () { Core.toast("退出登录失败:" + e.message, "err"); });
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ---------- 消息回调设置 ---------- */
|
||
function openCallbackDialog() {
|
||
Core.loading(true, "获取回调配置…");
|
||
Core.api("wx/callback/info", {}).then(function (info) {
|
||
Core.loading(false);
|
||
var suggested = (info && info.callbackUrl) || (location.origin + "/api/v1/advicer/wx/callback");
|
||
var m = Core.modal({
|
||
title: "设置消息回调",
|
||
body:
|
||
'<div class="field"><label class="label">回调地址<span class="req">*</span></label>' +
|
||
'<input id="wcb-url" class="input mono" value="' + Core.esc(suggested) + '"></div>' +
|
||
'<div class="fs-12 c-dim" style="line-height:1.9">' +
|
||
"· 需为公网可访问地址,微信协议服务会把客户与销售的消息推送到该地址;<br>" +
|
||
"· 配置成功后消息自动入库,用于客户画像迭代、每晚等级评估与 AI 主动对话;<br>" +
|
||
"· 若服务器部署在反向代理后,请把域名替换为实际公网访问地址。" +
|
||
"</div>",
|
||
footer: '<button class="btn ghost mf-cancel">取消</button><button class="btn primary mf-save">保存并生效</button>'
|
||
});
|
||
$(".mf-cancel", m.root).onclick = m.close;
|
||
$(".mf-save", m.root).onclick = function () {
|
||
var url = $("#wcb-url", m.root).value.trim();
|
||
if (!url) { Core.toast("请输入回调地址", "err"); return; }
|
||
Core.loading(true, "正在设置回调…");
|
||
Core.api("wx/callback/set", { appId: (state().app && state().app.appId) || "", callbackUrl: url })
|
||
.then(function () { Core.loading(false); m.close(); Core.toast("回调地址已设置并生效", "ok"); })
|
||
.catch(function (e) { Core.loading(false); Core.toast("设置失败:" + e.message, "err"); });
|
||
};
|
||
}).catch(function (e) { Core.loading(false); Core.toast(e.message, "err"); });
|
||
}
|
||
|
||
/* ==================== 导出 ==================== */
|
||
WxN.renderLoginCard = renderLoginCard;
|
||
WxN.getQrCode = getQrCode;
|
||
WxN.checkOnline = checkOnline;
|
||
WxN.checkOnlineLoop = checkOnlineLoop;
|
||
WxN.resetToOffline = resetToOffline;
|
||
WxN.handleOnlineStatus = handleOnlineStatus;
|
||
WxN.doLogout = doLogout;
|
||
WxN.openCallbackDialog = openCallbackDialog;
|
||
WxN.stopLoginPolling = stopLoginPolling;
|
||
|
||
})(window);
|