391 lines
18 KiB
JavaScript
391 lines
18 KiB
JavaScript
/* ============================================================
|
||
* 项目后台 · 主入口
|
||
* 共享工具 / 状态 / 路由 / 登录
|
||
* 模块文件: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 = '<span class="dot"></span>' + 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 = '<span class="dot live"></span>#' + state.project.projectId + " " + Core.esc(state.project.name || "") + ' <span style="font-size:10px;opacity:0.7">\u25bc</span>';
|
||
} else {
|
||
badge.className = "badge yellow";
|
||
badge.style.cssText = "cursor:pointer;position:relative";
|
||
badge.innerHTML = '<span class="dot"></span>\u672a\u9009\u62e9\u9879\u76ee <span style="font-size:10px;opacity:0.7">\u25bc</span>';
|
||
}
|
||
// 绑定点击展开下拉
|
||
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 = '<div class="pj-dd-search"><input class="input sm" placeholder="\u641c\u7d22\u9879\u76ee\u2026" autofocus></div>' +
|
||
'<div class="pj-dd-list"><div class="empty" style="padding:16px 0"><span class="spin"></span></div></div>' +
|
||
'<div class="pj-dd-foot"><button class="btn primary sm block" style="width:100%">+ \u65b0\u589e\u9879\u76ee</button></div>';
|
||
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 = '<div class="empty" style="padding:16px 0"><span class="spin"></span></div>';
|
||
Core.api("project/list", { name: kw || "", page: 1, page_size: 50 })
|
||
.then(function (data) {
|
||
var list = (data && data.list) || [];
|
||
if (!list.length) {
|
||
listEl.innerHTML = '<div class="empty" style="padding:16px 0;font-size:12px">' + (kw ? "\u65e0\u5339\u914d\u9879\u76ee" : "\u6682\u65e0\u9879\u76ee") + "</div>";
|
||
return;
|
||
}
|
||
var activeId = state.project && state.project.projectId;
|
||
listEl.innerHTML = list.map(function (p) {
|
||
var isActive = String(p.project_id) === String(activeId);
|
||
return '<div class="pj-dd-item' + (isActive ? " active" : "") + '" data-id="' + p.project_id + '" data-name="' + Core.esc(p.name) + '">' +
|
||
'<span class="mono fs-11" style="color:var(--text-2);margin-right:6px">#' + p.project_id + '</span>' +
|
||
'<span>' + Core.esc(p.name) + '</span>' +
|
||
(isActive ? '<span class="badge green" style="margin-left:auto;font-size:10px;padding:1px 6px">\u5f53\u524d</span>' : '') +
|
||
"</div>";
|
||
}).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 = '<div class="empty" style="padding:16px 0;font-size:12px;color:var(--danger)">' + Core.esc(e.message) + "</div>";
|
||
});
|
||
}
|
||
|
||
/** 全局点击关闭下拉 */
|
||
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 ? '<option value="">' + Core.esc(placeholder) + "</option>" : "";
|
||
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 '<option value="' + s.sup_id + '"' + (String(selected) === String(s.sup_id) ? " selected" : "") + ">#" + s.sup_id + " " + Core.esc(s.sup_name) + "</option>";
|
||
}), "请选择模型配置");
|
||
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 '<option value="' + s.industry_id + '"' + (String(selected) === String(s.industry_id) ? " selected" : "") + ">" + Core.esc(s.name) + "</option>";
|
||
}), "不复制(空模板)");
|
||
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 '<option value="' + a.advicer_id + '"' + (String(selected) === String(a.advicer_id) ? " selected" : "") + ">#" + a.advicer_id + " " + Core.esc(a.name || "") + "</option>";
|
||
}), "请选择销售");
|
||
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 = '<span class="badge green"><span class="dot live"></span>微信工作台 · 销售:' +
|
||
Core.esc(state.wxAdvicer.name || "#" + state.wxAdvicer.advicerId) +
|
||
'</span><span class="spacer"></span><button class="btn ghost sm" id="wxBackToList">返回销售列表</button>';
|
||
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 = '<div class="card"><div class="empty">wx.js 未加载</div></div>';
|
||
}
|
||
}
|
||
|
||
/* ==================== 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);
|
||
}
|
||
}
|