/* ==================== 视图四:数据中心 ==================== */ var DATA_TABS = [ ["advicers", "\u9500\u552e\u4eba\u5458"], ["versions", "\u6570\u5b57\u4eba\u7ba1\u7406"], ["skills", "\u804a\u5929\u6280\u5de7"], ["clients", "客户"], ["leads", "成交线索"], ["reminds", "定时提醒"] ]; function renderData() { if (!state.project || !state.project.projectId) { switchView("projects"); return; } var st = state.data; $("#view-data").innerHTML = '
' + '
' + DATA_TABS.map(function (t) { return '
' + t[1] + "
"; }).join("") + "
" + '
' + "
"; $$("#data-tabs .tab").forEach(function (t) { t.onclick = function () { st.tab = t.dataset.tab; $$("#data-tabs .tab").forEach(function (x) { x.classList.toggle("active", x === t); }); // 同步侧边栏高亮 if (typeof switchView === "function") { $$(".nav-item").forEach(function (i) { var match = i.dataset.view === "data" && i.dataset.tab === st.tab; i.classList.toggle("active", match); }); // 更新顶栏标题 var tabMeta = { advicers: "销售管理", versions: "数字人管理", skills: "聊天技巧", clients: "客户管理", leads: "成交线索", reminds: "定时提醒" }; $("#pageTitle").textContent = tabMeta[st.tab] || "数据中心"; $("#pageSub").textContent = "管理当前项目下的" + (tabMeta[st.tab] || "数据"); } renderDataBody(); }; }); renderDataBody(); } function renderDataBody() { console.log("[renderDataBody] tab=" + state.data.tab); var body = $("#data-body"); if (!body) { console.error("[renderDataBody] #data-body not found!"); return; } body.innerHTML = '
'; var tab = state.data.tab; if (tab === "advicers") return renderTabAdvicers(body); if (tab === "versions") return renderTabVersions(body); if (tab === "skills") return renderTabSkills(body); if (tab === "clients") return renderTabClients(body); if (tab === "leads") return renderTabLeads(body); if (tab === "reminds") return renderTabReminds(body); console.warn("[renderDataBody] unknown tab: " + tab); } function dataHead(title, sub, addBtnId, addBtnText) { return '
' + title + '' + '' + sub + '' + '
"; } /* ---------- Tab 1:销售人员 ---------- */ function renderTabAdvicers(body) { Core.api("advicer/list", { projectId: state.project.projectId }).then(function (list) { list = list || []; var html = dataHead("销售人员", "共 " + list.length + " 人", "adv-add", "新增销售"); if (!list.length) { html += '
◉
暂无销售人员,点击右上角新增
'; } else { html += '
' + "" + list.map(function (a) { var gender = a.gender === 1 ? "男" : a.gender === 2 ? "女" : "-"; var hOn = a.hosting_enabled === 1 || a.hosting_enabled === true; return "" + '" + '" + '" + '" + "" + "" + '" + '' + '' + '" + '' + ""; }).join("") + "
ID姓名生日入职时间性别行业经验微信设备ID托管文件助手创建时间操作
' + a.advicer_id + "' + Core.esc(a.name) + "' + Core.fmtTime(a.birth).slice(0, 10) + "' + (a.join_date ? Core.fmtTime(a.join_date).slice(0, 10) : "-") + "" + gender + "" + (a.working_years || "-") + " 年' + Core.esc(a.wx_device_id || '-') + "' + Core.fmtTime(a.create_at) + "' + '' + '
"; } body.innerHTML = html; $("#adv-add").onclick = function () { advicerForm(null); }; $$(".adv-edit").forEach(function (b) { b.onclick = function () { var item = list.filter(function (x) { return String(x.advicer_id) === b.dataset.id; })[0]; advicerForm(item); }; }); $$(".adv-avatar").forEach(function (b) { b.onclick = function () { state.data.advicerId = Number(b.dataset.id); state.data.tab = "versions"; switchView("data"); }; }); $$(".adv-wx").forEach(function (b) { b.onclick = function () { openAdvicerWxWorkbench(Number(b.dataset.id), b.dataset.name, b.dataset.device); }; }); $$(".adv-hosting").forEach(function (b) { b.onchange = function () { var advicerId = Number(b.dataset.id); var advicerName = b.dataset.name || ""; if (!b.checked) { // 关闭托管,直接生效不弹窗 Core.api("advicer/hosting/update", { advicerId: advicerId, hostingEnabled: 0, hostingVersionId: b.dataset.ver || "", hostingSkillId: b.dataset.skill || "" }).then(function () { Core.toast("托管已关闭", "ok"); renderDataBody(); }).catch(function (e) { Core.toast(e.message, "err"); b.checked = true; // 失败时回弹 }); return; } // 开启托管,弹窗配置版本和技巧 hostingConfigDialog( advicerId, advicerName, b.dataset.ver || "", b.dataset.skill || "" ); }; }); // 文件助手开关:独立切换,立即生效 $$(".adv-filehelper").forEach(function (b) { b.onchange = function () { var advicerId = Number(b.dataset.id); Core.api("advicer/hosting/update", { advicerId: advicerId, replyFilehelper: b.checked ? 1 : 0 }).then(function () { Core.toast(b.checked ? "文件助手回复已开启" : "文件助手回复已关闭", "ok"); }).catch(function (e) { Core.toast(e.message, "err"); b.checked = !b.checked; }); }; }); }).catch(function (e) { body.innerHTML = '
⚠
' + Core.esc(e.message) + "
"; }); } function advicerForm(item) { var d = item || {}; var isEdit = !!d.advicer_id; var birth = d.birth ? Core.fmtTime(d.birth).slice(0, 10) : ""; var joinDate = d.join_date ? Core.fmtTime(d.join_date).slice(0, 10) : ""; var m = Core.modal({ title: isEdit ? "编辑销售 #" + d.advicer_id : "新增销售", body: '
' + '
' + '
' + "
" + '
' + '
' + '
' + "
" + '
' + '
' + '
' + '
' + '
销售是「版本 / 技巧 / 客户 / AI 会话」的归属主体,生日将用于拟人化对话。
', footer: '' }); $(".mf-cancel", m.root).onclick = m.close; initAllDatePickers(m.root); $(".mf-save", m.root).onclick = function () { var payload = { advicerId: d.advicer_id || 0, projectId: state.project.projectId, name: $("#adv-name", m.root).value.trim(), birth: $("#adv-birth", m.root).value, gender: Number($("#adv-gender", m.root).value), workingYears: Number($("#adv-years", m.root).value) || 0, joinDate: $("#adv-joindate", m.root).value }; if (!payload.name) { Core.toast("请填写姓名", "err"); return; } if (!payload.birth) { Core.toast("请选择生日", "err"); return; } Core.api(isEdit ? "advicer/update" : "advicer/add", payload) .then(function () { // 如果是编辑且有 wx_device_id,单独保存 var wxDeviceId = $("#adv-wxdevice", m.root).value.trim(); if (isEdit && d.advicer_id) { return Core.api("advicer/wx_device/update", { advicerId: d.advicer_id, wxDeviceId: wxDeviceId }) .then(function () { m.close(); Core.toast("保存成功", "ok"); renderDataBody(); }); } m.close(); Core.toast("保存成功", "ok"); renderDataBody(); }) .catch(function (e) { Core.toast(e.message, "err"); }); }; } /** 托管配置弹窗(开启时配置版本和技巧,弹窗内不含开关) */ function hostingConfigDialog(advicerId, advicerName, versionId, skillId) { var projectId = state.project.projectId; var m = Core.modal({ title: "\u2699 托管配置 \u00b7 销售 #" + advicerId, body: '
开启托管后,AI 将自动使用该销售配置的数字人版本和聊天技巧回复客户消息。
不选择具体版本时,默认使用最新创建的一条。
' + '
' + '' + '
留空 = 每次自动使用最新版本
' + '
' + '' + '
留空 = 每次自动使用最新一条
', footer: '' }); $(".mf-cancel", m.root).onclick = function () { m.close(); renderDataBody(); }; // 加载数字人版本列表 Core.api("advicer/version/list", { advicerId: advicerId }).then(function (list) { list = list || []; var sel = $("#hc-version", m.root); if (!list.length) { sel.innerHTML = ''; sel.disabled = true; } else { sel.innerHTML = '' + list.map(function (v) { var selected = (v.id === versionId) ? ' selected' : ''; return ''; }).join(""); } }).catch(function () {}); // 加载聊天技巧列表 Core.api("skill/list", { projectId: projectId }).then(function (list) { list = list || []; var sel = $("#hc-skill", m.root); if (!list.length) { sel.innerHTML = ''; sel.disabled = true; } else { sel.innerHTML = '' + list.map(function (s) { var selected = (s.id === skillId) ? ' selected' : ''; return ''; }).join(""); } }).catch(function () {}); // 确认开启 $(".mf-save", m.root).onclick = function () { var newVer = $("#hc-version", m.root).value; var newSkill = $("#hc-skill", m.root).value; // 检查微信设备绑定和登录状态 Core.api("advicer/info", { advicerId: advicerId }).then(function (info) { if (!info || !info.wx_device_id) { m.close(); hostingWarnDialog("该销售尚未绑定微信设备,请先绑定后再开启托管", advicerId, advicerName); return; } Core.wxApi("login/checkOnline", { appId: info.wx_device_id }).then(function (online) { if (online !== true) { m.close(); hostingWarnDialog("当前微信设备已下线,请重新扫码登录后再开启托管", advicerId, advicerName); return; } doSaveHosting(advicerId, 1, newVer, newSkill, m); }).catch(function () { m.close(); hostingWarnDialog("当前微信设备已下线,请重新扫码登录后再开启托管", advicerId, advicerName); }); }).catch(function (e) { Core.toast(e.message, "err"); }); }; } /** 托管警告弹窗(未绑定设备 / 设备已下线),带「取消」和「去绑定」按钮 */ function hostingWarnDialog(msg, advicerId, advicerName) { var m = Core.modal({ title: '\u26a0 托管提醒', body: '
' + '
' + '\u26a0' + '
' + Core.esc(msg) + '
' + '
', footer: '' }); $('.hw-cancel', m.root).onclick = m.close; $('.hw-go', m.root).onclick = function () { m.close(); openAdvicerWxWorkbench(advicerId, advicerName, ''); }; } /** 保存托管配置 */ function doSaveHosting(advicerId, enabled, versionId, skillId, modal) { Core.api("advicer/hosting/update", { advicerId: advicerId, hostingEnabled: enabled, hostingVersionId: versionId, hostingSkillId: skillId }).then(function () { modal.close(); Core.toast("托管配置已保存", "ok"); renderDataBody(); }).catch(function (e) { Core.toast(e.message, "err"); }); } /* ---------- Tab 2:销售版本(可视化编辑器,与项目资料格式一致) ---------- */ /* 版本维度预设(名称→推荐类型) */ var VER_PRESET_DIMS = { "方言/口音特征": "object", "词汇偏好": "object", "句式模式": "object", "标点与排版习惯": "object", "个性标签": "array", "语气标签": "object", "标志性对话": "list" }; var VER_TYPE_LABELS = { string: "描述", object: "详细信息", array: "多条目", list: "列表" }; function renderTabVersions(body) { var advicerInfo = state.data.advicerId ? ' · 销售 #' + state.data.advicerId : ''; body.innerHTML = dataHead("数字人管理" + advicerInfo, "基于聊天分析沉淀的销售个人风格(方言/句式/语气/对话)", "ver-add", "新增版本") + '
'; if (!state.data.advicerId) { loadAdvicerOptions($("' + '
' + "" + '
' + '' + '
' + '
', footer: '' }); loadAdvicerOptions($("#vf-advicer", m.root), d.advicerId || state.data.advicerId); $(".mf-cancel", m.root).onclick = m.close; // 渲染已有维度 var vfDims = $("#vf-dims", m.root); var vfItems = []; Object.keys(dimData).forEach(function (key) { var val = dimData[key]; var type = inferSectionType(val); var content = parseSectionContent(val, type); vfItems.push({ key: key, type: type, content: content }); }); drawVfDims(vfDims, vfItems); // 新增维度 $("#vf-add-dim", m.root).onclick = function () { addVfDimDialog(vfDims, vfItems); }; // AI 生成 $("#vf-ai-gen", m.root).onclick = function () { var advicerId = Number($("#vf-advicer", m.root).value) || d.advicerId || state.data.advicerId; if (!advicerId) { Core.toast("请先选择销售", "err"); return; } versionAiGenerateDialog(advicerId, m, vfDims, vfItems); }; // 保存 $(".mf-save", m.root).onclick = function () { var advicerId = Number($("#vf-advicer", m.root).value); if (!advicerId) { Core.toast("请选择销售", "err"); return; } var dataObj = collectVfDims(vfDims); var descVal = $("#vf-desc", m.root).value.trim(); if (!descVal) { var now = new Date(); var pad = function(n) { return n < 10 ? "0" + n : "" + n; }; descVal = now.getFullYear() + pad(now.getMonth() + 1) + pad(now.getDate()) + pad(now.getHours()) + pad(now.getMinutes()) + pad(now.getSeconds()); } var payload = { advicerId: advicerId, versionDesc: descVal, data: dataObj }; if (isEdit) payload.id = d.id; Core.api(isEdit ? "advicer/version/update" : "advicer/version/add", payload) .then(function () { m.close(); Core.toast("保存成功", "ok"); loadVersions(); }) .catch(function (e) { Core.toast(e.message, "err"); }); }; } /** 渲染版本维度卡片列表 */ function drawVfDims(container, items) { container.innerHTML = ''; items.forEach(function (item, idx) { var card = document.createElement('div'); card.className = 'pji-section-card'; card.dataset.idx = idx; card.dataset.key = item.key; card.dataset.type = item.type; var typeLabel = VER_TYPE_LABELS[item.type] || item.type; card.innerHTML = '
' + '' + Core.esc(item.key) + '' + '' + typeLabel + '' + '' + '
' + '
' + sectionContentHtml(item.type, item.content) + '
'; container.appendChild(card); bindVfDimEvents(card, item); }); } /** 绑定单个维度卡片的事件 */ function bindVfDimEvents(card, item) { // 删除 var delBtn = $('.vf-dim-del', card); if (delBtn) { delBtn.onclick = function () { card.remove(); }; } // KV 编辑器事件 bindKvEvents(card); // 列表编辑器事件 bindListEvents(card); // 多条目编辑器事件 bindArrayEvents(card); } /** 绑定 KV 编辑器(object 类型) */ function bindKvEvents(card) { $$('.dr-kv-add', card).forEach(function (btn) { btn.onclick = function () { var editor = btn.closest('.dr-kv-editor'); if (!editor) return; var row = document.createElement('div'); row.className = 'dr-kv-row'; row.innerHTML = kvRowHtml('', ''); editor.insertBefore(row, btn); bindKvRowEvents(row); }; }); $$('.dr-kv-row', card).forEach(function (row) { bindKvRowEvents(row); }); } function bindKvRowEvents(row) { var del = $('.dr-kv-del', row); if (del) { del.onclick = function () { row.remove(); }; } } /** 绑定多条目编辑器(array 类型) */ function bindArrayEvents(card) { var addBtn = $('.dr-example-add', card); if (addBtn) { addBtn.onclick = function () { var multi = addBtn.closest('.dr-example-multi'); if (!multi) return; var idx = $$('.dr-example-row', multi).length; var row = document.createElement('div'); row.className = 'dr-example-row'; row.innerHTML = '' + ''; multi.insertBefore(row, addBtn); $('.dr-example-del', row).onclick = function () { row.remove(); }; }; } $$('.dr-example-del', card).forEach(function (btn) { btn.onclick = function () { btn.closest('.dr-example-row').remove(); }; }); } /** 绑定列表编辑器(list 类型) */ function bindListEvents(card) { // 添加条目 var addEntry = $('.dr-list-entry-add', card); if (addEntry) { addEntry.onclick = function () { var editor = addEntry.closest('.dr-list-editor'); if (!editor) return; var idx = $$('.dr-list-entry', editor).length; var entry = document.createElement('div'); entry.className = 'dr-list-entry'; entry.dataset.entry = idx; entry.innerHTML = '
#' + (idx + 1) + '' + '
' + ''; editor.insertBefore(entry, addEntry); bindKvEvents(entry); $('.dr-list-entry-del', entry).onclick = function () { entry.remove(); }; }; } // 删除条目 $$('.dr-list-entry-del', card).forEach(function (btn) { btn.onclick = function () { btn.closest('.dr-list-entry').remove(); }; }); // 每个条目内的 KV 添加 $$('.dr-list-entry', card).forEach(function (entry) { bindKvEvents(entry); }); } function bindKvEvents(container) { $$('.dr-kv-add', container).forEach(function (btn) { btn.onclick = function () { var editor = btn.closest('.dr-kv-editor') || btn.closest('.dr-list-entry'); if (!editor) return; var row = document.createElement('div'); row.className = 'dr-kv-row'; row.innerHTML = kvRowHtml('', ''); editor.insertBefore(row, btn); bindKvRowEvents(row); }; }); $$('.dr-kv-row', container).forEach(function (row) { bindKvRowEvents(row); }); } /** 收集维度卡片数据为扁平 map */ function collectVfDims(container) { var data = {}; $$('.pji-section-card', container).forEach(function (card) { var key = card.dataset.key; var type = card.dataset.type; if (!key) return; data[key] = readCardContent(card, type); }); return data; } /** 从卡片 DOM 读取内容 */ function readCardContent(card, type) { if (type === 'string') { var input = $('.pji-content', card); return input ? input.value : ''; } if (type === 'object') { var pairs = []; $$('.dr-kv-row', card).forEach(function (row) { var inputs = $$('input', row); if (inputs.length >= 2) { var k = inputs[0].value.trim(); var v = inputs[1].value; if (k) pairs.push({ k: k, v: v }); } }); var obj = {}; pairs.forEach(function (p) { obj[p.k] = p.v; }); return obj; } if (type === 'array') { var items = []; $$('.dr-example-item', card).forEach(function (inp) { var v = inp.value.trim(); if (v) items.push(v); }); return items; } if (type === 'list') { var entries = []; $$('.dr-list-entry', card).forEach(function (entry) { var obj = {}; $$('.dr-kv-row', entry).forEach(function (row) { var inputs = $$('input', row); if (inputs.length >= 2) { var k = inputs[0].value.trim(); var v = inputs[1].value; if (k) obj[k] = v; } }); if (Object.keys(obj).length) entries.push(obj); }); return entries; } return null; } /** 新增维度对话框 */ function addVfDimDialog(container, items) { var existingKeys = {}; items.forEach(function (it) { existingKeys[it.key] = true; }); var presetOpts = Object.keys(VER_PRESET_DIMS).map(function (name) { var disabled = existingKeys[name] ? ' disabled' : ''; var suffix = existingKeys[name] ? '(已添加)' : ''; return ''; }).join(''); var typeOpts = Object.keys(VER_TYPE_LABELS).map(function (t) { return ''; }).join(''); var m = Core.modal({ title: '新增维度', body: '
' + '
' + '' + '
', footer: '' }); var nameSel = $('#vfd-name', m.root); var customWrap = $('#vfd-custom-wrap', m.root); nameSel.onchange = function () { customWrap.style.display = nameSel.value === '__custom__' ? '' : 'none'; // 预设维度自动匹配推荐类型 if (nameSel.value !== '__custom__' && VER_PRESET_DIMS[nameSel.value]) { $('#vfd-type', m.root).value = VER_PRESET_DIMS[nameSel.value]; } }; $('.mf-cancel', m.root).onclick = m.close; $('.mf-ok', m.root).onclick = function () { var name = nameSel.value; if (name === '__custom__') { name = $('#vfd-custom', m.root).value.trim(); if (!name) { Core.toast('请输入维度名称', 'err'); return; } } if (existingKeys[name]) { Core.toast('该维度已存在', 'err'); return; } var type = $('#vfd-type', m.root).value; var content = type === 'string' ? '' : []; var item = { key: name, type: type, content: content }; items.push(item); drawVfDims(container, items); m.close(); }; } /** AI 生成销售风格维度对话框 */ function versionAiGenerateDialog(advicerId, parentModal, vfDims, vfItems) { Core.api("modelsup/list", { page: 1, page_size: 50 }).then(function (data) { var sups = (data && data.list) || []; if (!sups.length) { Core.toast("尚未配置模型提供方,请先到「模型配置」添加", "err"); return; } var defaultSupId = 0; if (state.detail && state.detail.Base) { defaultSupId = state.detail.Base.model_sup_id || 0; } var options = sups.map(function (s) { var selected = (s.sup_id === defaultSupId) ? " selected" : ""; return '"; }).join(""); var m = Core.modal({ title: "\u2726 AI 生成销售风格", width: "wide", body: '
AI 将根据项目信息和销售背景,自动生成 7 个风格维度。
' + '
' + '
' + '
' + '
' + '
', footer: '' + '' + '' }); var resultEl = $("#vag-result", m.root); $(".mf-close", m.root).onclick = m.close; $(".mf-gen", m.root).onclick = function () { var supId = Number($("#vag-sup", m.root).value); if (!supId) { Core.toast("请选择模型配置", "err"); return; } var userDesc = $("#vag-desc", m.root).value.trim(); Core.loading(true, "AI 生成中,请稍候…"); Core.api("advicer/version/generate", { advicerId: advicerId, supId: supId, desc: userDesc }) .then(function (data) { Core.loading(false); resultEl.value = Core.prettyJson(data); $(".mf-apply", m.root).disabled = !resultEl.value.trim(); Core.toast("生成完成", "ok"); }) .catch(function (e) { Core.loading(false); Core.toast(e.message, "err"); }); }; $(".mf-apply", m.root).onclick = function () { var content = resultEl.value.trim(); if (!content) return; try { var dataObj = JSON.parse(content); // 将生成的维度合并到表单 Object.keys(dataObj).forEach(function (key) { var val = dataObj[key]; var type = inferSectionType(val); var parsed = parseSectionContent(val, type); // 检查是否已存在 var existIdx = -1; for (var i = 0; i < vfItems.length; i++) { if (vfItems[i].key === key) { existIdx = i; break; } } if (existIdx >= 0) { vfItems[existIdx] = { key: key, type: type, content: parsed }; } else { vfItems.push({ key: key, type: type, content: parsed }); } }); drawVfDims(vfDims, vfItems); m.close(); Core.toast("风格维度已填入表单,请检查后保存", "ok"); } catch (e) { Core.toast("解析生成结果失败:" + e.message, "err"); } }; }).catch(function (e) { Core.toast(e.message, "err"); }); } var SKILL_DIMS = { "需求挖掘": "object", "痛点应对": "object", "价值塑造": "object", "促单话术": "object", "沟通节奏": "object" }; /** 将旧格式的复杂嵌套维度值归一化为扁平 KV(string→string) */ function normalizeSkillDim(val) { if (!val || typeof val !== "object") return {}; var result = {}; Object.keys(val).forEach(function (k) { var v = val[k]; if (typeof v === "string") { result[k] = v; } else if (Array.isArray(v)) { result[k] = v.join(";"); } else if (typeof v === "object" && v !== null) { // 嵌套对象:将每个子 key 的值也转为字符串 var sub = {}; Object.keys(v).forEach(function (sk) { var sv = v[sk]; sub[sk] = typeof sv === "string" ? sv : Array.isArray(sv) ? sv.join(";") : JSON.stringify(sv); }); result[k] = JSON.stringify(sub); } else { result[k] = String(v); } }); return result; } /** 从 API 返回的记录中提取技巧维度数据(兼容新旧格式) */ function extractSkillData(s) { // 新格式:后端已转换,直接返回 data 字段 if (s.data && Object.keys(s.data).length) return s.data; // 旧格式:从类型化字段组装 var d = {}; if (s.needsMining && Object.keys(s.needsMining).length) d["需求挖掘"] = s.needsMining; if (s.painPointResponse && Object.keys(s.painPointResponse).length) d["痛点应对"] = s.painPointResponse; if (s.valueBuilding && Object.keys(s.valueBuilding).length) d["价值塑造"] = s.valueBuilding; if (s.closingTechniques && Object.keys(s.closingTechniques).length) d["促单话术"] = s.closingTechniques; if (s.communicationRhythm && Object.keys(s.communicationRhythm).length) d["沟通节奏"] = s.communicationRhythm; return d; } function renderTabSkills(body) { body.innerHTML = dataHead("聊天技巧", "需求挖掘 / 痛点应对 / 价值塑造 / 促单 / 沟通节奏", "skl-add", "新增技巧") + '
' + '
'; loadSkills(); $("#skl-add").onclick = function () { skillForm(null); }; $("#skl-ai-gen").onclick = function () { // 先打开空白技巧表单,再触发 AI 生成 skillForm({ desc: "AI 生成 " + Core.nowTime().slice(0, 16) }); setTimeout(function () { var skDims = $("#skf-dims"); var skItems = []; $$(".pji-section-card", skDims).forEach(function (card) { skItems.push({ key: card.dataset.key, type: card.dataset.type, content: readCardContent(card, card.dataset.type) }); }); skillAiGenerateDialog(null, skDims, skItems); }, 200); }; } function loadSkills() { var box = $("#skl-list"); if (!box) return; box.innerHTML = '
'; Core.api("skill/list", { projectId: state.project.projectId }).then(function (list) { list = list || []; if (!list.length) { box.innerHTML = '
\u2726
暂无聊天技巧,可手动新增、AI 生成或从「数据分析」保存
'; return; } box.innerHTML = '
' + "" + list.map(function (s) { var dimData = extractSkillData(s); var filled = Object.keys(dimData).length; return "" + '" + '" + '" + '" + ''; }).join("") + "
ID描述技巧项更新时间操作
' + Core.esc(String(s.id).slice(-8)) + "' + Core.esc(s.desc || "-") + " 0 ? "yellow" : "gray") + '">' + filled + "/5' + Core.fmtTime(s.lastUpdateTime) + "' + '
"; $$(".skl-edit").forEach(function (b) { b.onclick = function () { var item = list.filter(function (x) { return String(x.id) === b.dataset.id; })[0]; skillForm(item); }; }); $$(".skl-del").forEach(function (b) { b.onclick = function () { Core.confirm("确定删除该聊天技巧?").then(function (ok) { if (!ok) return; Core.api("skill/del", { id: b.dataset.id }) .then(function () { Core.toast("已删除", "ok"); loadSkills(); }) .catch(function (e) { Core.toast(e.message, "err"); }); }); }; }); }).catch(function (e) { box.innerHTML = '
\u26a0
' + Core.esc(e.message) + "
"; }); } function skillForm(item) { var d = item || {}; var isEdit = !!d.id; var m = Core.modal({ title: isEdit ? "编辑聊天技巧" : "新增聊天技巧", width: "wide", body: '
' + '
' + '
', footer: '' }); $(".mf-cancel", m.root).onclick = m.close; // 渲染维度卡片 var skDims = $("#skf-dims", m.root); var skItems = []; var dimData = extractSkillData(d); Object.keys(SKILL_DIMS).forEach(function (name) { var raw = dimData[name]; // 归一化:旧数据可能是复杂嵌套结构,转为扁平 KV var val = normalizeSkillDim(raw); var hasData = Object.keys(val).length > 0; skItems.push({ key: name, type: "object", content: hasData ? normalizeKvPairs(val) : [] }); }); drawVfDims(skDims, skItems); // AI 生成 $("#skf-ai-gen", m.root).onclick = function () { skillAiGenerateDialog(m, skDims, skItems); }; // 保存 $(".mf-save", m.root).onclick = function () { var dataObj = collectVfDims(skDims); var payload = { projectId: state.project.projectId, data: dataObj, desc: $("#skf-desc", m.root).value.trim() }; if (isEdit) payload.id = d.id; Core.api(isEdit ? "skill/update" : "skill/add", payload) .then(function () { m.close(); Core.toast("保存成功", "ok"); loadSkills(); }) .catch(function (e) { Core.toast(e.message, "err"); }); }; } /** AI 生成聊天技巧对话框 */ function skillAiGenerateDialog(parentModal, skDims, skItems) { Core.api("modelsup/list", { page: 1, page_size: 50 }).then(function (data) { var sups = (data && data.list) || []; if (!sups.length) { Core.toast("尚未配置模型提供方,请先到「模型配置」添加", "err"); return; } var defaultSupId = 0; if (state.detail && state.detail.Base) { defaultSupId = state.detail.Base.model_sup_id || 0; } var options = sups.map(function (s) { var selected = (s.sup_id === defaultSupId) ? " selected" : ""; return '"; }).join(""); var m = Core.modal({ title: "\u2726 AI 生成聊天技巧", width: "wide", body: '
AI 将根据项目信息和项目资料,自动生成 5 个维度的聊天技巧。若项目资料为空,将仅基于项目基础信息生成。
' + '
" + '
' + '
', footer: '' + '' + '' }); var resultEl = $("#sg-result", m.root); $(".mf-close", m.root).onclick = m.close; $(".mf-gen", m.root).onclick = function () { var supId = Number($("#sg-sup", m.root).value); if (!supId) { Core.toast("请选择模型配置", "err"); return; } Core.loading(true, "AI 生成中,请稍候…"); Core.api("skill/generate", { projectId: state.project.projectId, supId: supId }) .then(function (data) { Core.loading(false); resultEl.value = Core.prettyJson(data); $(".mf-apply", m.root).disabled = !resultEl.value.trim(); Core.toast("生成完成", "ok"); }) .catch(function (e) { Core.loading(false); Core.toast(e.message, "err"); }); }; $(".mf-apply", m.root).onclick = function () { var content = resultEl.value.trim(); if (!content) return; try { var dataObj = JSON.parse(content); // 将生成的维度合并到可视化编辑器 Object.keys(dataObj).forEach(function (key) { var val = dataObj[key]; var type = inferSectionType(val); var parsed = parseSectionContent(val, type); var existIdx = -1; for (var i = 0; i < skItems.length; i++) { if (skItems[i].key === key) { existIdx = i; break; } } if (existIdx >= 0) { skItems[existIdx] = { key: key, type: type, content: parsed }; } else { skItems.push({ key: key, type: type, content: parsed }); } }); drawVfDims(skDims, skItems); m.close(); Core.toast("聊天技巧已填入表单,请检查后保存", "ok"); } catch (e) { Core.toast("解析生成结果失败:" + e.message, "err"); } }; }).catch(function (e) { Core.toast(e.message, "err"); }); } /* ---------- Tab 4:客户 ---------- */ function renderTabClients(body) { // 客户 tab 现仅展示「托管客户」;客户画像改为在托管客户行内「查看画像」按钮进入 body.innerHTML = '
托管客户' + '处于托管标签分组内的微信好友;点击「查看画像」查看并维护该客户的 AI 客户画像
' + '
'; loadHostedCustomers(body.querySelector("#cli-list")); } /* ---------- 查看/维护托管客户的 AI 客户画像(client 表按 wxid 关联,动态栏目编辑器) ---------- */ function viewClientProfile(c) { if (!c) return; var wxid = c.user_name || ""; var name = c.remark || c.nick_name || c.user_name || "客户"; var appId = c.advicer_wx_device_id || ""; var advicerId = c.advicer_id || 0; var advicerName = c.advicer_name || ""; if (!wxid) { Core.toast("该客户缺少微信ID,无法关联画像", "err"); return; } Core.api("client/list", { projectId: state.project.projectId }).then(function (list) { list = list || []; var profile = list.filter(function (x) { return x.wxid === wxid; })[0] || null; if (profile) { clientProfileForm(profile, { name: name, wxid: wxid, appId: appId, advicerId: advicerId, advicerName: advicerName }); return; } Core.confirm("客户「" + name + "」暂无 AI 画像,是否现在手动新建一份?(也可直接点「画像更新」由聊天记录自动生成)").then(function (ok) { if (!ok) return; clientProfileForm(null, { name: name, wxid: wxid, appId: appId, advicerId: advicerId, advicerName: advicerName }); }); }).catch(function (e) { Core.toast(e.message, "err"); }); } /* ---------- 客户等级常量 / 微信绑定 / 等级评估 ---------- */ /* ---------- 托管客户列表 ---------- */ function loadHostedCustomers(box) { if (!box) box = document.querySelector("#cli-list"); if (!box) return; box.innerHTML = '
'; var pid = state.project.projectId; Promise.all([ Core.api("advicer/customer/hosted", { projectId: pid }), Core.api("client/list", { projectId: pid }).catch(function () { return []; }) ]).then(function (arr) { var list = arr[0] || []; // 客户画像按 wxid 建索引,用于展示等级评估结果 var profMap = {}; (arr[1] || []).forEach(function (x) { if (x.wxid) profMap[x.wxid] = x; }); if (!list.length) { box.innerHTML = '
\u25c9
\u5c1a\u65e0\u6258\u7ba1\u5ba2\u6237\uff0c\u5f00\u542f\u6258\u7ba1\u6807\u7b7e\u540e\u6d88\u606f\u56de\u8c03\u65f6\u81ea\u52a8\u540c\u6b65
'; return; } box.innerHTML = '
\u5171 ' + list.length + ' \u4f4d\u6258\u7ba1\u5ba2\u6237\uff08\u5904\u4e8e\u6258\u7ba1\u6807\u7b7e\u5206\u7ec4\u5185\u7684\u5fae\u4fe1\u597d\u53cb\uff0c\u6bcf\u6b21\u6d88\u606f\u56de\u8c03\u65f6\u81ea\u52a8\u540c\u6b65\u6700\u65b0\u4fe1\u606f\uff09
' + '
' + '' + list.map(function (c) { var displayName = c.remark || c.nick_name || c.user_name || "-"; var avatar = c.big_head_img_url || c.small_head_img_url || ""; var avatarCell = avatar ? '' : '\u25c9'; var sex = c.sex === 1 ? "\u7537" : c.sex === 2 ? "\u5973" : "-"; var area = [c.province, c.city].filter(function (s) { return s; }).join(" ") || "-"; var sessionStatus = c.session_id ? '\u5df2\u6258\u7ba1' : '\u672a\u6ce8\u518c'; var labels = (c.label_names || "").split(",").filter(function (s) { return s; }); var labelHtml = labels.map(function (l) { return '' + Core.esc(l) + ''; }).join(" "); var prof = profMap[c.user_name]; var levelCell = '-'; if (prof) { if (prof.clientLevel) { var lvDesc = CLIENT_LEVELS[prof.clientLevel] || prof.clientLevel; var lvCls = CLIENT_LEVEL_CLS[prof.clientLevel] || "gray"; var lvTip = prof.levelReason || ""; if (!isZeroTime(prof.levelUpdateAt)) lvTip += (lvTip ? "\n" : "") + "更新:" + Core.fmtTime(prof.levelUpdateAt); levelCell = '' + Core.esc(lvDesc) + ''; } else { levelCell = '未评估'; } } // 当前任务:对话 AI 回写的简洁 mission,展示在客户列表 var missionCell = '-'; if (prof && prof.mission) { var ms = String(prof.mission); missionCell = '' + Core.esc(ms.length > 14 ? ms.slice(0, 14) + "…" : ms) + ''; } return "" + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; }).join("") + "
头像昵称微信备注托管标签性别地区归属销售客户等级当前任务会话状态更新时间操作
' + avatarCell + '' + Core.esc(displayName) + '' + (c.user_name ? '
' + Core.esc(c.user_name) + '
' : '') + '
' + Core.esc(c.remark || "-") + '' + (labelHtml || "-") + '' + sex + '' + Core.esc(area) + '' + Core.esc(c.advicer_name || "-") + '' + levelCell + '' + missionCell + '' + sessionStatus + '' + Core.fmtTime(c.update_at) + '' + '' + '' + '' + '
"; var byId = {}; list.forEach(function (c) { byId[c.customer_id] = c; }); $$('.hc-profile', box).forEach(function (b) { b.onclick = function () { viewClientProfile(byId[b.dataset.cid]); }; }); $$('.hc-chat', box).forEach(function (b) { b.onclick = function () { viewHostedChat(byId[b.dataset.cid]); }; }); $$('.hc-remove', box).forEach(function (b) { b.onclick = function () { removeHostedCustomer(byId[b.dataset.cid]); }; }); }).catch(function (e) { box.innerHTML = '
\u26a0
' + Core.esc(e.message) + '
'; }); } /* ---------- 托管客户操作:聊天记录 / 移除 ---------- */ // 将客户从托管标签中移除(微信 API + DB 同步) function removeHostedCustomer(c) { if (!c) return; var name = c.remark || c.nick_name || c.user_name || "该客户"; Core.confirm("将客户「" + name + "」从托管标签中移除?移除后不再命中托管分组,也不会触发 AI 自动回复。").then(function (ok) { if (!ok) return; Core.api("advicer/customer/remove_label", { projectId: state.project.projectId, customerId: c.customer_id }) .then(function () { Core.toast("已从托管标签移除", "ok"); loadHostedCustomers(); }) .catch(function (e) { Core.toast(e.message, "err"); }); }); } // 查看客户聊天记录(只读,来自微信消息流水) function viewHostedChat(c) { if (!c) return; var name = c.remark || c.nick_name || c.user_name || "客户"; var m = Core.modal({ title: "聊天记录 · " + name, width: "wide", body: '
' }); var box = $("#hc-chat", m.root); Core.api("wx/conversation/msgs", { selfWxid: c.self_wxid, wxid: c.user_name, page_size: 200 }) .then(function (list) { list = list || []; if (!list.length) { box.innerHTML = '
\u2726
与「' + Core.esc(name) + '」暂无聊天记录
'; return; } box.innerHTML = list.map(function (msg) { var isSelf = msg.direction === "self" || msg.direction === "ai"; var tag = msg.direction === "ai" ? "AI" : msg.direction === "self" ? "销售" : "客户"; var avatarHtml = msg.direction === "ai" ? '
\u2726
' : '
' + Core.esc(Core.initial(isSelf ? tag : name)) + '
'; var bubbleClass = isSelf ? "msg-bubble conv-msg-self" : "msg-bubble conv-msg-other"; return '
' + avatarHtml + '
' + hostedChatContent(msg) + '
' + '
' + tag + ' · ' + Core.esc(Core.fmtTime(msg.createAt || msg.create_at)) + '
'; }).join(""); box.scrollTop = box.scrollHeight; }) .catch(function (e) { box.innerHTML = '
\u26a0
' + Core.esc(e.message) + '
'; }); } function hostedChatContent(msg) { var content = msg.content || ""; switch (msg.msgType) { case "text": return Core.esc(content); case "image": return '\ud83d\udcf7 [图片]'; case "emoji": return '\ud83d\ude0a [表情]'; case "voice": return '\ud83c\udfa4 [语音]'; case "video": return '\ud83c\udfac [视频]'; case "location": return '\ud83d\udccd [位置]'; case "namecard": return '\ud83d\udc64 [名片]'; case "app_msg": return '\ud83d\udd17 [链接/小程序]'; default: return '[' + Core.esc(msg.msgType || "消息") + ']'; } } var CLIENT_LEVELS = { regular: "熟客", intent: "意向客户", sleeping: "沉睡客户", non: "非客户" }; var CLIENT_LEVEL_CLS = { regular: "purple", intent: "green", sleeping: "yellow", non: "gray" }; function isZeroTime(t) { return !t || String(t).indexOf("0001-01-01") === 0; } function evaluateClient(id, name, btn, onDone) { Core.confirm("对客户「" + (name || "未命名") + "」执行等级评估?(AI 将分析近期聊天记录判定等级)").then(function (ok) { if (!ok) return; var oldText = btn ? btn.textContent : ""; if (btn) { btn.textContent = "评估中…"; btn.disabled = true; } Core.api("client/evaluate", { clientId: id, force: true }) .then(function (res) { if (btn) { btn.textContent = oldText; btn.disabled = false; } if (res && res.async) { Core.toast(res.msg || "评估任务已启动,请稍后刷新", "ok"); return; } var d = res && res.details && res.details[0]; Core.toast(d ? "评估完成:" + (d.levelDesc || d.level) + " · " + (d.reason || "") : "评估完成", "ok"); if (onDone) onDone(); }) .catch(function (e) { if (btn) { btn.textContent = oldText; btn.disabled = false; } Core.toast(e.message, "err"); }); }); } function iterateClient(id, name, btn, onDone) { Core.confirm("对客户「" + (name || "未命名") + "」立即执行画像迭代?(AI 将再次分析近期聊天记录,自动新建/完善客户画像)").then(function (ok) { if (!ok) return; var oldText = btn ? btn.textContent : ""; if (btn) { btn.textContent = "迭代中…"; btn.disabled = true; } Core.api("smart/iterate/run", { clientId: id, force: true }) .then(function (res) { if (btn) { btn.textContent = oldText; btn.disabled = false; } res = res || {}; var detail = (res.details || [])[0] || ""; Core.toast("迭代完成:扫描 " + (res.scanned || 0) + " 条未分析消息·" + (detail || "画像无新增信息"), "ok"); if (onDone) onDone(); }) .catch(function (e) { if (btn) { btn.textContent = oldText; btn.disabled = false; } Core.toast(e.message, "err"); }); }); } /* 客户画像动态栏目编辑器(对齐「项目资料」编辑方式);支持画像更新(旧画像+聊天记录重生成) */ function clientProfileForm(item, ctx) { var d = item || {}; ctx = ctx || {}; var isEdit = !!d.id; var wxid = d.wxid || ctx.wxid || ""; var appId = d.appId || ctx.appId || ""; // 归属销售:画像自动归属于客户来源的销售(编辑态取已存 advicerId,新建取来源托管客户的 advicer_id) var advicerId = Number(d.advicerId || ctx.advicerId || 0); // 动态栏目数据源:从画像 profile(栏目名→内容)还原 state._cpSections = buildSectionsFromData(d.profile || {}); var m = Core.modal({ title: isEdit ? ("客户画像 · " + (ctx.name || "")) : ("新建客户画像" + (ctx.name ? " · " + ctx.name : "")), width: "wide", body: '
归属销售:' + Core.esc(ctx.advicerName || (advicerId ? ("#" + advicerId) : "未关联")) + '(画像自动归属于该客户来源的销售,无需手动选择)
' + '
' + '按「项目资料」方式维护客户画像栏目;后端按约定栏目名「姓名」「生日」读取,用于称呼与生日关怀。' + '' + '' + '
' + '
', footer: '' + (isEdit ? '' : '') + '' }); // 将共享编辑器指向本弹窗(数据源=state._cpSections,容器=弹窗内 #pji-sections) setPjiEditorTarget(m.root, function () { return state._cpSections; }, function (v) { state._cpSections = v; }); drawPjiSections(); var closeProfile = function () { setPjiEditorTarget(null); m.close(); loadHostedCustomers(); }; $("#cp-add-section", m.root).onclick = function () { addPjiSectionDialog(); }; $(".mf-cancel", m.root).onclick = closeProfile; var mask = m.root.closest(".modal-mask"); if (mask) mask.querySelector(".x").onclick = closeProfile; // 绑定 等级评估/删除(仅编辑态或后端建档后补挂) function bindEditActions() { var be = $(".cp-eval", m.root); if (be && !be._bound) { be._bound = true; be.onclick = function () { evaluateClient(d.id, ctx.name || "", be, closeProfile); }; } var bd = $(".mf-del", m.root); if (bd && !bd._bound) { bd._bound = true; bd.onclick = function () { Core.confirm("确定删除该客户画像?").then(function (ok) { if (!ok) return; Core.api("client/del", { id: d.id }).then(function () { closeProfile(); Core.toast("已删除画像", "ok"); }) .catch(function (e) { Core.toast(e.message, "err"); }); }); }; } } if (isEdit) bindEditActions(); // 新建/重生成后画像变为已存在:补齐 等级评估/删除 按钮 function refreshEditActions() { if (!isEdit) return; var foot = m.root.querySelector(".modal-foot"); if (foot && !foot.querySelector(".cp-eval")) { var cancelBtn = foot.querySelector(".mf-cancel"); var evalBtn = document.createElement("button"); evalBtn.className = "btn ghost cp-eval"; evalBtn.textContent = "等级评估"; var delBtn = document.createElement("button"); delBtn.className = "btn danger mf-del"; delBtn.textContent = "删除画像"; if (cancelBtn) { foot.insertBefore(evalBtn, cancelBtn); foot.insertBefore(delBtn, cancelBtn); } else { foot.appendChild(evalBtn); foot.appendChild(delBtn); } } bindEditActions(); } // 重新按 id/wxid 取回最新画像并就地重绘(不关闭弹窗) function reloadProfileIntoDialog() { Core.api("client/list", { projectId: state.project.projectId }).then(function (list) { list = list || []; var found = list.filter(function (x) { return (d.id && String(x.id) === String(d.id)) || (wxid && x.wxid === wxid); })[0]; if (!found) { Core.toast("未取回最新画像,请重新打开", "err"); return; } d = found; isEdit = !!d.id; wxid = d.wxid || wxid; appId = d.appId || appId; advicerId = Number(d.advicerId || advicerId || 0); state._cpSections = buildSectionsFromData(d.profile || {}); drawPjiSections(); refreshEditActions(); Core.toast("已载入最新画像", "ok"); }).catch(function (e) { Core.toast(e.message, "err"); }); } // 画像更新:基于旧画像 + 聊天记录重新生成(clientId 优先,否则按 wxid 自动建档) var regenBtn = $(".cp-regen", m.root); if (regenBtn) regenBtn.onclick = function () { if (!d.id && !wxid) { Core.toast("请先保存画像后再更新", "err"); return; } Core.confirm("将结合客户旧画像与最近聊天记录重新生成画像栏目(会覆盖当前编辑内容),是否继续?").then(function (ok) { if (!ok) return; syncPjiSectionsFromDom(); var req = d.id ? { clientId: d.id } : { wxid: wxid, appId: appId, projectId: state.project.projectId }; var oldText = regenBtn.textContent; regenBtn.disabled = true; regenBtn.textContent = "更新中…"; Core.api("smart/profile/regenerate", req).then(function (res) { regenBtn.disabled = false; regenBtn.textContent = oldText; res = res || {}; Core.toast((res.details && res.details[0]) || "画像更新完成", "ok"); reloadProfileIntoDialog(); }).catch(function (e) { regenBtn.disabled = false; regenBtn.textContent = oldText; Core.toast(e.message, "err"); }); }); }; // 保存:将栏目展平为 {栏目名: content} 的 profile 全量覆盖 $(".mf-save", m.root).onclick = function () { if (!advicerId) { Core.toast("未能确定归属销售(该客户未关联销售),无法保存画像", "err"); return; } var sections = collectPjiSectionsData(); var profile = {}; sections.forEach(function (s) { var nm = (s.label || s.key || "").trim(); if (!nm) return; var c = s.content; // 跳过空白栏目 if (c === "" || c == null) return; if (Array.isArray(c) && !c.length) return; if (c && typeof c === "object" && !Array.isArray(c) && !Object.keys(c).length) return; profile[nm] = c; }); var payload = { projectId: state.project.projectId, advicerId: advicerId, profile: profile }; if (isEdit) payload.id = d.id; else { payload.wxid = wxid; payload.appId = appId; } Core.api(isEdit ? "client/update" : "client/add", payload) .then(function () { closeProfile(); Core.toast("保存成功", "ok"); }) .catch(function (e) { Core.toast(e.message, "err"); }); }; } /* ---------- 销售人员微信工作台入口 ---------- */ /** * 打开销售人员的微信工作台 * 检查项目 wx_token(必须),不检查 wx_device_id(登录检测由 wx.js mount 负责) */ function openAdvicerWxWorkbench(advicerId, advicerName, wxDeviceId) { var checkAndEnter = function () { var projectWxToken = ''; if (state.detail && state.detail.Base) { projectWxToken = state.detail.Base.wx_token || ''; } if (!projectWxToken) { var m = Core.modal({ title: '微信工作台未就绪', body: '
' + '
' + '⚠' + '
当前项目尚未配置 X-finder-TOKEN,' + '请先在项目设置中完成微信接入配置,方可使用微信工作台功能。
' + '
', footer: '' }); $('.cm-cancel', m.root).onclick = m.close; $('.cm-go', m.root).onclick = function () { m.close(); state._pendingSettingsTab = true; switchView('projectDetail'); }; return; } enterWxWorkbench(advicerId, advicerName); }; if (state.detail && state.detail.Base) { checkAndEnter(); } else { Core.api('project/info', { projectId: state.project.projectId }).then(function (info) { state.detail = info || {}; checkAndEnter(); }).catch(function (e) { Core.toast('加载项目信息失败:' + e.message, 'err'); }); } } /** * 进入微信工作台视图 * 通过 URL hash 传递 advicerId,刷新页面时工作台可自动恢复 */ function enterWxWorkbench(advicerId, advicerName) { state.wxAdvicer = { advicerId: advicerId, name: advicerName }; location.hash = 'wx-advicer=' + advicerId; switchView('wx'); } /* ---------- Tab 6:成交线索(约定到访/成交/付款) ---------- */ var LEAD_TYPE_META = { visit: "约定到访", deal: "约定成交", payment: "约定付款" }; var LEAD_STATUS_META = { pending: "待跟进", done: "已完成", cancelled: "已取消" }; function renderTabLeads(body) { state.data.leadStatus = state.data.leadStatus || ""; var load = function () { Core.api("smart/lead/list", { projectId: state.project.projectId, status: state.data.leadStatus }).then(function (list) { list = list || []; var chips = [["", "全部"], ["pending", "待跟进"], ["done", "已完成"]].map(function (c) { return ''; }).join(" "); var html = '
成交线索' + '共 ' + list.length + ' 条·对话复盘自动抽取到访/成交/付款约定,供销售跟进并注入后续 AI 对话' + chips + '
'; if (!list.length) { html += '
◉
暂无成交线索
'; } else { html += '
' + "" + list.map(function (t) { var isPending = t.status === "pending"; var result = (t.result || "").slice(0, 40); var ops = "-"; if (isPending) { ops = '' + ''; } return "" + '" + "" + '" + '" + '" + '" + '" + '"; }).join("") + "
客户线索类型约定约定时间状态来源跟进结果操作
' + Core.esc((t.wxid || "").slice(0, 12) || "-") + "" + (LEAD_TYPE_META[t.lead_type] || t.lead_type || "-") + "' + Core.esc(t.title || "-") + "" + (t.detail ? '
' + Core.esc(t.detail) + "
" : "") + "
' + Core.fmtTime(t.expect_at) + "' + (LEAD_STATUS_META[t.status] || t.status) + "' + (t.source === "manual" ? "手动" : "AI") + "' + Core.esc(result) + "' + ops + "
"; } body.innerHTML = html; $("#ld-add").onclick = function () { leadForm(); }; $$(".ld-chip").forEach(function (b) { b.onclick = function () { state.data.leadStatus = b.dataset.status; renderDataBody(); }; }); $$(".ld-cancel").forEach(function (b) { b.onclick = function () { cancelLead(b.dataset.id, b.dataset.title); }; }); $$(".ld-done").forEach(function (b) { b.onclick = function () { completeLead(b.dataset.id, b.dataset.title); }; }); }).catch(function (e) { body.innerHTML = '
⚠
' + Core.esc(e.message) + "
"; }); }; load(); } /* 托管客户单选下拉(数据源=advicer/customer/hosted;value=wxid,data-appid=登录实例) */ function loadHostedCustomerOptions(sel, currentWxid) { if (!sel) return; sel.innerHTML = ''; Core.api("advicer/customer/hosted", { projectId: state.project.projectId }).then(function (list) { list = list || []; if (!list.length) { sel.innerHTML = ''; return; } sel.innerHTML = '' + list.map(function (c) { var label = c.remark || c.nick_name || c.user_name || "客户"; var selected = (currentWxid && c.user_name === currentWxid) ? " selected" : ""; return ''; }).join(""); }).catch(function (e) { sel.innerHTML = ''; }); } function leadForm() { var m = Core.modal({ title: "新增成交线索", body: '
' + '
' + '
' + '
' + "
" + '
' + '
', footer: '' }); loadHostedCustomerOptions($("#ld-client", m.root)); $(".mf-cancel", m.root).onclick = m.close; $(".mf-save", m.root).onclick = function () { var sel = $("#ld-client", m.root); var wxid = sel.value; var appId = sel.options[sel.selectedIndex] ? (sel.options[sel.selectedIndex].dataset.appid || "") : ""; var expectAt = $("#ld-expect", m.root).value.trim(); if (!wxid) { Core.toast("请选择客户", "err"); return; } if (!/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(expectAt)) { Core.toast("约定时间格式应形如 2026-09-29 14:00", "err"); return; } Core.api("smart/lead/create", { wxid: wxid, appId: appId, projectId: state.project.projectId, leadType: $("#ld-type", m.root).value, title: $("#ld-title", m.root).value.trim(), detail: $("#ld-detail", m.root).value.trim(), expectAt: expectAt.replace("T", " ").slice(0, 16) }).then(function () { m.close(); Core.toast("已新增成交线索", "ok"); renderDataBody(); }) .catch(function (e) { Core.toast(e.message, "err"); }); }; } function cancelLead(id, title) { Core.confirm("确定取消成交线索「" + (title || "未命名") + "」?").then(function (ok) { if (!ok) return; Core.api("smart/lead/cancel", { id: Number(id) }).then(function () { Core.toast("已取消", "ok"); renderDataBody(); }).catch(function (e) { Core.toast(e.message, "err"); }); }); } function completeLead(id, title) { Core.confirm("将成交线索「" + (title || "未命名") + "」标记为已完成?").then(function (ok) { if (!ok) return; Core.api("smart/lead/complete", { id: Number(id) }).then(function () { Core.toast("已标记完成", "ok"); renderDataBody(); }).catch(function (e) { Core.toast(e.message, "err"); }); }); } /* ---------- Tab 7:定时提醒(到点由 AI 主动发消息) ---------- */ var REMIND_STATUS_META = { pending: "待执行", done: "已发送", cancelled: "已取消", failed: "失败" }; function renderTabReminds(body) { state.data.remindStatus = state.data.remindStatus || ""; var load = function () { Core.api("smart/remind/list", { projectId: state.project.projectId, status: state.data.remindStatus }).then(function (list) { list = list || []; var chips = [["", "全部"], ["pending", "待执行"], ["done", "已发送"], ["failed", "失败"]].map(function (c) { return ''; }).join(" "); var html = '
定时提醒' + '共 ' + list.length + ' 条·到点由 AI 主动发消息提醒客户' + chips + '
'; if (!list.length) { html += '
◉
暂无定时提醒
'; } else { html += '
' + "" + list.map(function (t) { var isPending = t.status === "pending"; var result = (t.result || t.err_msg || "").slice(0, 40); var ops = "-"; if (isPending) { ops = ''; } return "" + '" + '" + '" + '" + '" + '" + '"; }).join("") + "
客户提醒到点时间状态来源结果/失败操作
' + Core.esc((t.wxid || "").slice(0, 12) || "-") + "' + Core.esc(t.title || "-") + "" + (t.detail ? '
' + Core.esc(t.detail) + "
" : "") + "
' + Core.fmtTime(t.due_at) + "' + (REMIND_STATUS_META[t.status] || t.status) + "' + (t.source === "manual" ? "手动" : "AI") + "' + Core.esc(result) + "' + ops + "
"; } body.innerHTML = html; $("#rm-add").onclick = function () { remindForm(); }; $$(".rm-chip").forEach(function (b) { b.onclick = function () { state.data.remindStatus = b.dataset.status; renderDataBody(); }; }); $$(".rm-cancel").forEach(function (b) { b.onclick = function () { cancelRemind(b.dataset.id, b.dataset.title); }; }); }).catch(function (e) { body.innerHTML = '
⚠
' + Core.esc(e.message) + "
"; }); }; load(); } function remindForm() { var m = Core.modal({ title: "新增定时提醒", body: '
' + '
' + '
' + '
', footer: '' }); loadHostedCustomerOptions($("#rm-client", m.root)); $(".mf-cancel", m.root).onclick = m.close; $(".mf-save", m.root).onclick = function () { var sel = $("#rm-client", m.root); var wxid = sel.value; var appId = sel.options[sel.selectedIndex] ? (sel.options[sel.selectedIndex].dataset.appid || "") : ""; var dueAt = $("#rm-due", m.root).value.trim(); if (!wxid) { Core.toast("请选择客户", "err"); return; } if (!/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(dueAt)) { Core.toast("到点时间格式应形如 2026-09-29 14:00", "err"); return; } Core.api("smart/remind/create", { wxid: wxid, appId: appId, projectId: state.project.projectId, title: $("#rm-title", m.root).value.trim(), detail: $("#rm-detail", m.root).value.trim(), dueAt: dueAt.replace("T", " ").slice(0, 16) }).then(function () { m.close(); Core.toast("已新增定时提醒", "ok"); renderDataBody(); }) .catch(function (e) { Core.toast(e.message, "err"); }); }; } function cancelRemind(id, title) { Core.confirm("确定取消定时提醒「" + (title || "未命名") + "」?").then(function (ok) { if (!ok) return; Core.api("smart/remind/cancel", { id: Number(id) }).then(function () { Core.toast("已取消", "ok"); renderDataBody(); }).catch(function (e) { Core.toast(e.message, "err"); }); }); }