ai_scheduler/web/assets/js/project_data.js

1250 lines
58 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.

/* ==================== 视图四:数据中心 ==================== */
var DATA_TABS = [
["advicers", "\u9500\u552e\u4eba\u5458"],
["versions", "\u6570\u5b57\u4eba\u7ba1\u7406"],
["skills", "\u804a\u5929\u6280\u5de7"],
["clients", "\u5ba2\u6237"]
];
function renderData() {
if (!state.project || !state.project.projectId) { switchView("projects"); return; }
var st = state.data;
$("#view-data").innerHTML =
'<div class="card">' +
'<div class="tabs" id="data-tabs">' + DATA_TABS.map(function (t) {
return '<div class="tab' + (st.tab === t[0] ? " active" : "") + '" data-tab="' + t[0] + '">' + t[1] + "</div>";
}).join("") + "</div>" +
'<div id="data-body"></div>' +
"</div>";
$$("#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: "客户管理" };
$("#pageTitle").textContent = tabMeta[st.tab] || "数据中心";
$("#pageSub").textContent = "管理当前项目下的" + (tabMeta[st.tab] || "数据");
}
renderDataBody();
};
});
renderDataBody();
}
function renderDataBody() {
var body = $("#data-body");
body.innerHTML = '<div class="empty"><span class="spin"></span></div>';
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);
}
function dataHead(title, sub, addBtnId, addBtnText) {
return '<div class="row mb-2"><span class="fs-13 c-main" style="font-weight:700">' + title + '</span>' +
'<span class="fs-12 c-dim">' + sub + '</span><span class="spacer"></span>' +
'<button class="btn primary sm" id="' + addBtnId + '">+ ' + addBtnText + "</button></div>";
}
/* ---------- 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 += '<div class="empty"><div class="big">◉</div>暂无销售人员,点击右上角新增</div>';
} else {
html += '<div class="table-wrap"><table class="table"><thead><tr>' +
"<th>ID</th><th>姓名</th><th>生日</th><th>入职时间</th><th>性别</th><th>行业经验</th><th>微信设备ID</th><th>托管</th><th>文件助手</th><th>创建时间</th><th>操作</th></tr></thead><tbody>" +
list.map(function (a) {
var gender = a.gender === 1 ? "男" : a.gender === 2 ? "女" : "-";
var hOn = a.hosting_enabled === 1 || a.hosting_enabled === true;
return "<tr>" +
'<td class="mono">' + a.advicer_id + "</td>" +
'<td><span class="name">' + Core.esc(a.name) + "</span></td>" +
'<td class="fs-12">' + Core.fmtTime(a.birth).slice(0, 10) + "</td>" +
'<td class="fs-12">' + (a.join_date ? Core.fmtTime(a.join_date).slice(0, 10) : "-") + "</td>" +
"<td>" + gender + "</td>" +
"<td>" + (a.working_years || "-") + " 年</td>" +
'<td class="mono fs-12">' + Core.esc(a.wx_device_id || '-') + "</td>" +
'<td><label class="switch"><input type="checkbox" class="adv-hosting" data-id="' + a.advicer_id + '" data-name="' + Core.esc(a.name || "") + '" data-enabled="' + (hOn ? 1 : 0) + '" data-ver="' + Core.esc(a.hosting_version_id || "") + '" data-skill="' + Core.esc(a.hosting_skill_id || "") + '"' + (hOn ? ' checked' : '') + '><span class="slider"></span></label></td>' +
'<td><label class="switch"><input type="checkbox" class="adv-filehelper" data-id="' + a.advicer_id + '"' + ((a.reply_filehelper === 1 || a.reply_filehelper === true) ? ' checked' : '') + '><span class="slider"></span></label></td>' +
'<td class="fs-12">' + Core.fmtTime(a.create_at) + "</td>" +
'<td class="ops"><button class="btn ghost sm adv-edit" data-id="' + a.advicer_id + '">编辑</button>' +
'<button class="btn ghost sm adv-avatar" data-id="' + a.advicer_id + '" data-name="' + Core.esc(a.name || "") + '">数字人管理</button>' +
'<button class="btn primary sm adv-wx" data-id="' + a.advicer_id + '" data-name="' + Core.esc(a.name || "") + '" data-device="' + Core.esc(a.wx_device_id || "") + '">微信工作台</button></td>' +
"</tr>";
}).join("") + "</tbody></table></div>";
}
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 = '<div class="empty"><div class="big">⚠</div>' + Core.esc(e.message) + "</div>";
});
}
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:
'<div class="grid cols-2">' +
'<div class="field"><label class="label">\u59d3\u540d<span class="req">*</span></label><input id="adv-name" class="input" value="' + Core.esc(d.name || "") + '"></div>' +
'<div class="field"><label class="label">\u751f\u65e5<span class="req">*</span></label><input id="adv-birth" class="input dp-date" placeholder="\u70b9\u51fb\u9009\u62e9\u65e5\u671f" value="' + birth + '"></div>' +
"</div>" +
'<div class="grid cols-2">' +
'<div class="field"><label class="label">\u5165\u804c\u65f6\u95f4</label><input id="adv-joindate" class="input dp-date" placeholder="\u70b9\u51fb\u9009\u62e9\u65e5\u671f" value="' + joinDate + '"></div>' +
'<div class="field"><label class="label">性别</label><select id="adv-gender" class="select">' +
'<option value="1"' + (d.gender === 1 ? " selected" : "") + ">男</option>" +
'<option value="2"' + (d.gender === 2 ? " selected" : "") + '>\u5973</option></select></div>' +
"</div>" +
'<div class="grid cols-2">' +
'<div class="field"><label class="label">行业经验</label><input id="adv-years" class="input" type="number" value="' + (d.working_years || 1) + '"></div>' +
'<div class="field"><label class="label">微信设备ID</label><input id="adv-wxdevice" class="input mono" value="' + Core.esc(d.wx_device_id || '') + '" placeholder="\u5fae\u4fe1\u5de5\u4f5c\u53f0\u8bbe\u5907\u6807\u8bc6"></div>' +
'</div>' +
'<div class="fs-12 c-dim">销售是「版本 / 技巧 / 客户 / AI 会话」的归属主体,生日将用于拟人化对话。</div>',
footer: '<button class="btn ghost mf-cancel">取消</button><button class="btn primary mf-save">保存</button>'
});
$(".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:
'<div class="card-sub" style="margin-top:0">开启托管后,AI 将自动使用该销售配置的数字人版本和聊天技巧回复客户消息。<br>不选择具体版本时,默认使用最新创建的一条。</div>' +
'<div class="field"><label class="label">数字人版本</label>' +
'<select id="hc-version" class="select"><option value="">加载中…</option></select>' +
'<div class="fs-12 c-dim" style="margin-top:4px">留空 = 每次自动使用最新版本</div></div>' +
'<div class="field"><label class="label">聊天技巧</label>' +
'<select id="hc-skill" class="select"><option value="">加载中…</option></select>' +
'<div class="fs-12 c-dim" style="margin-top:4px">留空 = 每次自动使用最新一条</div></div>',
footer: '<button class="btn ghost mf-cancel">取消</button><button class="btn primary mf-save">确认开启</button>'
});
$(".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 = '<option value="">无版本</option>';
sel.disabled = true;
} else {
sel.innerHTML = '<option value="">\u27a4 使用最新版本</option>' +
list.map(function (v) {
var selected = (v.id === versionId) ? ' selected' : '';
return '<option value="' + v.id + '"' + selected + '>' + Core.esc(v.versionDesc || v.id) + '</option>';
}).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 = '<option value="">无技巧</option>';
sel.disabled = true;
} else {
sel.innerHTML = '<option value="">\u27a4 使用最新技巧</option>' +
list.map(function (s) {
var selected = (s.id === skillId) ? ' selected' : '';
return '<option value="' + s.id + '"' + selected + '>' + Core.esc(s.desc || s.id) + '</option>';
}).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: '<div style="font-size:13.5px;line-height:1.9;padding:4px 0">' +
'<div style="display:flex;align-items:flex-start;gap:10px">' +
'<span style="font-size:22px;line-height:1">\u26a0</span>' +
'<div>' + Core.esc(msg) + '</div>' +
'</div></div>',
footer: '<button class="btn ghost hw-cancel">\u53d6\u6d88</button><button class="btn primary hw-go">\u53bb\u7ed1\u5b9a</button>'
});
$('.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", "新增版本") +
'<div id="ver-list"></div>';
if (!state.data.advicerId) {
loadAdvicerOptions($("<select>"), null).then(function (advicers) {
if (advicers.length) { state.data.advicerId = advicers[0].advicer_id; loadVersions(); }
else { $("#ver-list").innerHTML = '<div class="empty"><div class="big">\u2726</div>请先新增销售人员</div>'; }
});
} else {
loadVersions();
}
$("#ver-add").onclick = function () { versionForm(null); };
}
function loadVersions() {
var box = $("#ver-list");
if (!box) return;
box.innerHTML = '<div class="empty"><span class="spin"></span></div>';
Core.api("advicer/version/list", { advicerId: state.data.advicerId || 0 }).then(function (list) {
list = list || [];
if (!list.length) {
box.innerHTML = '<div class="empty"><div class="big">✦</div>暂无版本,可手动新增或 AI 生成</div>';
return;
}
box.innerHTML = '<div class="table-wrap"><table class="table"><thead><tr>' +
"<th>ID</th><th>版本描述</th><th>销售</th><th>维度数</th><th>更新时间</th><th>操作</th></tr></thead><tbody>" +
list.map(function (v) {
var dimCount = v.data ? Object.keys(v.data).length : 0;
return "<tr>" +
'<td class="mono fs-12">' + Core.esc(String(v.id).slice(-8)) + "</td>" +
'<td><span class="name">' + Core.esc(v.versionDesc || "-") + "</span></td>" +
'<td class="mono">#' + v.advicerId + "</td>" +
"<td>" + dimCount + " 项</td>" +
'<td class="fs-12">' + Core.fmtTime(v.lastUpdateTime) + "</td>" +
'<td class="ops"><button class="btn ghost sm ver-edit" data-id="' + v.id + '">编辑</button>' +
'<button class="btn danger sm ver-del" data-id="' + v.id + '">删除</button></td></tr>';
}).join("") + "</tbody></table></div>";
$$(".ver-edit").forEach(function (b) {
b.onclick = function () {
var item = list.filter(function (x) { return String(x.id) === b.dataset.id; })[0];
versionForm(item);
};
});
$$(".ver-del").forEach(function (b) {
b.onclick = function () {
Core.confirm("确定删除该销售版本?").then(function (ok) {
if (!ok) return;
Core.api("advicer/version/del", { id: b.dataset.id })
.then(function () { Core.toast("已删除", "ok"); loadVersions(); })
.catch(function (e) { Core.toast(e.message, "err"); });
});
};
});
}).catch(function (e) {
box.innerHTML = '<div class="empty"><div class="big">⚠</div>' + Core.esc(e.message) + "</div>";
});
}
/** 版本表单(可视化编辑器) */
function versionForm(item) {
var d = item || {};
var isEdit = !!d.id;
// 从 data 字段读取维度数据
var dimData = d.data || {};
var m = Core.modal({
title: isEdit ? "编辑销售版本" : "新增销售版本",
width: "wide",
body:
'<div class="grid cols-2">' +
'<div class="field"><label class="label">销售<span class="req">*</span></label><select id="vf-advicer" class="select"></select></div>' +
'<div class="field"><label class="label">版本描述</label><input id="vf-desc" class="input" value="' + Core.esc(d.versionDesc || "") + '" placeholder="如:2026-09 风格样本"></div>' +
"</div>" +
'<div class="row mb-2"><span class="spacer"></span>' +
'<button class="btn ghost sm" id="vf-ai-gen">\u2726 AI 生成风格</button>' +
'<button class="btn ghost sm" id="vf-add-dim" style="margin-left:8px">+ 新增维度</button></div>' +
'<div id="vf-dims"></div>',
footer: '<button class="btn ghost mf-cancel">取消</button><button class="btn primary mf-save">保存</button>'
});
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 =
'<div class="pji-section-head">' +
'<span class="pji-dim-name" style="font-weight:600">' + Core.esc(item.key) + '</span>' +
'<span class="badge blue fs-11">' + typeLabel + '</span>' +
'<span class="spacer"></span>' +
'<button class="btn danger sm vf-dim-del" title="删除维度">\u2715</button></div>' +
'<div class="pji-section-body">' + sectionContentHtml(item.type, item.content) + '</div>';
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 = '<input class="input sm dr-example-item" placeholder="条目 ' + (idx + 1) + '" value="">' +
'<button type="button" class="btn danger sm dr-example-del" title="删除">\u2715</button>';
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 = '<div class="dr-list-entry-head"><span class="dr-list-entry-idx">#' + (idx + 1) + '</span>' +
'<button type="button" class="btn danger sm dr-list-entry-del" title="删除整条">\u2715</button></div>' +
'<button type="button" class="btn ghost sm dr-kv-add">+ 添加字段</button>';
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 '<option value="' + Core.esc(name) + '"' + disabled + '>' + Core.esc(name) + suffix + '</option>';
}).join('');
var typeOpts = Object.keys(VER_TYPE_LABELS).map(function (t) {
return '<option value="' + t + '">' + VER_TYPE_LABELS[t] + '</option>';
}).join('');
var m = Core.modal({
title: '新增维度',
body:
'<div class="field"><label class="label">维度名称</label>' +
'<select id="vfd-name" class="select">' + presetOpts + '<option value="__custom__">自定义...</option></select></div>' +
'<div class="field" id="vfd-custom-wrap" style="display:none"><label class="label">自定义名称</label>' +
'<input id="vfd-custom" class="input" placeholder="输入维度名称"></div>' +
'<div class="field"><label class="label">类型</label><select id="vfd-type" class="select">' + typeOpts + '</select></div>',
footer: '<button class="btn ghost mf-cancel">取消</button><button class="btn primary mf-ok">确定</button>'
});
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 '<option value="' + s.sup_id + '"' + selected + '>#' + s.sup_id + " " + Core.esc(s.sup_name) + " \u00b7 " + Core.esc(s.json_model || "") + "</option>";
}).join("");
var m = Core.modal({
title: "\u2726 AI 生成销售风格",
width: "wide",
body:
'<div class="card-sub" style="margin-bottom:12px">AI 将根据项目信息和销售背景,自动生成 7 个风格维度。</div>' +
'<div class="field"><label class="label">模型配置<span class="req">*</span></label><select id="vag-sup" class="select">' + options + '</select></div>' +
'<div class="field"><label class="label">一句话描述她/他 <span class="c-dim fs-12">(可选,作为 AI 生成参考)</span></label>' +
'<textarea id="vag-desc" class="textarea" style="min-height:60px" placeholder="如:她性格开朗,说话直爽,爱用比喻,擅长用案例说服客户…"></textarea></div>' +
'<div class="field"><label class="label">生成结果 <span class="c-dim fs-12">(预览)</span></label>' +
'<textarea id="vag-result" class="textarea json" style="min-height:280px" placeholder="点击「开始生成」"></textarea></div>',
footer: '<button class="btn ghost mf-close">关闭</button>' +
'<button class="btn primary mf-gen">\u27f3 开始生成</button>' +
'<button class="btn success mf-apply" disabled>\u2714 应用到表单</button>'
});
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", "新增技巧") +
'<div class="row mb-2"><span class="spacer"></span><button class="btn ghost sm" id="skl-ai-gen">\u2726 AI 生成</button></div>' +
'<div id="skl-list"></div>';
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 = '<div class="empty"><span class="spin"></span></div>';
Core.api("skill/list", { projectId: state.project.projectId }).then(function (list) {
list = list || [];
if (!list.length) {
box.innerHTML = '<div class="empty"><div class="big">\u2726</div>暂无聊天技巧,可手动新增、AI 生成或从「数据分析」保存</div>';
return;
}
box.innerHTML = '<div class="table-wrap"><table class="table"><thead><tr>' +
"<th>ID</th><th>描述</th><th>技巧项</th><th>更新时间</th><th>操作</th></tr></thead><tbody>" +
list.map(function (s) {
var dimData = extractSkillData(s);
var filled = Object.keys(dimData).length;
return "<tr>" +
'<td class="mono fs-12">' + Core.esc(String(s.id).slice(-8)) + "</td>" +
'<td><span class="name">' + Core.esc(s.desc || "-") + "</span></td>" +
'<td><span class="badge ' + (filled >= 3 ? "green" : filled > 0 ? "yellow" : "gray") + '">' + filled + "/5</span></td>" +
'<td class="fs-12">' + Core.fmtTime(s.lastUpdateTime) + "</td>" +
'<td class="ops"><button class="btn ghost sm skl-edit" data-id="' + s.id + '">编辑</button>' +
'<button class="btn danger sm skl-del" data-id="' + s.id + '">删除</button></td></tr>';
}).join("") + "</tbody></table></div>";
$$(".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 = '<div class="empty"><div class="big">\u26a0</div>' + Core.esc(e.message) + "</div>";
});
}
function skillForm(item) {
var d = item || {};
var isEdit = !!d.id;
var m = Core.modal({
title: isEdit ? "编辑聊天技巧" : "新增聊天技巧",
width: "wide",
body:
'<div class="row mb-2"><div class="field" style="flex:1"><label class="label">描述</label><input id="skf-desc" class="input" value="' + Core.esc(d.desc || "") + '" placeholder="如:成交客户跟进技巧"></div>' +
'<span class="spacer"></span><button class="btn ghost sm" id="skf-ai-gen" style="margin-top:20px">\u2726 AI 生成</button></div>' +
'<div id="skf-dims"></div>',
footer: '<button class="btn ghost mf-cancel">取消</button><button class="btn primary mf-save">保存</button>'
});
$(".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 '<option value="' + s.sup_id + '"' + selected + '>#' + s.sup_id + " \u00b7 " + Core.esc(s.sup_name) + " \u00b7 " + Core.esc(s.json_model || "") + "</option>";
}).join("");
var m = Core.modal({
title: "\u2726 AI 生成聊天技巧",
width: "wide",
body:
'<div class="card-sub" style="margin-top:0">AI 将根据项目信息和项目资料,自动生成 5 个维度的聊天技巧。若项目资料为空,将仅基于项目基础信息生成。</div>' +
'<div class="field"><label class="label">模型配置<span class="req">*</span></label><select id="sg-sup" class="select">' + options + "</select></div>" +
'<div class="field"><label class="label">生成结果 <span class="c-dim fs-12">(预览)</span></label>' +
'<textarea id="sg-result" class="textarea json" style="min-height:280px" placeholder="点击「开始生成」"></textarea></div>',
footer: '<button class="btn ghost mf-close">关闭</button>' +
'<button class="btn primary mf-gen">\u27f3 开始生成</button>' +
'<button class="btn success mf-apply" disabled>\u2714 应用到表单</button>'
});
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) {
body.innerHTML =
dataHead("客户", "客户画像(身份/目的/需求/顾虑/决策)", "cli-add", "新增客户") +
'<div id="cli-list"></div>';
$("#cli-add").onclick = function () { clientForm(null); };
loadClients();
}
function loadClients() {
var box = $("#cli-list");
if (!box) return;
box.innerHTML = '<div class="empty"><span class="spin"></span></div>';
Core.api("client/list", { projectId: state.project.projectId }).then(function (list) {
list = list || [];
state.data.clients = list;
if (!list.length) {
box.innerHTML = '<div class="empty"><div class="big">◉</div>暂无客户,可手动新增或从「数据分析 → 客户维度」保存</div>';
return;
}
box.innerHTML = '<div class="table-wrap"><table class="table"><thead><tr>' +
"<th>ID</th><th>姓名</th><th>客户等级</th><th>微信</th><th>预算</th><th>偏好</th><th>销售</th><th>操作</th></tr></thead><tbody>" +
list.map(function (c) {
var pi = c.personalInfo || {};
var cd = c.coreDemands || {};
var lv = c.clientLevel || "";
var lvDesc = CLIENT_LEVELS[lv] || "未评估";
var lvCls = CLIENT_LEVEL_CLS[lv] || "gray";
var lvTip = c.levelReason || "暂无评估理由(每晚 0 点自动评估)";
if (!isZeroTime(c.levelUpdateAt)) lvTip += "\n更新:" + Core.fmtTime(c.levelUpdateAt);
var wxCell = c.wxid
? '<span class="mono fs-12">' + Core.esc(c.wxid) + "</span>"
: '<button class="btn ghost sm cli-bind" data-id="' + c.id + '" data-name="' + Core.esc(pi.name || "") + '">绑定微信</button>';
return "<tr>" +
'<td class="mono fs-12">' + Core.esc(String(c.id).slice(-8)) + "</td>" +
'<td><span class="name">' + Core.esc(pi.name || "-") + "</span>" +
(pi.gender ? '<span class="fs-12 c-dim" style="margin-left:6px">' + Core.esc(pi.gender) + "</span>" : "") + "</td>" +
'<td><span class="badge ' + lvCls + '" title="' + Core.esc(lvTip) + '">' + Core.esc(lvDesc) + "</span></td>" +
"<td>" + wxCell + "</td>" +
'<td class="fs-12">' + Core.esc(cd.totalBudget || "-") + "</td>" +
'<td class="fs-12 cell-clip">' + Core.esc(cd.preferredLayout || "-") + "</td>" +
'<td class="mono">#' + c.advicerId + "</td>" +
'<td class="ops"><button class="btn ghost sm cli-edit" data-id="' + c.id + '">编辑</button>' +
'<button class="btn ghost sm cli-eval" data-id="' + c.id + '" data-name="' + Core.esc(pi.name || "") + '">评估</button>' +
(c.wxid ? '<button class="btn ghost sm cli-iterate" data-id="' + c.id + '" data-name="' + Core.esc(pi.name || "") + '">迭代</button>' : "") +
'<button class="btn danger sm cli-del" data-id="' + c.id + '">删除</button></td></tr>';
}).join("") + "</tbody></table></div>";
$$(".cli-edit").forEach(function (b) {
b.onclick = function () {
var item = list.filter(function (x) { return String(x.id) === b.dataset.id; })[0];
clientForm(item);
};
});
$$(".cli-del").forEach(function (b) {
b.onclick = function () {
Core.confirm("确定删除该客户?").then(function (ok) {
if (!ok) return;
Core.api("client/del", { id: b.dataset.id })
.then(function () { Core.toast("已删除", "ok"); loadClients(); })
.catch(function (e) { Core.toast(e.message, "err"); });
});
};
});
$$(".cli-bind").forEach(function (b) {
b.onclick = function () { bindClientDialog(b.dataset.id, b.dataset.name); };
});
$$(".cli-eval").forEach(function (b) {
b.onclick = function () { evaluateClient(b.dataset.id, b.dataset.name, b); };
});
$$(".cli-iterate").forEach(function (b) {
b.onclick = function () { iterateClient(b.dataset.id, b.dataset.name, b); };
});
}).catch(function (e) {
box.innerHTML = '<div class="empty"><div class="big">⚠</div>' + Core.esc(e.message) + "</div>";
});
}
/* ---------- 客户等级常量 / 微信绑定 / 等级评估 ---------- */
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 bindClientDialog(id, name) {
var m = Core.modal({
title: "绑定微信" + (name ? " · " + name : ""),
body:
'<div class="field"><label class="label">客户微信 wxid<span class="req">*</span></label>' +
'<input id="cb-wxid" class="input mono" placeholder="如:wxid_xxxxxxxx"></div>' +
'<div class="field"><label class="label">登录实例 appId(可选)</label>' +
'<input id="cb-appid" class="input mono" placeholder="多实例时指定,留空=默认实例"></div>' +
'<div class="fs-12 c-dim" style="line-height:1.9">绑定后该微信发来的消息自动关联本客户;客户画像迭代、每晚等级评估与 AI 主动对话均依赖绑定关系。</div>',
footer: '<button class="btn ghost mf-cancel">取消</button><button class="btn primary mf-save">绑定</button>'
});
$(".mf-cancel", m.root).onclick = m.close;
$(".mf-save", m.root).onclick = function () {
var wxid = $("#cb-wxid", m.root).value.trim();
if (!wxid) { Core.toast("请输入 wxid", "err"); return; }
Core.api("client/bind", { id: id, wxid: wxid, appId: $("#cb-appid", m.root).value.trim() })
.then(function () { m.close(); Core.toast("绑定成功", "ok"); loadClients(); })
.catch(function (e) { Core.toast(e.message, "err"); });
};
}
function evaluateClient(id, name, btn) {
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");
loadClients();
})
.catch(function (e) {
if (btn) { btn.textContent = oldText; btn.disabled = false; }
Core.toast(e.message, "err");
});
});
}
function iterateClient(id, name, btn) {
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");
loadClients();
})
.catch(function (e) {
if (btn) { btn.textContent = oldText; btn.disabled = false; }
Core.toast(e.message, "err");
});
});
}
function clientForm(item) {
var d = item || {};
var isEdit = !!d.id;
var pi = d.personalInfo || {};
var pp = d.purchasePurpose || {};
var cd = d.coreDemands || {};
var concerns = (d.concerns || []).join("\n");
var decisions = (d.decisionProfile || []).join("\n");
var m = Core.modal({
title: isEdit ? "编辑客户" : "新增客户",
width: "wide",
body:
'<div class="grid cols-3">' +
'<div class="field"><label class="label">姓氏/称呼</label><input id="cf-name" class="input" value="' + Core.esc(pi.name || "") + '"></div>' +
'<div class="field"><label class="label">性别</label><input id="cf-gender" class="input" value="' + Core.esc(pi.gender || "") + '" placeholder="男/女"></div>' +
'<div class="field"><label class="label">来源地</label><input id="cf-loc" class="input" value="' + Core.esc(pi.location || "") + '"></div>' +
"</div>" +
'<div class="grid cols-3">' +
'<div class="field"><label class="label">家庭结构</label><input id="cf-family" class="input" value="' + Core.esc(pi.familyOrganize || "") + '" placeholder="如:夫妻+1孩"></div>' +
'<div class="field"><label class="label">\u751f\u65e5\uff08\u7528\u4e8e\u751f\u65e5\u5173\u6000\uff09</label><input id="cf-birthday" class="input dp-date" placeholder="\u70b9\u51fb\u9009\u62e9\u65e5\u671f" value="' + Core.esc(pi.birthday || "") + '"></div>' +
'<div class="field" style="display:flex;align-items:flex-end"><label class="checkbox-row"><input type="checkbox" id="cf-first" ' + (pi.isFirstHome ? "checked" : "") + "> 首次置业</label></div>" +
"</div>" +
'<div class="grid cols-3">' +
'<div class="field"><label class="label">主要目的</label><input id="cf-p1" class="input" value="' + Core.esc(pp.primaryPurpose || "") + '"></div>' +
'<div class="field"><label class="label">次要目的</label><input id="cf-p2" class="input" value="' + Core.esc(pp.secondaryPurpose || "") + '"></div>' +
'<div class="field"><label class="label">决策人</label><input id="cf-dm" class="input" value="' + Core.esc(pp.decisionMakers || "") + '"></div>' +
"</div>" +
'<div class="grid cols-3">' +
'<div class="field"><label class="label">预算范围</label><input id="cf-budget" class="input" value="' + Core.esc(cd.totalBudget || "") + '" placeholder="如:350-400万"></div>' +
'<div class="field"><label class="label">偏好户型</label><input id="cf-layout" class="input" value="' + Core.esc(cd.preferredLayout || "") + '"></div>' +
'<div class="field"><label class="label">核心诉求</label><input id="cf-appeal" class="input" value="' + Core.esc(cd.coreAppeal || "") + '"></div>' +
"</div>" +
'<div class="grid cols-2">' +
'<div class="field"><label class="label">顾虑(每行一条)</label><textarea id="cf-concerns" class="textarea" style="min-height:70px">' + Core.esc(concerns) + "</textarea></div>" +
'<div class="field"><label class="label">决策建议(每行一条)</label><textarea id="cf-decisions" class="textarea" style="min-height:70px">' + Core.esc(decisions) + "</textarea></div>" +
"</div>" +
'<div class="field"><label class="label">归属销售<span class="req">*</span></label><select id="cf-advicer" class="select"></select></div>',
footer: '<button class="btn ghost mf-cancel">取消</button><button class="btn primary mf-save">保存</button>'
});
loadAdvicerOptions($("#cf-advicer", m.root), d.advicerId || state.data.advicerId);
initAllDatePickers(m.root);
$(".mf-cancel", m.root).onclick = m.close;
$(".mf-save", m.root).onclick = function () {
var advicerId = Number($("#cf-advicer", m.root).value);
if (!advicerId) { Core.toast("请选择归属销售", "err"); return; }
var toLines = function (id) {
return $(id, m.root).value.split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s; });
};
var payload = {
projectId: state.project.projectId,
advicerId: advicerId,
personalInfo: {
name: $("#cf-name", m.root).value.trim(),
gender: $("#cf-gender", m.root).value.trim(),
location: $("#cf-loc", m.root).value.trim(),
isFirstHome: $("#cf-first", m.root).checked,
familyOrganize: $("#cf-family", m.root).value.trim(),
birthday: $("#cf-birthday", m.root).value
},
purchasePurpose: {
primaryPurpose: $("#cf-p1", m.root).value.trim(),
secondaryPurpose: $("#cf-p2", m.root).value.trim(),
decisionMakers: $("#cf-dm", m.root).value.trim()
},
coreDemands: {
totalBudget: $("#cf-budget", m.root).value.trim(),
preferredLayout: $("#cf-layout", m.root).value.trim(),
coreAppeal: $("#cf-appeal", m.root).value.trim()
},
concerns: toLines("#cf-concerns"),
decisionProfile: toLines("#cf-decisions")
};
if (isEdit) payload.id = d.id;
Core.api(isEdit ? "client/update" : "client/add", payload)
.then(function () { m.close(); Core.toast("保存成功", "ok"); loadClients(); })
.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: '<div style="font-size:13.5px;line-height:1.9;padding:4px 0">' +
'<div style="display:flex;align-items:flex-start;gap:10px">' +
'<span style="font-size:22px;line-height:1">⚠</span>' +
'<div>当前项目尚未配置 <strong style="color:var(--accent)">X-finder-TOKEN</strong>,' +
'请先在项目设置中完成微信接入配置,方可使用微信工作台功能。</div>' +
'</div></div>',
footer: '<button class="btn ghost cm-cancel">取消</button><button class="btn primary cm-go">前往设置</button>'
});
$('.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');
}