/* ============================================================ * 项目后台 · 主入口 * 共享工具 / 状态 / 路由 / 登录 * 模块文件:project_list.js · project_detail.js · project_analysis.js * project_data.js · project_activity.js · project_drill.js * 依赖:core.js、wx.js * ============================================================ */ var $ = function (sel, root) { return (root || document).querySelector(sel); }; var $$ = function (sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }; var ROLE_META = { advicer: { title: "销售维度", cls: "advicer", desc: "方言特征 / 句式习惯 / 语气性格 / 标志对话" }, project: { title: "项目维度", cls: "project", desc: "区域价值 / 竞品对比 / 核心卖点 / 配套 / 开发商背书" }, skill: { title: "聊天技巧维度", cls: "skill", desc: "需求挖掘 / 痛点应对 / 价值塑造 / 促单 / 沟通节奏" }, client: { title: "客户维度", cls: "client", desc: "客户画像(身份 / 动机 / 需求 / 顾虑 / 决策)" } }; var DIM_NAMES = [ ["desc", "行业/项目介绍"], ["advicer_desc", "销售作用说明"], ["client_dimension", "客户维度 模板"], ["project_dimension", "项目维度 模板"], ["advicer_dimension", "销售维度 模板"], ["talk_skill_dimension", "聊天技巧维度 模板"], ["rule_dimension", "风控维度 模板"] ]; // 维度名称列表(供 AI 生成对话框使用,过滤掉非维度字段) var DIMENSIONS = DIM_NAMES.filter(function (d) { return d[0].indexOf("_dimension") > 0; }).map(function (d) { return d[1].replace(" \u6a21\u677f", ""); }); var state = { view: (function () { try { return localStorage.getItem("as_current_view") || "projects"; } catch (e) { return "projects"; } })(), project: null, projects: { list: [], total: 0, page: 1, kw: "" }, detail: null, analysis: { file: null, result: null, running: false }, data: { tab: "advicers", advicerId: 0 }, activity: { list: [] }, drill: { sessionId: null, msgs: [], mission: "", busy: false }, wxMounted: false, wxAdvicer: null // 当前微信工作台归属销售 {advicerId, name} }; var VIEWS = { projects: { title: "项目管理", sub: "创建项目并复制行业模板,上传聊天记录进行 AI 分析", render: function () { renderProjects(); } }, projectDetail: { title: "项目设置", sub: "编辑基础信息与项目维度模板", render: function () { renderProjectDetail(); }, needProject: true }, analysis: { title: "聊天记录分析", sub: "上传销售与客户的聊天记录,AI 提取四大维度", render: function () { renderAnalysis(); }, needProject: true }, data: { title: "数据中心", sub: "管理销售 / 版本 / 技巧 / 客户", render: function () { renderData(); }, needProject: true }, activity: { title: "活动管理", sub: "发布产品活动,AI 按客户等级(熟客/意向/沉睡/非客户)自动推送邀约", render: function () { renderActivity(); }, needProject: true }, smart: { title: "智能策略", sub: "四级回复策略 / 熟客兜底 / 画像迭代 / 主动触达", render: function () { renderSmart(); }, needProject: true }, drill: { title: "AI 演练", sub: "模拟销售与客户对话,验证话术效果", render: function () { renderDrill(); }, needProject: true }, modelsup: { title: "模型配置", sub: "查看与修改当前项目的 AI 模型配置", render: function () { renderModelsup(); }, needProject: true }, wx: { title: "微信工作台", sub: "扫码登录微信 · 通讯录 · 聊天发送 · AI 话术建议", render: function () { renderWx(); }, needProject: true } }; /* ==================== 启动 ==================== */ document.addEventListener("DOMContentLoaded", function () { Core.onAuthFail = function () { Core.toast("登录已过期,请重新登录", "err"); showLogin(); }; $("#loginBtn").onclick = doLogin; $("#loginPwd").addEventListener("keydown", function (e) { if (e.key === "Enter") doLogin(); }); $("#logoutBtn").onclick = function () { Core.logout(); }; $$(".nav-item").forEach(function (item) { item.onclick = function () { if (item.dataset.href) { location.href = item.dataset.href; return; } var v = item.dataset.view; if (VIEWS[v].needProject && (!state.project || !state.project.projectId)) { Core.toast("请先在「项目管理」中进入一个项目", "err"); switchView("projects"); return; } // 数据中心快捷入口:设置 tab 后跳转 if (item.dataset.tab && v === "data") { state.data.tab = item.dataset.tab; } switchView(v); }; }); state.project = Core.store(Core.KEYS.project); console.log("[init] savedView=" + state.view + ", project=" + (state.project ? state.project.projectId : "null") + ", token=" + !!Core.store(Core.KEYS.token) + ", wxApp=" + JSON.stringify(Core.store(Core.KEYS.wxApp))); // 恢复视图:如果视图需要项目但没有项目,回退到 projectDetail var savedView = state.view; if (VIEWS[savedView] && VIEWS[savedView].needProject && (!state.project || !state.project.projectId)) { state.view = "projectDetail"; } // 「聊天记录分析」已从侧边栏隐藏:历史残留视图回退到数据中心 if (state.view === "analysis") state.view = "data"; refreshProjectBadge(); updateNavVisibility(); if (Core.store(Core.KEYS.token)) showApp(); else showLogin(); }); function showLogin() { $("#appView").classList.add("hidden"); $("#loginView").classList.remove("hidden"); } function showApp() { $("#loginView").classList.add("hidden"); $("#appView").classList.remove("hidden"); var admin = Core.store(Core.KEYS.admin) || {}; $("#sideAdmin").textContent = admin.name || "-"; $("#adminBadge").innerHTML = '' + Core.esc(admin.name || ""); switchView(state.view); } function doLogin() { var account = $("#loginAccount").value.trim(); var pwd = $("#loginPwd").value; if (!account || !pwd) { Core.toast("请输入账号和密码", "err"); return; } Core.loading(true, "正在登录…"); Core.api("login", { account: account, pwd: pwd }) .then(function (data) { Core.loading(false); if (!data || !data.token) { Core.toast("登录失败:响应异常", "err"); return; } Core.store(Core.KEYS.token, data.token); Core.store(Core.KEYS.admin, { admin_id: data.admin_id, name: data.name, account: data.account, type: data.type }); Core.toast("欢迎回来," + (data.name || account), "ok"); showApp(); }) .catch(function (e) { Core.loading(false); Core.toast(e.message, "err"); }); } function switchView(view) { console.log("[switchView] -> " + view + ", project=" + (state.project ? state.project.projectId : "null")); if (state.view === "wx" && view !== "wx" && window.Wx) { try { Wx.unmount(); } catch (ex) { } // 离开微信工作台时清除 URL hash if (location.hash) history.replaceState(null, '', location.pathname + location.search); } state.view = view; try { localStorage.setItem("as_current_view", view); } catch (e) { } var conf = VIEWS[view]; if (!conf) { view = "projects"; conf = VIEWS[view]; state.view = view; } // 数据中心根据当前 tab 动态显示标题 var title = conf.title; var sub = conf.sub; if (view === "data") { var tabMeta = { advicers: "销售管理", skills: "聊天技巧", clients: "客户管理", versions: "数字人管理" }; title = tabMeta[state.data.tab] || conf.title; sub = "管理当前项目下的" + (tabMeta[state.data.tab] || "数据"); } var pageTitle = $("#pageTitle"); var pageSub = $("#pageSub"); if (pageTitle) pageTitle.textContent = title; if (pageSub) pageSub.textContent = sub; // 高亮侧边栏 $$(".nav-item").forEach(function (i) { var match = i.dataset.view === view; if (match && i.dataset.tab && state.data.tab !== i.dataset.tab) match = false; if (match && view === "data" && !i.dataset.tab) match = false; i.classList.toggle("active", match); }); // 切换视图可见性 $$(".view").forEach(function (v) { v.classList.add("hidden"); }); var viewEl = $("#view-" + view); if (viewEl) viewEl.classList.remove("hidden"); // 渲染视图内容(保护性调用) try { conf.render(); } catch (e) { console.error("render error [" + view + "]:", e); } } function refreshProjectBadge() { var badge = $("#projectBadge"); if (!badge) return; if (state.project && state.project.projectId) { badge.className = "badge green"; badge.style.cssText = "cursor:pointer;position:relative"; badge.innerHTML = '#' + state.project.projectId + " " + Core.esc(state.project.name || "") + ' \u25bc'; } else { badge.className = "badge yellow"; badge.style.cssText = "cursor:pointer;position:relative"; badge.innerHTML = '\u672a\u9009\u62e9\u9879\u76ee \u25bc'; } // 绑定点击展开下拉 badge.onclick = function (e) { e.stopPropagation(); var existing = $(".pj-dropdown", badge); if (existing) { existing.remove(); return; } // 关闭其他已打开的下拉 var old = $(".pj-dropdown"); if (old) old.remove(); var dd = document.createElement("div"); dd.className = "pj-dropdown"; dd.innerHTML = '' + '
' + '
'; badge.appendChild(dd); // 加载项目列表 loadProjectDropdown(dd); // 搜索 var searchInput = $(".pj-dd-search input", dd); searchInput.oninput = Core.debounce(function () { loadProjectDropdown(dd, searchInput.value.trim()); }, 300); searchInput.onclick = function (e) { e.stopPropagation(); }; // 新增项目 $(".pj-dd-foot button", dd).onclick = function (e) { e.stopPropagation(); dd.remove(); projectCreateDialog(); }; }; } /** 加载项目下拉列表 */ function loadProjectDropdown(dd, kw) { var listEl = $(".pj-dd-list", dd); if (!listEl) return; listEl.innerHTML = '
'; Core.api("project/list", { name: kw || "", page: 1, page_size: 50 }) .then(function (data) { var list = (data && data.list) || []; if (!list.length) { listEl.innerHTML = '
' + (kw ? "\u65e0\u5339\u914d\u9879\u76ee" : "\u6682\u65e0\u9879\u76ee") + "
"; return; } var activeId = state.project && state.project.projectId; listEl.innerHTML = list.map(function (p) { var isActive = String(p.project_id) === String(activeId); return '
' + '#' + p.project_id + '' + '' + Core.esc(p.name) + '' + (isActive ? '\u5f53\u524d' : '') + "
"; }).join(""); // 绑定点击选择 $$(".pj-dd-item", listEl).forEach(function (item) { item.onclick = function (e) { e.stopPropagation(); var pid = Number(item.dataset.id); var pname = item.dataset.name; setActiveProject({ projectId: pid, name: pname }); dd.remove(); Core.toast("\u5df2\u5207\u6362\u9879\u76ee\uff1a" + pname, "ok"); // 刷新当前视图 var conf = VIEWS[state.view]; if (conf) try { conf.render(); } catch (ex) { } }; }); }) .catch(function (e) { listEl.innerHTML = '
' + Core.esc(e.message) + "
"; }); } /** 全局点击关闭下拉 */ document.addEventListener("click", function () { var dd = $(".pj-dropdown"); if (dd) dd.remove(); }); function setActiveProject(p) { var pid = p.project_id || p.projectId; var pname = p.name || ""; state.project = { projectId: pid, name: pname }; Core.store(Core.KEYS.project, state.project); refreshProjectBadge(); updateNavVisibility(); } function updateNavVisibility() { var hasProject = state.project && state.project.projectId; $$(".nav-project-only").forEach(function (el) { if (hasProject) el.classList.remove("hidden"); else el.classList.add("hidden"); }); } /* ==================== 公共选项加载 ==================== */ function fillSelect(sel, options, placeholder) { var html = placeholder ? '" : ""; html += options.join(""); sel.innerHTML = html; } function loadModelsupOptions(sel, selected) { return Core.api("modelsup/list", { page: 1, page_size: 50 }).then(function (data) { var list = (data && data.list) || []; fillSelect(sel, list.map(function (s) { return '"; }), "请选择模型配置"); return list; }); } function loadIndustryOptions(sel, selected) { return Core.api("industry/list", { page: 1, page_size: 50 }).then(function (data) { var list = (data && data.list) || []; fillSelect(sel, list.map(function (s) { return '"; }), "不复制(空模板)"); return list; }); } function loadAdvicerOptions(sel, selected) { return Core.api("advicer/list", { projectId: state.project.projectId }).then(function (list) { list = list || []; fillSelect(sel, list.map(function (a) { return '"; }), "请选择销售"); return list; }); } /* ==================== 微信工作台 ==================== */ function renderWx() { console.log("[renderWx] called, wxAdvicer=" + JSON.stringify(state.wxAdvicer) + ", Wx=" + !!window.Wx); var el = $("#view-wx"); // 先卸载旧的 wx 模块实例(清理 WebSocket / 定时器等) if (window.Wx) { try { Wx.unmount(); } catch (ex) { } } el.innerHTML = ""; // 刷新场景:state.wxAdvicer 丢失,从 URL hash 恢复 advicerId if (!state.wxAdvicer) { var hashMatch = (location.hash || '').match(/wx-advicer=(\d+)/); if (hashMatch) { state.wxAdvicer = { advicerId: Number(hashMatch[1]), name: '' }; } } // 显示当前归属销售提示 if (state.wxAdvicer) { var hint = document.createElement("div"); hint.className = "card"; hint.style.cssText = "padding:8px 14px;margin-bottom:8px;display:flex;align-items:center;gap:8px;font-size:12px"; hint.innerHTML = '微信工作台 · 销售:' + Core.esc(state.wxAdvicer.name || "#" + state.wxAdvicer.advicerId) + ''; el.appendChild(hint); var backBtn = hint.querySelector("#wxBackToList"); if (backBtn) backBtn.onclick = function () { state.data.tab = "advicers"; switchView("data"); }; } var wxContainer = document.createElement("div"); wxContainer.id = "wx-mount-point"; el.appendChild(wxContainer); if (window.Wx) { Wx.mount(wxContainer); state.wxMounted = true; } else { wxContainer.innerHTML = '
wx.js 未加载
'; } } /* ==================== Flatpickr 日历选择器 ==================== */ /** * 初始化 Flatpickr 日历选择器 * @param {HTMLElement|string} el - 目标 input 元素或 CSS 选择器 * @param {object} opts - Flatpickr 配置项 * @returns {object|null} Flatpickr 实例 */ function initDatePicker(el, opts) { if (typeof flatpickr === "undefined") return null; var input = typeof el === "string" ? $(el) : el; if (!input) return null; var defaults = { locale: "zh", allowInput: true, dateFormat: "Y-m-d", disableMobile: true }; // 合并配置 var cfg = {}; for (var k in defaults) cfg[k] = defaults[k]; if (opts) for (var k in opts) cfg[k] = opts[k]; // 如果是 datetime-local 类型或 dp-datetime class,自动调整格式 if (input.type === "datetime-local" || input.classList.contains("dp-datetime") || cfg.enableTime) { cfg.enableTime = true; cfg.dateFormat = cfg.dateFormat || "Y-m-d H:i"; cfg.time_24hr = true; } return flatpickr(input, cfg); } /** * 批量初始化容器内所有日期/时间输入框 * @param {HTMLElement|string} root - 容器元素或 CSS 选择器 */ function initAllDatePickers(root) { if (typeof flatpickr === "undefined") return; var container = typeof root === "string" ? $(root) : root; if (!container) return; // 支持 class="dp-date" 和 class="dp-datetime" 以及原生 type var inputs = container.querySelectorAll('.dp-date, .dp-datetime, input[type="date"], input[type="datetime-local"]'); for (var i = 0; i < inputs.length; i++) { var isDateTime = inputs[i].classList.contains("dp-datetime") || inputs[i].type === "datetime-local"; initDatePicker(inputs[i], isDateTime ? { enableTime: true, time_24hr: true, dateFormat: "Y-m-d H:i" } : null); } }