/* ============================================================ * AI Scheduler 前端核心库(零依赖,全局命名空间 Core) * - 统一 API 调用(管理后台 JWT / 微信代理 X-finder-TOKEN) * - 本地存储(登录态、微信 Token) * - 通用 UI:toast / modal / confirm / loading / 上传 * ============================================================ */ (function (global) { "use strict"; var ADMIN_BASE = "/api/v1/admin/advice/admin/"; var WX_BASE = ADMIN_BASE + "wx/"; var UPLOAD_URL = "/api/v1/admin/advice/admin/file/upload"; var KEYS = { token: "as_admin_token", admin: "as_admin_info", wxToken: "as_wx_finder_token", wxApp: "as_wx_app_info", wxBrief: "as_wx_brief_cache", project: "as_active_project" }; /* ---------------- 本地存储 ---------------- */ function store(key, val) { if (val === undefined) { try { return JSON.parse(localStorage.getItem(key)); } catch (e) { return null; } } if (val === null) { localStorage.removeItem(key); return; } localStorage.setItem(key, JSON.stringify(val)); } /* ---------------- 弹窗基础设施 ---------------- */ function el(html) { var d = document.createElement("div"); d.innerHTML = html.trim(); return d.firstElementChild; } function esc(s) { if (s === undefined || s === null) return ""; return String(s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); } var toastWrap = null; function toast(msg, type) { if (!toastWrap) { toastWrap = el('
'); document.body.appendChild(toastWrap); } var icons = { ok: "✓", err: "✕", info: "ℹ" }; var t = el( '
' + '' + (icons[type] || icons.info) + "" + "" + esc(msg) + "
" ); toastWrap.appendChild(t); setTimeout(function () { t.style.transition = "opacity .3s ease, transform .3s ease"; t.style.opacity = "0"; t.style.transform = "translateX(12px)"; setTimeout(function () { t.remove(); }, 320); }, type === "err" ? 4200 : 2600); } var loadingMask = null; function loading(show, text) { if (show) { if (loadingMask) { loadingMask.querySelector(".lm-text").textContent = text || "处理中…"; return; } loadingMask = el( '
' + esc(text || "处理中…") + "
" ); document.body.appendChild(loadingMask); } else if (loadingMask) { loadingMask.remove(); loadingMask = null; } } /** * modal({ title, body, footer, width(''|'wide'|'xwide'), onMount(el) }) * 返回 { close, root } */ function modal(opts) { opts = opts || {}; var widthCls = opts.width ? " " + opts.width : ""; var mask = el( '" ); document.body.appendChild(mask); var api = { root: mask.querySelector(".modal"), body: mask.querySelector(".modal-body"), close: function () { mask.remove(); } }; mask.querySelector(".x").onclick = api.close; mask.addEventListener("mousedown", function (e) { if (e.target === mask) { // 不关闭:日期/时间选择器弹出层交互时 document.activeElement 仍为 date input var ae = document.activeElement; if (ae && (ae.type === "date" || ae.type === "datetime-local")) return; api.close(); } }); if (opts.onMount) opts.onMount(api); return api; } function confirm(msg, title) { return new Promise(function (resolve) { var m = modal({ title: title || "操作确认", body: '
' + esc(msg) + "
", footer: '' }); m.root.querySelector(".cm-no").onclick = function () { m.close(); resolve(false); }; m.root.querySelector(".cm-yes").onclick = function () { m.close(); resolve(true); }; m.root.closest(".modal-mask").querySelector(".x").onclick = function () { m.close(); resolve(false); }; }); } /* ---------------- 请求封装 ---------------- */ function buildError(msg, code) { var e = new Error(msg || "请求失败"); e.code = code; return e; } function parseBody(res) { return res.text().then(function (txt) { if (!txt) return {}; try { return JSON.parse(txt); } catch (e) { return { message: txt }; } }); } /** 管理后台接口:POST JSON,自动携带 JWT,解包 {code,message,data} */ function api(path, body) { var headers = { "Content-Type": "application/json" }; var token = store(KEYS.token); if (token) headers["Authorization"] = "Bearer " + token; return fetch(ADMIN_BASE + path, { method: "POST", headers: headers, body: JSON.stringify(body || {}) }).then(function (res) { return parseBody(res).then(function (json) { // 未登录/鉴权失败 if (res.status === 401 || (json && (json.code === 401 || json.code === 1004))) { store(KEYS.token, null); store(KEYS.admin, null); if (typeof Core.onAuthFail === "function") Core.onAuthFail(); throw buildError(json.message || "登录已过期,请重新登录", 401); } if (json && typeof json.code === "number" && json.code !== 0 && json.code !== 200) { throw buildError(json.message || "请求失败", json.code); } return json ? json.data : null; }); }); } /** * 微信代理接口(包装转发,标准 {code,message,data} 格式) * wxApi('login/getLoginQrCode', {appId:''}) → data 解包后返回 */ function wxApi(path, body, tokenOverride) { var payload = {}; for (var k in (body || {})) payload[k] = body[k]; // 自动注入 projectId(后端用于查询项目级 wx_token) if (!payload.projectId) { var proj = store(KEYS.project); if (proj && proj.projectId) payload.projectId = proj.projectId; } var headers = { "Content-Type": "application/json" }; var token = store(KEYS.token); if (token) headers["Authorization"] = "Bearer " + token; return fetch(WX_BASE + path, { method: "POST", headers: headers, body: JSON.stringify(payload) }).then(function (res) { return parseBody(res).then(function (json) { if (res.status === 401 || (json && (json.code === 401 || json.code === 1004))) { store(KEYS.token, null); store(KEYS.admin, null); if (typeof Core.onAuthFail === "function") Core.onAuthFail(); throw buildError(json.message || "登录已过期,请重新登录", 401); } if (json && typeof json.code === "number" && json.code !== 0 && json.code !== 200) { throw buildError(json.message || "请求失败", json.code); } return json ? json.data : null; }); }); } /** 上传文件(multipart)→ {path,url,name,size} */ function upload(file, onProgress) { var fd = new FormData(); fd.append("file", file); var token = store(KEYS.token); return fetch(UPLOAD_URL, { method: "POST", headers: token ? { "Authorization": "Bearer " + token } : {}, body: fd }).then(function (res) { return parseBody(res).then(function (json) { if (json && typeof json.code === "number" && json.code !== 0 && json.code !== 200) { throw buildError(json.message || "上传失败", json.code); } return json ? json.data : null; }); }); } /* ---------------- 工具函数 ---------------- */ function fmtTime(t, withSec) { if (!t) return "-"; var d = new Date(t); if (isNaN(d.getTime())) { // Go 时间字符串如 2026-09-22T10:00:00+08:00 兼容 d = new Date(String(t).replace(/-/g, "/").replace("T", " ").split("+")[0]); if (isNaN(d.getTime())) return String(t); } var p = function (n) { return n < 10 ? "0" + n : "" + n; }; var s = d.getFullYear() + "-" + p(d.getMonth() + 1) + "-" + p(d.getDate()) + " " + p(d.getHours()) + ":" + p(d.getMinutes()); if (withSec) s += ":" + p(d.getSeconds()); return s; } function nowTime() { return fmtTime(new Date(), true); } function fmtSize(b) { if (!b && b !== 0) return "-"; if (b < 1024) return b + " B"; if (b < 1048576) return (b / 1024).toFixed(1) + " KB"; return (b / 1048576).toFixed(2) + " MB"; } function debounce(fn, ms) { var t; return function () { var args = arguments, self = this; clearTimeout(t); t = setTimeout(function () { fn.apply(self, args); }, ms || 300); }; } function copyText(text) { if (navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard.writeText(text).then(function () { toast("已复制到剪贴板", "ok"); }); } var ta = document.createElement("textarea"); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); ta.remove(); toast("已复制到剪贴板", "ok"); return Promise.resolve(); } function qs(name) { var m = new URLSearchParams(location.search); return m.get(name); } /** JSON 美化输出(解析失败返回原文) */ function prettyJson(v) { try { if (typeof v === "string") return JSON.stringify(JSON.parse(v), null, 2); return JSON.stringify(v, null, 2); } catch (e) { return typeof v === "string" ? v : JSON.stringify(v); } } /** 安全解析 JSON 字符串(失败抛出带说明错误) */ function parseJsonField(text, label) { if (!text || !String(text).trim()) return null; try { return JSON.parse(text); } catch (e) { throw buildError((label || "JSON") + " 格式错误:" + e.message); } } /** 首字符头像(中文取首字) */ function initial(name) { if (!name) return "?"; return String(name).trim().charAt(0).toUpperCase(); } function logout() { store(KEYS.token, null); store(KEYS.admin, null); location.reload(); } /* ---------------- 多气泡工具 ---------------- */ // splitBubbles 将文本按换行拆分为多条消息(模拟真人连发多条短消息) function splitBubbles(text) { if (!text) return []; return String(text).split(/\r?\n+/).map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; }); } // sleep 异步等待(发送节奏控制) function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); } /* ---------------- 导出 ---------------- */ var Core = { ADMIN_BASE: ADMIN_BASE, WX_BASE: WX_BASE, KEYS: KEYS, store: store, el: el, esc: esc, toast: toast, loading: loading, modal: modal, confirm: confirm, api: api, wxApi: wxApi, upload: upload, fmtTime: fmtTime, nowTime: nowTime, fmtSize: fmtSize, debounce: debounce, splitBubbles: splitBubbles, sleep: sleep, copyText: copyText, qs: qs, prettyJson: prettyJson, parseJsonField: parseJsonField, initial: initial, logout: logout, onAuthFail: null }; global.Core = Core; })(window);