711 lines
37 KiB
JavaScript
711 lines
37 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></tr></thead><tbody>" +
|
||
list.map(function (a) {
|
||
var gender = a.gender === 1 ? "男" : a.gender === 2 ? "女" : "-";
|
||
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 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);
|
||
};
|
||
});
|
||
}).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"); });
|
||
};
|
||
}
|
||
|
||
/* ---------- Tab 2:销售版本 ---------- */
|
||
var JSON_TPL = {
|
||
dialectFeatures: '{\n "region": "",\n "intensity": 0,\n "keyWords": []\n}',
|
||
sentencePatterns: '{\n "openingMode": [],\n "explanationMode": [],\n "confirmationMode": [],\n "summaryMode": [],\n "transitionMode": []\n}',
|
||
personalityTags: "[]",
|
||
toneTags: '{\n "enthusiasm": 0,\n "patience": 0,\n "confidence": 0,\n "friendliness": 0,\n "persuasion": 0\n}',
|
||
signatureDialogues: "[]",
|
||
needsMining: "{}",
|
||
painPointResponse: "{}",
|
||
valueBuilding: "{}",
|
||
closingTechniques: "{}",
|
||
communicationRhythm: "{}",
|
||
regionValue: "{}",
|
||
competitionComparison: "{}",
|
||
coreSellingPoints: "{}",
|
||
supportingFacilities: "{}",
|
||
developerBacking: "{}",
|
||
houseTypes: "[]"
|
||
};
|
||
|
||
function vJsonField(label, key, value) {
|
||
return '<div class="field"><label class="label">' + label + '</label>' +
|
||
'<textarea class="textarea json vf-json" data-key="' + key + '" style="min-height:92px">' + Core.esc(value) + "</textarea></div>";
|
||
}
|
||
|
||
function parseVfJson(root, labelMap) {
|
||
var out = {};
|
||
var tplKeys = Object.keys(labelMap);
|
||
$$(".vf-json", root).forEach(function (ta) {
|
||
var key = ta.dataset.key;
|
||
out[key] = Core.parseJsonField(ta.value, labelMap[key] || key);
|
||
tplKeys = tplKeys.filter(function (k) { return k !== key; });
|
||
});
|
||
// 未渲染的字段填默认模板
|
||
tplKeys.forEach(function (k) { out[k] = JSON.parse(JSON_TPL[k] || "{}"); });
|
||
return out;
|
||
}
|
||
|
||
function renderTabVersions(body) {
|
||
var advicerInfo = state.data.advicerId ? ' · 销售 #' + state.data.advicerId : '';
|
||
body.innerHTML =
|
||
dataHead("数字人管理" + advicerInfo, "基于聊天分析沉淀的销售个人风格(方言/句式/语气/对话)", "ver-add", "新增版本") +
|
||
'<div id="ver-list"></div>';
|
||
// 销售版本与当前销售绑定,自动使用销售人员 tab 选中的销售
|
||
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>暂无版本,可手动新增或从「数据分析」保存</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 dialect = (v.dialectFeatures && v.dialectFeatures.region) || "-";
|
||
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>" + Core.esc(dialect) + "</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>";
|
||
});
|
||
}
|
||
|
||
var VERSION_LABELS = { dialectFeatures: "方言特征", sentencePatterns: "句式模式", personalityTags: "个性标签", toneTags: "语气标签", signatureDialogues: "标志性对话" };
|
||
|
||
function versionForm(item) {
|
||
var d = item || {};
|
||
var isEdit = !!d.id;
|
||
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>" +
|
||
vJsonField("方言特征 dialectFeatures", "dialectFeatures", d.dialectFeatures ? Core.prettyJson(d.dialectFeatures) : JSON_TPL.dialectFeatures) +
|
||
vJsonField("句式模式 sentencePatterns", "sentencePatterns", d.sentencePatterns ? Core.prettyJson(d.sentencePatterns) : JSON_TPL.sentencePatterns) +
|
||
'<div class="grid cols-2">' +
|
||
vJsonField("个性标签 personalityTags", "personalityTags", d.personalityTags ? Core.prettyJson(d.personalityTags) : JSON_TPL.personalityTags) +
|
||
vJsonField("语气标签 toneTags", "toneTags", d.toneTags ? Core.prettyJson(d.toneTags) : JSON_TPL.toneTags) +
|
||
"</div>" +
|
||
vJsonField("标志性对话 signatureDialogues", "signatureDialogues", d.signatureDialogues ? Core.prettyJson(d.signatureDialogues) : JSON_TPL.signatureDialogues),
|
||
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;
|
||
$(".mf-save", m.root).onclick = function () {
|
||
var advicerId = Number($("#vf-advicer", m.root).value);
|
||
if (!advicerId) { Core.toast("请选择销售", "err"); return; }
|
||
var payload;
|
||
try { payload = parseVfJson(m.root, VERSION_LABELS); }
|
||
catch (e) { Core.toast(e.message, "err"); return; }
|
||
payload.advicerId = advicerId;
|
||
payload.versionDesc = $("#vf-desc", m.root).value.trim();
|
||
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"); });
|
||
};
|
||
}
|
||
|
||
/* ---------- Tab 3:聊天技巧 ---------- */
|
||
var SKILL_LABELS = { needsMining: "需求挖掘", painPointResponse: "痛点应对", valueBuilding: "价值塑造", closingTechniques: "促单话术", communicationRhythm: "沟通节奏" };
|
||
|
||
function renderTabSkills(body) {
|
||
body.innerHTML =
|
||
dataHead("聊天技巧", "需求挖掘 / 痛点应对 / 价值塑造 / 促单 / 沟通节奏", "skl-add", "新增技巧") +
|
||
'<div class="row mb-2"><span class="fs-12 c-dim">按销售筛选:</span><div style="width:260px"><select id="skl-filter" class="select sm"></select></div></div>' +
|
||
'<div id="skl-list"></div>';
|
||
loadAdvicerOptions($("#skl-filter"), state.data.advicerId || null).then(function (advicers) {
|
||
if (advicers.length && !state.data.advicerId) state.data.advicerId = advicers[0].advicer_id;
|
||
$("#skl-filter").value = String(state.data.advicerId || "");
|
||
loadSkills();
|
||
});
|
||
$("#skl-filter").onchange = function () { state.data.advicerId = Number(this.value) || 0; loadSkills(); };
|
||
$("#skl-add").onclick = function () { skillForm(null); };
|
||
}
|
||
|
||
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, advicerId: state.data.advicerId || 0 }).then(function (list) {
|
||
list = 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></tr></thead><tbody>" +
|
||
list.map(function (s) {
|
||
var filled = [s.needsMining, s.painPointResponse, s.valueBuilding, s.closingTechniques, s.communicationRhythm].filter(function (x) {
|
||
return x && Object.keys(x).length;
|
||
}).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 class="mono">#' + s.advicerId + "</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">⚠</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="grid cols-2">' +
|
||
'<div class="field"><label class="label">销售<span class="req">*</span></label><select id="skf-advicer" class="select"></select></div>' +
|
||
'<div class="field"><label class="label">描述</label><input id="skf-desc" class="input" value="' + Core.esc(d.desc || "") + '" placeholder="如:成交客户跟进技巧"></div>' +
|
||
"</div>" +
|
||
vJsonField("需求挖掘 needsMining", "needsMining", d.needsMining ? Core.prettyJson(d.needsMining) : JSON_TPL.needsMining) +
|
||
vJsonField("痛点应对 painPointResponse", "painPointResponse", d.painPointResponse ? Core.prettyJson(d.painPointResponse) : JSON_TPL.painPointResponse) +
|
||
vJsonField("价值塑造 valueBuilding", "valueBuilding", d.valueBuilding ? Core.prettyJson(d.valueBuilding) : JSON_TPL.valueBuilding) +
|
||
vJsonField("促单话术 closingTechniques", "closingTechniques", d.closingTechniques ? Core.prettyJson(d.closingTechniques) : JSON_TPL.closingTechniques) +
|
||
vJsonField("沟通节奏 communicationRhythm", "communicationRhythm", d.communicationRhythm ? Core.prettyJson(d.communicationRhythm) : JSON_TPL.communicationRhythm),
|
||
footer: '<button class="btn ghost mf-cancel">取消</button><button class="btn primary mf-save">保存</button>'
|
||
});
|
||
loadAdvicerOptions($("#skf-advicer", m.root), d.advicerId || state.data.advicerId);
|
||
$(".mf-cancel", m.root).onclick = m.close;
|
||
$(".mf-save", m.root).onclick = function () {
|
||
var advicerId = Number($("#skf-advicer", m.root).value);
|
||
if (!advicerId) { Core.toast("请选择销售", "err"); return; }
|
||
var payload;
|
||
try { payload = parseVfJson(m.root, SKILL_LABELS); }
|
||
catch (e) { Core.toast(e.message, "err"); return; }
|
||
payload.projectId = state.project.projectId;
|
||
payload.advicerId = advicerId;
|
||
payload.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"); });
|
||
};
|
||
}
|
||
|
||
/* ---------- 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"); });
|
||
};
|
||
}
|
||
|
||
/* ---------- 销售人员微信工作台入口 ---------- */
|
||
/**
|
||
* 打开销售人员的微信工作台(新流程)
|
||
* 1. 检测项目 wx_token → 未配置则跳转项目设置
|
||
* 2. 检测销售 wx_device_id → 未配置则弹窗填写
|
||
* 3. 两者都配置后进入工作台
|
||
*/
|
||
function openAdvicerWxWorkbench(advicerId, advicerName, wxDeviceId) {
|
||
// 1. 检查项目级 wx_token
|
||
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();
|
||
// 设置标记,renderProjectDetailForm 渲染完成后自动切到设置 tab
|
||
state._pendingSettingsTab = true;
|
||
switchView('projectDetail');
|
||
};
|
||
return;
|
||
}
|
||
// 2. 检查销售 wx_device_id
|
||
if (!wxDeviceId) {
|
||
var m = Core.modal({
|
||
title: '设置微信设备 ID · ' + Core.esc(advicerName),
|
||
body:
|
||
'<div class="field">' +
|
||
'<label class="label">微信设备 ID<span class="req">*</span></label>' +
|
||
'<input id="awx-device" class="input mono" placeholder="请输入该销售的微信设备标识">' +
|
||
'</div>' +
|
||
'<div class="fs-12 c-dim" style="line-height:1.8">' +
|
||
'每个销售人员需配置独立的微信设备 ID,配置后方可进入微信工作台。' +
|
||
'</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 deviceId = $('#awx-device', m.root).value.trim();
|
||
if (!deviceId) { Core.toast('请输入微信设备 ID', 'err'); return; }
|
||
Core.api('advicer/wx_device/update', { advicerId: advicerId, wxDeviceId: deviceId })
|
||
.then(function () {
|
||
m.close();
|
||
Core.toast('设备 ID 已保存', 'ok');
|
||
enterWxWorkbench(advicerId, advicerName, deviceId);
|
||
})
|
||
.catch(function (e) { Core.toast(e.message, 'err'); });
|
||
};
|
||
return;
|
||
}
|
||
// 3. 两者都已配置,直接进入工作台(在线检测由工作台 mount 负责)
|
||
enterWxWorkbench(advicerId, advicerName);
|
||
};
|
||
// 如果 state.detail 尚未加载,先加载项目信息
|
||
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');
|
||
}
|
||
|