ai_scheduler/web/assets/js/project_data.js

711 lines
37 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></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');
}