1250 lines
58 KiB
JavaScript
1250 lines
58 KiB
JavaScript
/* ==================== 视图四:数据中心 ==================== */
|
||
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');
|
||
}
|
||
|