ai_scheduler/web/assets/js/project.js

391 lines
18 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.

/* ============================================================
* 项目后台 · 主入口
* 共享工具 / 状态 / 路由 / 登录
* 模块文件: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);
}
}