ai_scheduler/web/assets/js/project_data.js

1624 lines
79 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", "客户"],
["leads", "成交线索"],
["reminds", "定时提醒"]
];
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: "客户管理", leads: "成交线索", reminds: "定时提醒" };
$("#pageTitle").textContent = tabMeta[st.tab] || "数据中心";
$("#pageSub").textContent = "管理当前项目下的" + (tabMeta[st.tab] || "数据");
}
renderDataBody();
};
});
renderDataBody();
}
function renderDataBody() {
console.log("[renderDataBody] tab=" + state.data.tab);
var body = $("#data-body");
if (!body) { console.error("[renderDataBody] #data-body not found!"); return; }
body.innerHTML = '<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);
if (tab === "leads") return renderTabLeads(body);
if (tab === "reminds") return renderTabReminds(body);
console.warn("[renderDataBody] unknown tab: " + tab);
}
function dataHead(title, sub, addBtnId, addBtnText) {
return '<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) {
// 客户 tab 现仅展示「托管客户」;客户画像改为在托管客户行内「查看画像」按钮进入
body.innerHTML =
'<div class="row mb-2"><span class="fs-13 c-main" style="font-weight:700">托管客户</span>' +
'<span class="fs-12 c-dim">处于托管标签分组内的微信好友;点击「查看画像」查看并维护该客户的 AI 客户画像</span></div>' +
'<div id="cli-list"></div>';
loadHostedCustomers(body.querySelector("#cli-list"));
}
/* ---------- 查看/维护托管客户的 AI 客户画像(client 表按 wxid 关联,动态栏目编辑器) ---------- */
function viewClientProfile(c) {
if (!c) return;
var wxid = c.user_name || "";
var name = c.remark || c.nick_name || c.user_name || "客户";
var appId = c.advicer_wx_device_id || "";
var advicerId = c.advicer_id || 0;
var advicerName = c.advicer_name || "";
if (!wxid) { Core.toast("该客户缺少微信ID,无法关联画像", "err"); return; }
Core.api("client/list", { projectId: state.project.projectId }).then(function (list) {
list = list || [];
var profile = list.filter(function (x) { return x.wxid === wxid; })[0] || null;
if (profile) { clientProfileForm(profile, { name: name, wxid: wxid, appId: appId, advicerId: advicerId, advicerName: advicerName }); return; }
Core.confirm("客户「" + name + "」暂无 AI 画像,是否现在手动新建一份?(也可直接点「画像更新」由聊天记录自动生成)").then(function (ok) {
if (!ok) return;
clientProfileForm(null, { name: name, wxid: wxid, appId: appId, advicerId: advicerId, advicerName: advicerName });
});
}).catch(function (e) { Core.toast(e.message, "err"); });
}
/* ---------- 客户等级常量 / 微信绑定 / 等级评估 ---------- */
/* ---------- 托管客户列表 ---------- */
function loadHostedCustomers(box) {
if (!box) box = document.querySelector("#cli-list");
if (!box) return;
box.innerHTML = '<div class="empty"><span class="spin"></span></div>';
var pid = state.project.projectId;
Promise.all([
Core.api("advicer/customer/hosted", { projectId: pid }),
Core.api("client/list", { projectId: pid }).catch(function () { return []; })
]).then(function (arr) {
var list = arr[0] || [];
// 客户画像按 wxid 建索引,用于展示等级评估结果
var profMap = {};
(arr[1] || []).forEach(function (x) { if (x.wxid) profMap[x.wxid] = x; });
if (!list.length) {
box.innerHTML = '<div class="empty"><div class="big">\u25c9</div>\u5c1a\u65e0\u6258\u7ba1\u5ba2\u6237\uff0c\u5f00\u542f\u6258\u7ba1\u6807\u7b7e\u540e\u6d88\u606f\u56de\u8c03\u65f6\u81ea\u52a8\u540c\u6b65</div>';
return;
}
box.innerHTML = '<div class="fs-12 c-dim" style="margin-bottom:8px">\u5171 ' + list.length + ' \u4f4d\u6258\u7ba1\u5ba2\u6237\uff08\u5904\u4e8e\u6258\u7ba1\u6807\u7b7e\u5206\u7ec4\u5185\u7684\u5fae\u4fe1\u597d\u53cb\uff0c\u6bcf\u6b21\u6d88\u606f\u56de\u8c03\u65f6\u81ea\u52a8\u540c\u6b65\u6700\u65b0\u4fe1\u606f\uff09</div>' +
'<div class="table-wrap"><table class="table"><thead><tr>' +
'<th>头像</th><th>昵称</th><th>微信备注</th><th>托管标签</th><th>性别</th><th>地区</th><th>归属销售</th><th>客户等级</th><th>当前任务</th><th>会话状态</th><th>更新时间</th><th>操作</th></tr></thead><tbody>' +
list.map(function (c) {
var displayName = c.remark || c.nick_name || c.user_name || "-";
var avatar = c.big_head_img_url || c.small_head_img_url || "";
var avatarCell = avatar
? '<img src="' + Core.esc(avatar) + '" style="width:32px;height:32px;border-radius:50%;object-fit:cover" onerror="this.style.display=\'none\'">'
: '<span style="display:inline-flex;width:32px;height:32px;border-radius:50%;background:var(--surface-2);align-items:center;justify-content:center;font-size:14px;color:var(--dim)">\u25c9</span>';
var sex = c.sex === 1 ? "\u7537" : c.sex === 2 ? "\u5973" : "-";
var area = [c.province, c.city].filter(function (s) { return s; }).join(" ") || "-";
var sessionStatus = c.session_id
? '<span class="badge green">\u5df2\u6258\u7ba1</span>'
: '<span class="badge gray">\u672a\u6ce8\u518c</span>';
var labels = (c.label_names || "").split(",").filter(function (s) { return s; });
var labelHtml = labels.map(function (l) {
return '<span class="badge blue fs-11">' + Core.esc(l) + '</span>';
}).join(" ");
var prof = profMap[c.user_name];
var levelCell = '<span class="c-dim">-</span>';
if (prof) {
if (prof.clientLevel) {
var lvDesc = CLIENT_LEVELS[prof.clientLevel] || prof.clientLevel;
var lvCls = CLIENT_LEVEL_CLS[prof.clientLevel] || "gray";
var lvTip = prof.levelReason || "";
if (!isZeroTime(prof.levelUpdateAt)) lvTip += (lvTip ? "\n" : "") + "更新:" + Core.fmtTime(prof.levelUpdateAt);
levelCell = '<span class="badge ' + lvCls + '" title="' + Core.esc(lvTip) + '">' + Core.esc(lvDesc) + '</span>';
} else {
levelCell = '<span class="badge gray" title="尚未进行等级评估">未评估</span>';
}
}
// 当前任务:对话 AI 回写的简洁 mission,展示在客户列表
var missionCell = '<span class="c-dim">-</span>';
if (prof && prof.mission) {
var ms = String(prof.mission);
missionCell = '<span class="fs-12" title="' + Core.esc(ms) + '">' + Core.esc(ms.length > 14 ? ms.slice(0, 14) + "…" : ms) + '</span>';
}
return "<tr>" +
'<td>' + avatarCell + '</td>' +
'<td><span class="name">' + Core.esc(displayName) + '</span>' +
(c.user_name ? '<div class="mono fs-11 c-dim">' + Core.esc(c.user_name) + '</div>' : '') + '</td>' +
'<td class="fs-12">' + Core.esc(c.remark || "-") + '</td>' +
'<td>' + (labelHtml || "-") + '</td>' +
'<td>' + sex + '</td>' +
'<td class="fs-12">' + Core.esc(area) + '</td>' +
'<td class="fs-12">' + Core.esc(c.advicer_name || "-") + '</td>' +
'<td>' + levelCell + '</td>' +
'<td>' + missionCell + '</td>' +
'<td>' + sessionStatus + '</td>' +
'<td class="fs-12">' + Core.fmtTime(c.update_at) + '</td>' +
'<td class="ops">' +
'<button class="btn ghost sm hc-profile" data-cid="' + c.customer_id + '">查看画像</button>' +
'<button class="btn ghost sm hc-chat" data-cid="' + c.customer_id + '">\u804a\u5929\u8bb0\u5f55</button>' +
'<button class="btn ghost sm danger hc-remove" data-cid="' + c.customer_id + '">\u79fb\u9664</button>' +
'</td></tr>';
}).join("") + "</tbody></table></div>";
var byId = {};
list.forEach(function (c) { byId[c.customer_id] = c; });
$$('.hc-profile', box).forEach(function (b) { b.onclick = function () { viewClientProfile(byId[b.dataset.cid]); }; });
$$('.hc-chat', box).forEach(function (b) { b.onclick = function () { viewHostedChat(byId[b.dataset.cid]); }; });
$$('.hc-remove', box).forEach(function (b) { b.onclick = function () { removeHostedCustomer(byId[b.dataset.cid]); }; });
}).catch(function (e) {
box.innerHTML = '<div class="empty"><div class="big">\u26a0</div>' + Core.esc(e.message) + '</div>';
});
}
/* ---------- 托管客户操作:聊天记录 / 移除 ---------- */
// 将客户从托管标签中移除(微信 API + DB 同步)
function removeHostedCustomer(c) {
if (!c) return;
var name = c.remark || c.nick_name || c.user_name || "该客户";
Core.confirm("将客户「" + name + "」从托管标签中移除?移除后不再命中托管分组,也不会触发 AI 自动回复。").then(function (ok) {
if (!ok) return;
Core.api("advicer/customer/remove_label", { projectId: state.project.projectId, customerId: c.customer_id })
.then(function () { Core.toast("已从托管标签移除", "ok"); loadHostedCustomers(); })
.catch(function (e) { Core.toast(e.message, "err"); });
});
}
// 查看客户聊天记录(只读,来自微信消息流水)
function viewHostedChat(c) {
if (!c) return;
var name = c.remark || c.nick_name || c.user_name || "客户";
var m = Core.modal({
title: "聊天记录 · " + name,
width: "wide",
body: '<div id="hc-chat" style="max-height:60vh;overflow:auto;display:flex;flex-direction:column;gap:12px"><div class="empty" style="padding:20px"><span class="spin"></span></div></div>'
});
var box = $("#hc-chat", m.root);
Core.api("wx/conversation/msgs", { selfWxid: c.self_wxid, wxid: c.user_name, page_size: 200 })
.then(function (list) {
list = list || [];
if (!list.length) {
box.innerHTML = '<div class="empty"><div class="big">\u2726</div>与「' + Core.esc(name) + '」暂无聊天记录</div>';
return;
}
box.innerHTML = list.map(function (msg) {
var isSelf = msg.direction === "self" || msg.direction === "ai";
var tag = msg.direction === "ai" ? "AI" : msg.direction === "self" ? "销售" : "客户";
var avatarHtml = msg.direction === "ai"
? '<div class="msg-avatar" style="background:rgba(45,224,255,0.15)">\u2726</div>'
: '<div class="msg-avatar">' + Core.esc(Core.initial(isSelf ? tag : name)) + '</div>';
var bubbleClass = isSelf ? "msg-bubble conv-msg-self" : "msg-bubble conv-msg-other";
return '<div class="msg-row' + (isSelf ? " self" : "") + '">' + avatarHtml +
'<div><div class="' + bubbleClass + '">' + hostedChatContent(msg) + '</div>' +
'<div class="msg-time">' + tag + ' · ' + Core.esc(Core.fmtTime(msg.createAt || msg.create_at)) + '</div></div></div>';
}).join("");
box.scrollTop = box.scrollHeight;
})
.catch(function (e) { box.innerHTML = '<div class="empty"><div class="big">\u26a0</div>' + Core.esc(e.message) + '</div>'; });
}
function hostedChatContent(msg) {
var content = msg.content || "";
switch (msg.msgType) {
case "text": return Core.esc(content);
case "image": return '<span class="conv-media">\ud83d\udcf7 [图片]</span>';
case "emoji": return '<span class="conv-media">\ud83d\ude0a [表情]</span>';
case "voice": return '<span class="conv-media">\ud83c\udfa4 [语音]</span>';
case "video": return '<span class="conv-media">\ud83c\udfac [视频]</span>';
case "location": return '<span class="conv-media">\ud83d\udccd [位置]</span>';
case "namecard": return '<span class="conv-media">\ud83d\udc64 [名片]</span>';
case "app_msg": return '<span class="conv-media">\ud83d\udd17 [链接/小程序]</span>';
default: return '<span class="conv-media">[' + Core.esc(msg.msgType || "消息") + ']</span>';
}
}
var CLIENT_LEVELS = { regular: "熟客", intent: "意向客户", sleeping: "沉睡客户", non: "非客户" };
var CLIENT_LEVEL_CLS = { regular: "purple", intent: "green", sleeping: "yellow", non: "gray" };
function isZeroTime(t) {
return !t || String(t).indexOf("0001-01-01") === 0;
}
function evaluateClient(id, name, btn, onDone) {
Core.confirm("对客户「" + (name || "未命名") + "」执行等级评估?(AI 将分析近期聊天记录判定等级)").then(function (ok) {
if (!ok) return;
var oldText = btn ? btn.textContent : "";
if (btn) { btn.textContent = "评估中…"; btn.disabled = true; }
Core.api("client/evaluate", { clientId: id, force: true })
.then(function (res) {
if (btn) { btn.textContent = oldText; btn.disabled = false; }
if (res && res.async) { Core.toast(res.msg || "评估任务已启动,请稍后刷新", "ok"); return; }
var d = res && res.details && res.details[0];
Core.toast(d ? "评估完成:" + (d.levelDesc || d.level) + " · " + (d.reason || "") : "评估完成", "ok");
if (onDone) onDone();
})
.catch(function (e) {
if (btn) { btn.textContent = oldText; btn.disabled = false; }
Core.toast(e.message, "err");
});
});
}
function iterateClient(id, name, btn, onDone) {
Core.confirm("对客户「" + (name || "未命名") + "」立即执行画像迭代?(AI 将再次分析近期聊天记录,自动新建/完善客户画像)").then(function (ok) {
if (!ok) return;
var oldText = btn ? btn.textContent : "";
if (btn) { btn.textContent = "迭代中…"; btn.disabled = true; }
Core.api("smart/iterate/run", { clientId: id, force: true })
.then(function (res) {
if (btn) { btn.textContent = oldText; btn.disabled = false; }
res = res || {};
var detail = (res.details || [])[0] || "";
Core.toast("迭代完成:扫描 " + (res.scanned || 0) + " 条未分析消息·" + (detail || "画像无新增信息"), "ok");
if (onDone) onDone();
})
.catch(function (e) {
if (btn) { btn.textContent = oldText; btn.disabled = false; }
Core.toast(e.message, "err");
});
});
}
/* 客户画像动态栏目编辑器(对齐「项目资料」编辑方式);支持画像更新(旧画像+聊天记录重生成) */
function clientProfileForm(item, ctx) {
var d = item || {};
ctx = ctx || {};
var isEdit = !!d.id;
var wxid = d.wxid || ctx.wxid || "";
var appId = d.appId || ctx.appId || "";
// 归属销售:画像自动归属于客户来源的销售(编辑态取已存 advicerId,新建取来源托管客户的 advicer_id)
var advicerId = Number(d.advicerId || ctx.advicerId || 0);
// 动态栏目数据源:从画像 profile(栏目名→内容)还原
state._cpSections = buildSectionsFromData(d.profile || {});
var m = Core.modal({
title: isEdit ? ("客户画像 · " + (ctx.name || "")) : ("新建客户画像" + (ctx.name ? " · " + ctx.name : "")),
width: "wide",
body:
'<div class="fs-12 c-dim" style="margin-bottom:8px">归属销售:' + Core.esc(ctx.advicerName || (advicerId ? ("#" + advicerId) : "未关联")) + '(画像自动归属于该客户来源的销售,无需手动选择)</div>' +
'<div class="row mb-2" style="align-items:center">' +
'<span class="fs-12 c-dim">按「项目资料」方式维护客户画像栏目;后端按约定栏目名「姓名」「生日」读取,用于称呼与生日关怀。</span>' +
'<span class="spacer"></span>' +
'<button type="button" class="btn ghost sm" id="cp-add-section">+ 新增栏目</button>' +
'</div>' +
'<div id="pji-sections" style="max-height:56vh;overflow:auto"></div>',
footer:
'<button class="btn ghost cp-regen">画像更新</button>' +
(isEdit ? '<button class="btn ghost cp-eval">等级评估</button><button class="btn danger mf-del">删除画像</button>' : '') +
'<button class="btn ghost mf-cancel">取消</button><button class="btn primary mf-save">保存</button>'
});
// 将共享编辑器指向本弹窗(数据源=state._cpSections,容器=弹窗内 #pji-sections)
setPjiEditorTarget(m.root, function () { return state._cpSections; }, function (v) { state._cpSections = v; });
drawPjiSections();
var closeProfile = function () { setPjiEditorTarget(null); m.close(); loadHostedCustomers(); };
$("#cp-add-section", m.root).onclick = function () { addPjiSectionDialog(); };
$(".mf-cancel", m.root).onclick = closeProfile;
var mask = m.root.closest(".modal-mask");
if (mask) mask.querySelector(".x").onclick = closeProfile;
// 绑定 等级评估/删除(仅编辑态或后端建档后补挂)
function bindEditActions() {
var be = $(".cp-eval", m.root);
if (be && !be._bound) { be._bound = true; be.onclick = function () { evaluateClient(d.id, ctx.name || "", be, closeProfile); }; }
var bd = $(".mf-del", m.root);
if (bd && !bd._bound) { bd._bound = true; bd.onclick = function () {
Core.confirm("确定删除该客户画像?").then(function (ok) {
if (!ok) return;
Core.api("client/del", { id: d.id }).then(function () { closeProfile(); Core.toast("已删除画像", "ok"); })
.catch(function (e) { Core.toast(e.message, "err"); });
});
}; }
}
if (isEdit) bindEditActions();
// 新建/重生成后画像变为已存在:补齐 等级评估/删除 按钮
function refreshEditActions() {
if (!isEdit) return;
var foot = m.root.querySelector(".modal-foot");
if (foot && !foot.querySelector(".cp-eval")) {
var cancelBtn = foot.querySelector(".mf-cancel");
var evalBtn = document.createElement("button");
evalBtn.className = "btn ghost cp-eval"; evalBtn.textContent = "等级评估";
var delBtn = document.createElement("button");
delBtn.className = "btn danger mf-del"; delBtn.textContent = "删除画像";
if (cancelBtn) { foot.insertBefore(evalBtn, cancelBtn); foot.insertBefore(delBtn, cancelBtn); }
else { foot.appendChild(evalBtn); foot.appendChild(delBtn); }
}
bindEditActions();
}
// 重新按 id/wxid 取回最新画像并就地重绘(不关闭弹窗)
function reloadProfileIntoDialog() {
Core.api("client/list", { projectId: state.project.projectId }).then(function (list) {
list = list || [];
var found = list.filter(function (x) {
return (d.id && String(x.id) === String(d.id)) || (wxid && x.wxid === wxid);
})[0];
if (!found) { Core.toast("未取回最新画像,请重新打开", "err"); return; }
d = found; isEdit = !!d.id; wxid = d.wxid || wxid; appId = d.appId || appId;
advicerId = Number(d.advicerId || advicerId || 0);
state._cpSections = buildSectionsFromData(d.profile || {});
drawPjiSections();
refreshEditActions();
Core.toast("已载入最新画像", "ok");
}).catch(function (e) { Core.toast(e.message, "err"); });
}
// 画像更新:基于旧画像 + 聊天记录重新生成(clientId 优先,否则按 wxid 自动建档)
var regenBtn = $(".cp-regen", m.root);
if (regenBtn) regenBtn.onclick = function () {
if (!d.id && !wxid) { Core.toast("请先保存画像后再更新", "err"); return; }
Core.confirm("将结合客户旧画像与最近聊天记录重新生成画像栏目(会覆盖当前编辑内容),是否继续?").then(function (ok) {
if (!ok) return;
syncPjiSectionsFromDom();
var req = d.id
? { clientId: d.id }
: { wxid: wxid, appId: appId, projectId: state.project.projectId };
var oldText = regenBtn.textContent;
regenBtn.disabled = true; regenBtn.textContent = "更新中…";
Core.api("smart/profile/regenerate", req).then(function (res) {
regenBtn.disabled = false; regenBtn.textContent = oldText;
res = res || {};
Core.toast((res.details && res.details[0]) || "画像更新完成", "ok");
reloadProfileIntoDialog();
}).catch(function (e) {
regenBtn.disabled = false; regenBtn.textContent = oldText;
Core.toast(e.message, "err");
});
});
};
// 保存:将栏目展平为 {栏目名: content} 的 profile 全量覆盖
$(".mf-save", m.root).onclick = function () {
if (!advicerId) { Core.toast("未能确定归属销售(该客户未关联销售),无法保存画像", "err"); return; }
var sections = collectPjiSectionsData();
var profile = {};
sections.forEach(function (s) {
var nm = (s.label || s.key || "").trim();
if (!nm) return;
var c = s.content;
// 跳过空白栏目
if (c === "" || c == null) return;
if (Array.isArray(c) && !c.length) return;
if (c && typeof c === "object" && !Array.isArray(c) && !Object.keys(c).length) return;
profile[nm] = c;
});
var payload = { projectId: state.project.projectId, advicerId: advicerId, profile: profile };
if (isEdit) payload.id = d.id;
else { payload.wxid = wxid; payload.appId = appId; }
Core.api(isEdit ? "client/update" : "client/add", payload)
.then(function () { closeProfile(); Core.toast("保存成功", "ok"); })
.catch(function (e) { Core.toast(e.message, "err"); });
};
}
/* ---------- 销售人员微信工作台入口 ---------- */
/**
* 打开销售人员的微信工作台
* 检查项目 wx_token(必须),不检查 wx_device_id(登录检测由 wx.js mount 负责)
*/
function openAdvicerWxWorkbench(advicerId, advicerName, wxDeviceId) {
var checkAndEnter = function () {
var projectWxToken = '';
if (state.detail && state.detail.Base) {
projectWxToken = state.detail.Base.wx_token || '';
}
if (!projectWxToken) {
var m = Core.modal({
title: '微信工作台未就绪',
body: '<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');
}
/* ---------- Tab 6:成交线索(约定到访/成交/付款) ---------- */
var LEAD_TYPE_META = {
visit: "约定到访", deal: "约定成交", payment: "约定付款"
};
var LEAD_STATUS_META = {
pending: "待跟进", done: "已完成", cancelled: "已取消"
};
function renderTabLeads(body) {
state.data.leadStatus = state.data.leadStatus || "";
var load = function () {
Core.api("smart/lead/list", { projectId: state.project.projectId, status: state.data.leadStatus }).then(function (list) {
list = list || [];
var chips = [["", "全部"], ["pending", "待跟进"], ["done", "已完成"]].map(function (c) {
return '<button class="btn ghost sm ld-chip' + (state.data.leadStatus === c[0] ? " primary" : "") + '" data-status="' + c[0] + '">' + c[1] + '</button>';
}).join(" ");
var html = '<div class="row mb-2"><span class="fs-13 c-main" style="font-weight:700">成交线索</span>' +
'<span class="fs-12 c-dim">共 ' + list.length + ' 条·对话复盘自动抽取到访/成交/付款约定,供销售跟进并注入后续 AI 对话</span><span class="spacer"></span>' +
chips + ' <button class="btn primary sm" id="ld-add">+ 新增线索</button></div>';
if (!list.length) {
html += '<div class="empty"><div class="big">◉</div>暂无成交线索</div>';
} else {
html += '<div class="table-wrap"><table class="table"><thead><tr>' +
"<th>客户</th><th>线索类型</th><th>约定</th><th>约定时间</th><th>状态</th><th>来源</th><th>跟进结果</th><th>操作</th></tr></thead><tbody>" +
list.map(function (t) {
var isPending = t.status === "pending";
var result = (t.result || "").slice(0, 40);
var ops = "-";
if (isPending) {
ops = '<button class="btn ghost sm ld-done" data-id="' + t.id + '" data-title="' + Core.esc(t.title || "") + '">完成</button>' +
'<button class="btn ghost sm ld-cancel" data-id="' + t.id + '" data-title="' + Core.esc(t.title || "") + '">取消</button>';
}
return "<tr>" +
'<td class="mono fs-12">' + Core.esc((t.wxid || "").slice(0, 12) || "-") + "</td>" +
"<td>" + (LEAD_TYPE_META[t.lead_type] || t.lead_type || "-") + "</td>" +
'<td><span class="name">' + Core.esc(t.title || "-") + "</span>" + (t.detail ? '<div class="fs-12 c-dim">' + Core.esc(t.detail) + "</div>" : "") + "</td>" +
'<td class="fs-12">' + Core.fmtTime(t.expect_at) + "</td>" +
'<td>' + (LEAD_STATUS_META[t.status] || t.status) + "</td>" +
'<td class="fs-12 c-dim">' + (t.source === "manual" ? "手动" : "AI") + "</td>" +
'<td class="fs-12 c-dim">' + Core.esc(result) + "</td>" +
'<td class="ops">' + ops + "</td></tr>";
}).join("") + "</tbody></table></div>";
}
body.innerHTML = html;
$("#ld-add").onclick = function () { leadForm(); };
$$(".ld-chip").forEach(function (b) {
b.onclick = function () { state.data.leadStatus = b.dataset.status; renderDataBody(); };
});
$$(".ld-cancel").forEach(function (b) {
b.onclick = function () { cancelLead(b.dataset.id, b.dataset.title); };
});
$$(".ld-done").forEach(function (b) {
b.onclick = function () { completeLead(b.dataset.id, b.dataset.title); };
});
}).catch(function (e) {
body.innerHTML = '<div class="empty"><div class="big">⚠</div>' + Core.esc(e.message) + "</div>";
});
};
load();
}
/* 托管客户单选下拉(数据源=advicer/customer/hosted;value=wxid,data-appid=登录实例) */
function loadHostedCustomerOptions(sel, currentWxid) {
if (!sel) return;
sel.innerHTML = '<option value="">加载中…</option>';
Core.api("advicer/customer/hosted", { projectId: state.project.projectId }).then(function (list) {
list = list || [];
if (!list.length) { sel.innerHTML = '<option value="">暂无托管客户</option>'; return; }
sel.innerHTML = '<option value="">请选择客户</option>' + list.map(function (c) {
var label = c.remark || c.nick_name || c.user_name || "客户";
var selected = (currentWxid && c.user_name === currentWxid) ? " selected" : "";
return '<option value="' + Core.esc(c.user_name || "") + '"' + selected +
' data-appid="' + Core.esc(c.advicer_wx_device_id || "") + '">' + Core.esc(label) + '</option>';
}).join("");
}).catch(function (e) {
sel.innerHTML = '<option value="">加载失败:' + Core.esc(e.message) + '</option>';
});
}
function leadForm() {
var m = Core.modal({
title: "新增成交线索",
body:
'<div class="field"><label class="label">客户<span class="req">*</span></label><select id="ld-client" class="select"></select></div>' +
'<div class="grid cols-2">' +
'<div class="field"><label class="label">线索类型<span class="req">*</span></label><select id="ld-type" class="select">' +
'<option value="visit">约定到访</option>' +
'<option value="deal">约定成交</option>' +
'<option value="payment">约定付款</option></select></div>' +
'<div class="field"><label class="label">约定时间<span class="req">*</span></label><input id="ld-expect" class="input" placeholder="YYYY-MM-DD HH:MM"></div>' +
"</div>" +
'<div class="field"><label class="label">标题</label><input id="ld-title" class="input" placeholder="如:到访售楼部 / 交付定金"></div>' +
'<div class="field"><label class="label">详情</label><textarea id="ld-detail" class="textarea" style="min-height:60px" placeholder="约定原文/关键信息"></textarea></div>',
footer: '<button class="btn ghost mf-cancel">取消</button><button class="btn primary mf-save">保存</button>'
});
loadHostedCustomerOptions($("#ld-client", m.root));
$(".mf-cancel", m.root).onclick = m.close;
$(".mf-save", m.root).onclick = function () {
var sel = $("#ld-client", m.root);
var wxid = sel.value;
var appId = sel.options[sel.selectedIndex] ? (sel.options[sel.selectedIndex].dataset.appid || "") : "";
var expectAt = $("#ld-expect", m.root).value.trim();
if (!wxid) { Core.toast("请选择客户", "err"); return; }
if (!/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(expectAt)) { Core.toast("约定时间格式应形如 2026-09-29 14:00", "err"); return; }
Core.api("smart/lead/create", {
wxid: wxid,
appId: appId,
projectId: state.project.projectId,
leadType: $("#ld-type", m.root).value,
title: $("#ld-title", m.root).value.trim(),
detail: $("#ld-detail", m.root).value.trim(),
expectAt: expectAt.replace("T", " ").slice(0, 16)
}).then(function () { m.close(); Core.toast("已新增成交线索", "ok"); renderDataBody(); })
.catch(function (e) { Core.toast(e.message, "err"); });
};
}
function cancelLead(id, title) {
Core.confirm("确定取消成交线索「" + (title || "未命名") + "」?").then(function (ok) {
if (!ok) return;
Core.api("smart/lead/cancel", { id: Number(id) }).then(function () {
Core.toast("已取消", "ok");
renderDataBody();
}).catch(function (e) { Core.toast(e.message, "err"); });
});
}
function completeLead(id, title) {
Core.confirm("将成交线索「" + (title || "未命名") + "」标记为已完成?").then(function (ok) {
if (!ok) return;
Core.api("smart/lead/complete", { id: Number(id) }).then(function () {
Core.toast("已标记完成", "ok");
renderDataBody();
}).catch(function (e) { Core.toast(e.message, "err"); });
});
}
/* ---------- Tab 7:定时提醒(到点由 AI 主动发消息) ---------- */
var REMIND_STATUS_META = {
pending: "待执行", done: "已发送", cancelled: "已取消", failed: "失败"
};
function renderTabReminds(body) {
state.data.remindStatus = state.data.remindStatus || "";
var load = function () {
Core.api("smart/remind/list", { projectId: state.project.projectId, status: state.data.remindStatus }).then(function (list) {
list = list || [];
var chips = [["", "全部"], ["pending", "待执行"], ["done", "已发送"], ["failed", "失败"]].map(function (c) {
return '<button class="btn ghost sm rm-chip' + (state.data.remindStatus === c[0] ? " primary" : "") + '" data-status="' + c[0] + '">' + c[1] + '</button>';
}).join(" ");
var html = '<div class="row mb-2"><span class="fs-13 c-main" style="font-weight:700">定时提醒</span>' +
'<span class="fs-12 c-dim">共 ' + list.length + ' 条·到点由 AI 主动发消息提醒客户</span><span class="spacer"></span>' +
chips + ' <button class="btn primary sm" id="rm-add">+ 新增提醒</button></div>';
if (!list.length) {
html += '<div class="empty"><div class="big">◉</div>暂无定时提醒</div>';
} else {
html += '<div class="table-wrap"><table class="table"><thead><tr>' +
"<th>客户</th><th>提醒</th><th>到点时间</th><th>状态</th><th>来源</th><th>结果/失败</th><th>操作</th></tr></thead><tbody>" +
list.map(function (t) {
var isPending = t.status === "pending";
var result = (t.result || t.err_msg || "").slice(0, 40);
var ops = "-";
if (isPending) {
ops = '<button class="btn ghost sm rm-cancel" data-id="' + t.id + '" data-title="' + Core.esc(t.title || "") + '">取消</button>';
}
return "<tr>" +
'<td class="mono fs-12">' + Core.esc((t.wxid || "").slice(0, 12) || "-") + "</td>" +
'<td><span class="name">' + Core.esc(t.title || "-") + "</span>" + (t.detail ? '<div class="fs-12 c-dim">' + Core.esc(t.detail) + "</div>" : "") + "</td>" +
'<td class="fs-12">' + Core.fmtTime(t.due_at) + "</td>" +
'<td>' + (REMIND_STATUS_META[t.status] || t.status) + "</td>" +
'<td class="fs-12 c-dim">' + (t.source === "manual" ? "手动" : "AI") + "</td>" +
'<td class="fs-12 c-dim">' + Core.esc(result) + "</td>" +
'<td class="ops">' + ops + "</td></tr>";
}).join("") + "</tbody></table></div>";
}
body.innerHTML = html;
$("#rm-add").onclick = function () { remindForm(); };
$$(".rm-chip").forEach(function (b) {
b.onclick = function () { state.data.remindStatus = b.dataset.status; renderDataBody(); };
});
$$(".rm-cancel").forEach(function (b) {
b.onclick = function () { cancelRemind(b.dataset.id, b.dataset.title); };
});
}).catch(function (e) {
body.innerHTML = '<div class="empty"><div class="big">⚠</div>' + Core.esc(e.message) + "</div>";
});
};
load();
}
function remindForm() {
var m = Core.modal({
title: "新增定时提醒",
body:
'<div class="field"><label class="label">客户<span class="req">*</span></label><select id="rm-client" class="select"></select></div>' +
'<div class="field"><label class="label">到点时间<span class="req">*</span></label><input id="rm-due" class="input" placeholder="YYYY-MM-DD HH:MM"></div>' +
'<div class="field"><label class="label">标题</label><input id="rm-title" class="input" placeholder="如:回访电话"></div>' +
'<div class="field"><label class="label">详情</label><textarea id="rm-detail" class="textarea" style="min-height:60px" placeholder="提醒原文/关键信息"></textarea></div>',
footer: '<button class="btn ghost mf-cancel">取消</button><button class="btn primary mf-save">保存</button>'
});
loadHostedCustomerOptions($("#rm-client", m.root));
$(".mf-cancel", m.root).onclick = m.close;
$(".mf-save", m.root).onclick = function () {
var sel = $("#rm-client", m.root);
var wxid = sel.value;
var appId = sel.options[sel.selectedIndex] ? (sel.options[sel.selectedIndex].dataset.appid || "") : "";
var dueAt = $("#rm-due", m.root).value.trim();
if (!wxid) { Core.toast("请选择客户", "err"); return; }
if (!/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(dueAt)) { Core.toast("到点时间格式应形如 2026-09-29 14:00", "err"); return; }
Core.api("smart/remind/create", {
wxid: wxid,
appId: appId,
projectId: state.project.projectId,
title: $("#rm-title", m.root).value.trim(),
detail: $("#rm-detail", m.root).value.trim(),
dueAt: dueAt.replace("T", " ").slice(0, 16)
}).then(function () { m.close(); Core.toast("已新增定时提醒", "ok"); renderDataBody(); })
.catch(function (e) { Core.toast(e.message, "err"); });
};
}
function cancelRemind(id, title) {
Core.confirm("确定取消定时提醒「" + (title || "未命名") + "」?").then(function (ok) {
if (!ok) return;
Core.api("smart/remind/cancel", { id: Number(id) }).then(function () {
Core.toast("已取消", "ok");
renderDataBody();
}).catch(function (e) { Core.toast(e.message, "err"); });
});
}