ai_scheduler/web/assets/js/wx_login.js

348 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
* 微信工作台 — 登录 / 在线检测 / 登出模块
* 依赖: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.style.display=\'none\'">'
: '<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();
WxN.loadProfile().then(function () { WxN.loadConversations(); });
WxN.loadContacts();
} 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);