ai_scheduler/web/assets/js/project_detail.js

924 lines
41 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.

/* ==================== 视图二:项目设置(tab 切换) ==================== */
function renderProjectDetail() {
if (!state.project || !state.project.projectId) { switchView("projects"); return; }
$("#view-projectDetail").innerHTML = '<div class="card"><div class="empty"><span class="spin"></span></div></div>';
Core.api("project/info", { projectId: state.project.projectId })
.then(function (info) {
state.detail = info || {};
var base = info.Base || {};
setActiveProject({ projectId: base.project_id || state.project.projectId, name: base.name || state.project.name });
// 加载项目资料(新表)
return Core.api("project/data/load", { projectId: state.project.projectId });
})
.then(function (resp) {
state.projectData = resp || {};
// resp 即为栏目扁平 map(保存什么就读到什么)
var data = resp || {};
state.pjiSections = buildSectionsFromData(data);
// 保存初始快照(用于重置)
state.pjiInitialSections = JSON.parse(JSON.stringify(state.pjiSections));
renderProjectDetailForm();
})
.catch(function (e) {
$("#view-projectDetail").innerHTML = '<div class="card"><div class="empty"><div class="big">\u26a0</div>' + Core.esc(e.message) + "</div></div>";
});
}
/* ---------- 主 Tab 定义 ---------- */
var PD_TABS = [
["base", "\u57fa\u7840\u4fe1\u606f"],
["projectInfo", "\u9879\u76ee\u8d44\u6599"],
["settings", "\u8bbe\u7f6e"]
];
/* ---------- 栏目类型选项(与维度格式一致) ---------- */
var PJI_TYPE_OPTIONS = [
["string", "\u63cf\u8ff0"],
["object", "\u8be6\u7ec6\u4fe1\u606f"],
["array", "\u591a\u6761\u76ee"],
["list", "\u5217\u8868"]
];
var PJI_TYPE_LABELS = { string: "\u63cf\u8ff0", object: "\u8be6\u7ec6\u4fe1\u606f", array: "\u591a\u6761\u76ee", list: "\u5217\u8868" };
function renderProjectDetailForm() {
var base = state.detail.Base || {};
var sup = state.detail.ModelInfo || {};
var hasDimEditor = typeof dimBlock === "function";
// ===== 顶部卡片 =====
var html =
'<div class="card glow">' +
'<div class="card-title"><span class="bar"></span>\u9879\u76ee\u8bbe\u7f6e <span class="badge blue mono">#' + (base.project_id || state.project.projectId) + "</span></div>";
// ===== Tab 栏 =====
html += '<div class="tabs" id="pd-tabs">';
PD_TABS.forEach(function (t) {
html += '<div class="tab' + (t[0] === "base" ? " active" : "") + '" data-tab="' + t[0] + '">' + t[1] + "</div>";
});
html += "</div>";
// ===== Tab 1:基础信息 =====
html += '<div id="pd-tab-base">';
// 项目基本信息
html +=
'<div class="card-title" style="font-size:14px;margin-bottom:10px"><span class="bar"></span>\u9879\u76ee\u4fe1\u606f' +
'<span class="spacer"></span><button class="btn primary sm" id="pd-save-all">\u4fdd\u5b58\u57fa\u7840\u4fe1\u606f</button></div>' +
'<div class="grid cols-2">' +
'<div class="field"><label class="label">\u9879\u76ee\u540d\u79f0</label><input id="pd-name" class="input" value="' + Core.esc(base.name || "") + '"></div>' +
"</div>";
// 行业模板应用
html +=
'<div class="divider"></div>' +
'<div class="card-title" style="font-size:14px;margin-bottom:10px"><span class="bar"></span>\u884c\u4e1a\u6a21\u677f</div>' +
'<div class="card-sub">\u4ece\u884c\u4e1a\u6a21\u677f\u91cd\u65b0\u590d\u5236\u7ef4\u5ea6\u5185\u5bb9\uff08\u8986\u76d6\u4e0b\u65b9\u7ef4\u5ea6\u6a21\u677f\u5b57\u6bb5\uff0c\u4e0d\u5f71\u54cd\u5176\u4ed6\u6570\u636e\uff09</div>' +
'<div class="row"><div class="flex1"><select id="pd-industry" class="select"><option>\u52a0\u8f7d\u4e2d\u2026</option></select></div>' +
'<button class="btn success" id="pd-copy">\u27f3 \u5e94\u7528\u6a21\u677f</button></div>';
// 维度子 tab
html += '<div class="divider"></div>';
html += '<div class="card-title" style="font-size:14px;margin-bottom:10px"><span class="bar"></span>\u7ef4\u5ea6\u6a21\u677f</div>';
html += '<div class="tabs sub-tabs" id="pd-dim-tabs">';
DIM_NAMES.forEach(function (d, i) {
var label = d[1].replace(/ \u6a21\u677f$/, "");
html += '<div class="tab' + (i === 0 ? " active" : "") + '" data-dim="' + d[0] + '">' + label + "</div>";
});
html += "</div>";
// 维度内容区
DIM_NAMES.forEach(function (d, i) {
var hidden = i === 0 ? "" : ' style="display:none"';
html += '<div class="pd-dim-panel" data-dim="' + d[0] + '"' + hidden + ">";
if (d[0] === "desc" || d[0] === "advicer_desc") {
html += '<div class="field"><label class="label">' + d[1] + '</label>' +
'<textarea class="textarea pd-dim" data-field="' + d[0] + '" style="min-height:120px">' + Core.esc(base[d[0]] || "") + "</textarea></div>";
} else if (d[0] === "rule_dimension") {
html += '<div class="field"><label class="label">\u98ce\u63a7\u7ef4\u5ea6 \u6a21\u677f</label>' +
'<textarea class="textarea pd-dim" data-field="rule_dimension" style="min-height:100px" placeholder="\u53ef\u9009">' +
Core.esc(base.rule_dimension || "") + "</textarea></div>";
} else if (hasDimEditor) {
var dimLabel = d[1].replace(/ \u6a21\u677f$/, "");
html += dimBlock(dimLabel, base[d[0]] || "");
} else {
html += '<div class="field"><label class="label">' + d[1] + '</label>' +
'<textarea class="textarea pd-dim" data-field="' + d[0] + '" style="min-height:100px">' + Core.esc(base[d[0]] || "") + "</textarea></div>";
}
html += "</div>";
});
html += "</div>"; // end pd-tab-base
// ===== Tab 2:项目资料(动态编辑,初始无栏目) =====
html += '<div id="pd-tab-projectInfo" style="display:none">';
html += '<div class="card-title" style="font-size:14px;margin-bottom:10px"><span class="bar"></span>\u9879\u76ee\u8d44\u6599' +
'<span class="spacer"></span>' +
'<button class="btn ghost sm" id="pji-add-section">+ \u65b0\u589e\u680f\u76ee</button>' +
'<button class="btn ghost sm" id="pji-ai-preset" style="margin-left:8px">\u2726 AI \u9884\u8bbe</button>' +
'<button class="btn ghost sm" id="pji-reset" style="margin-left:8px">\u91cd\u7f6e</button>' +
'<button class="btn primary sm" id="pji-save" style="margin-left:8px">\u4fdd\u5b58\u9879\u76ee\u8d44\u6599</button></div>';
html += '<div class="card-sub" style="margin-bottom:12px">\u6bcf\u4e2a\u680f\u76ee\u7684 key \u5373\u4e3a\u5b58\u50a8\u65f6\u7684\u5b57\u6bb5\u540d\uff0c\u6570\u636e\u4ee5\u6241\u5e73 JSON \u683c\u5f0f\u5b58\u50a8\uff0cAI \u53ef\u76f4\u63a5\u8bfb\u53d6\u3002</div>';
html += '<div id="pji-sections"></div>';
html += '</div>'; // end pd-tab-projectInfo
// ===== Tab 3:设置(模型配置 + 微信接入 Token) =====
html += '<div id="pd-tab-settings" style="display:none">';
html += '<div class="card-title" style="font-size:14px;margin-bottom:10px"><span class="bar"></span>\u5fae\u4fe1\u63a5\u5165\u914d\u7f6e' +
'<span class="spacer"></span><button class="btn primary sm" id="pd-save-wx">\u4fdde\u5b58\u914d\u7f6e</button></div>' +
'<div class="card-sub">\u914d\u7f6e\u9879\u76ee\u7ea7\u5fae\u4fe1\u534f\u8bae Token\uff0c\u7528\u4e8e\u8be5\u9879\u76ee\u4e0b\u6240\u6709\u9500\u552e\u4eba\u5458\u5171\u4eab\u7684\u5fae\u4fe1\u5de5\u4f5c\u53f0\u63a5\u5165\u3002</div>' +
'<div class="field"><label class="label">X-finder-TOKEN</label>' +
'<input id="pd-wx-token" class="input mono" placeholder="\u8bf7\u8f93\u5165\u5fae\u4fe1\u534f\u8bae\u670d\u52a1\u4ee4\u724c" value="' + Core.esc(base.wx_token || '') + '"></div>' +
'<div class="fs-12 c-dim" style="line-height:1.8">' +
'\u00b7 \u8be5 Token \u7528\u4e8e\u5bf9\u63a5\u5fae\u4fe1\u534f\u8bae\u670d\u52a1\uff0c\u9879\u76ee\u7ea7\u5171\u4eab\uff1b<br>' +
'\u00b7 \u8fdb\u5165\u5fae\u4fe1\u5de5\u4f5c\u53f0\u524d\u4f1a\u68c0\u6d4b\u9879\u76ee\u662f\u5426\u5df2\u914d\u7f6e Token\uff1b<br>' +
'\u00b7 \u9500\u552e\u4eba\u5458\u8fd8\u9700\u914d\u7f6e\u4e2a\u4eba\u7684\u5fae\u4fe1\u8bbe\u5907 ID\uff08wx_device_id\uff09\u65b9\u53ef\u8fdb\u5165\u5de5\u4f5c\u53f0\u3002</div>' +
'<div class="divider"></div>' +
'<div class="card-title" style="font-size:14px;margin-bottom:10px"><span class="bar"></span>\u6a21\u578b\u914d\u7f6e</div>' +
'<div class="grid cols-2">' +
'<div class="field"><label class="label">\u5f53\u524d\u6a21\u578b\u914d\u7f6e</label><select id="pd-settings-model" class="select"></select></div>' +
'<div class="field"><label class="label">\u6a21\u578b\u8be6\u60c5</label><input class="input" disabled value="' + Core.esc((sup.sup_name || '-') + (sup.chat_model ? ' \u00b7 ' + sup.chat_model : '')) + '"></div>' +
'</div>' +
'<button class="btn primary sm" id="pd-save-settings-model">\u4fdde\u5b58\u6a21\u578b\u914d\u7f6e</button>' +
'</div>'; // end pd-tab-settings
html += "</div>"; // end card
$("#view-projectDetail").innerHTML = html;
loadIndustryOptions($("#pd-industry"), base.industry_id);
// ===== 渲染项目资料动态栏目 =====
// 复位编辑器上下文为项目资料默认(防止客户画像弹窗遗留的目标污染本页)
setPjiEditorTarget(null);
drawPjiSections();
// ===== 主 Tab 切换 =====
$$("#pd-tabs .tab").forEach(function (t) {
t.onclick = function () {
$$("#pd-tabs .tab").forEach(function (x) { x.classList.toggle("active", x === t); });
var tab = t.dataset.tab;
$("#pd-tab-base").style.display = tab === "base" ? "" : "none";
$("#pd-tab-projectInfo").style.display = tab === "projectInfo" ? "" : "none";
$("#pd-tab-settings").style.display = tab === "settings" ? "" : "none";
};
});
// ===== 检查是否有待切换到设置 tab 的请求 =====
if (state._pendingSettingsTab) {
state._pendingSettingsTab = false;
var settingsTab = $("#pd-tabs .tab[data-tab='settings']");
if (settingsTab) settingsTab.click();
}
// ===== 维度子 Tab 切换 =====
$$("#pd-dim-tabs .tab").forEach(function (t) {
t.onclick = function () {
$$("#pd-dim-tabs .tab").forEach(function (x) { x.classList.toggle("active", x === t); });
$$(".pd-dim-panel").forEach(function (p) {
p.style.display = p.dataset.dim === t.dataset.dim ? "" : "none";
});
};
});
// ===== 绑定维度结构化编辑器事件 =====
if (hasDimEditor) {
$$(".dim-editor").forEach(function (editor) {
bindDimEditor(editor);
});
$$(".dim-gen").forEach(function (btn) {
btn.onclick = function () {
var dim = btn.dataset.dim;
var editor = btn.closest(".dim-editor");
generateDialog({
dimension: dim,
name: base.name || "",
desc: base.desc || "",
advicerDesc: base.advicer_desc || "",
onApply: function (content) {
var wrap = $(".dim-row-wrap", editor);
var ta = $(".dim-input", editor);
var rows = parseDimJson(content);
if (wrap) {
wrap.innerHTML = rows.length
? rows.map(function (r, i) { return dimRowHtml(i, r); }).join("")
: '<div class="dim-empty">\u6682\u65e0\u5b57\u6bb5\u5b9a\u4e49</div>';
wrap.classList.remove("hidden");
ta.classList.add("hidden");
$$(".dim-row", wrap).forEach(function (row) { bindDimRowEvents(row, editor); });
var toggleBtn = $(".dim-toggle-json", editor);
if (toggleBtn) toggleBtn.textContent = "{ } JSON";
}
if (ta) ta.value = content;
Core.toast(dim + " \u5df2\u586b\u5165\u8868\u5355", "ok");
}
});
};
});
}
// ===== 保存基础信息 =====
$("#pd-save-all").onclick = function () {
var payload = {
projectId: base.project_id || state.project.projectId,
name: $("#pd-name").value.trim()
};
if (hasDimEditor) {
$$(".dim-editor").forEach(function (ed) {
var field = ed.dataset.field;
var ta = $(".dim-input", ed);
var wrap = $(".dim-row-wrap", ed);
if (wrap && !wrap.classList.contains("hidden")) {
payload[field] = serializeDimEditor(ed);
} else if (ta) {
payload[field] = ta.value;
}
});
$$(".pd-dim").forEach(function (ta) { payload[ta.dataset.field] = ta.value; });
} else {
$$(".pd-dim").forEach(function (ta) { payload[ta.dataset.field] = ta.value; });
}
Core.loading(true, "\u4fdd\u5b58\u4e2d\u2026");
Core.api("project/base/update", payload)
.then(function () {
Core.loading(false);
Core.toast("\u4fdd\u5b58\u6210\u529f", "ok");
setActiveProject({ projectId: payload.projectId, name: payload.name });
renderProjectDetail();
})
.catch(function (e) { Core.loading(false); Core.toast(e.message, "err"); });
};
// ===== 应用行业模板 =====
$("#pd-copy").onclick = function () {
var industryId = Number($("#pd-industry").value);
if (!industryId) { Core.toast("\u8bf7\u9009\u62e9\u884c\u4e1a\u6a21\u677f", "err"); return; }
Core.confirm("\u786e\u5b9a\u4ece\u8be5\u884c\u4e1a\u6a21\u677f\u590d\u5236\u7ef4\u5ea6\u5185\u5bb9\uff1f\u5c06\u8986\u76d6\u5f53\u524d\u9879\u76ee\u7ef4\u5ea6\u6a21\u677f\u3002").then(function (ok) {
if (!ok) return;
Core.loading(true, "\u590d\u5236\u6a21\u677f\u4e2d\u2026");
Core.api("project/template/copy", { projectId: state.project.projectId, industryId: industryId })
.then(function () { Core.loading(false); Core.toast("\u6a21\u677f\u5df2\u5e94\u7528", "ok"); renderProjectDetail(); })
.catch(function (e) { Core.loading(false); Core.toast(e.message, "err"); });
});
};
// ===== 保存项目资料 =====
$("#pji-save").onclick = function () {
var sections = collectPjiSectionsData();
var dataObj = {};
sections.forEach(function (s) { dataObj[s.key] = s.content; });
Core.loading(true, "\u4fdd\u5b58\u4e2d\u2026");
Core.api("project/data/save", { projectId: state.project.projectId, data: dataObj })
.then(function () {
Core.loading(false);
Core.toast("\u9879\u76ee\u8d44\u6599\u5df2\u4fdd\u5b58", "ok");
state.pjiSections = sections;
state.pjiInitialSections = JSON.parse(JSON.stringify(sections));
})
.catch(function (e) { Core.loading(false); Core.toast(e.message, "err"); });
};
// ===== 重置(撤销本次修改,恢复为接口数据) =====
$("#pji-reset").onclick = function () {
Core.confirm("\u786e\u5b9a\u91cd\u7f6e\uff1f\u5c06\u64a4\u9500\u672c\u6b21\u6240\u6709\u65b0\u589e\u548c\u4fee\u6539\uff0c\u6062\u590d\u4e3a\u63a5\u53e3\u6570\u636e\u3002").then(function (ok) {
if (!ok) return;
// 先清空 DOM,防止 drawPjiSections 内的 sync 读到旧数据
var sectionsEl = $("#pji-sections");
if (sectionsEl) sectionsEl.innerHTML = "";
state.pjiSections = [];
Core.loading(true, "\u91cd\u65b0\u52a0\u8f7d\u4e2d\u2026");
Core.api("project/data/load", { projectId: state.project.projectId })
.then(function (resp) {
Core.loading(false);
state.projectData = resp || {};
state.pjiSections = buildSectionsFromData(resp || {});
state.pjiInitialSections = JSON.parse(JSON.stringify(state.pjiSections));
drawPjiSections();
Core.toast("\u5df2\u91cd\u7f6e\u4e3a\u63a5\u53e3\u6570\u636e", "ok");
})
.catch(function (e) { Core.loading(false); Core.toast(e.message, "err"); });
});
};
// ===== 新增栏目 =====
$("#pji-add-section").onclick = function () {
addPjiSectionDialog();
};
// ===== AI 预设 =====
$("#pji-ai-preset").onclick = function () {
aiPresetDialog();
};
// ===== 设置 Tab:加载模型配置选项 =====
loadModelsupOptions($("#pd-settings-model"), base.model_sup_id);
// ===== 设置 Tab:保存微信接入 Token =====
$("#pd-save-wx").onclick = function () {
var wxToken = $("#pd-wx-token").value.trim();
Core.loading(true, "保存中…");
Core.api("project/base/update", { projectId: state.project.projectId, wxToken: wxToken })
.then(function () {
Core.loading(false);
Core.toast("微信接入配置已保存", "ok");
})
.catch(function (e) { Core.loading(false); Core.toast(e.message, "err"); });
};
// ===== 设置 Tab:保存模型配置 =====
$("#pd-save-settings-model").onclick = function () {
var modelSupId = Number($("#pd-settings-model").value) || 0;
Core.loading(true, "保存中…");
Core.api("project/base/update", { projectId: state.project.projectId, modelSupId: modelSupId })
.then(function () {
Core.loading(false);
Core.toast("模型配置已保存", "ok");
renderProjectDetail();
})
.catch(function (e) { Core.loading(false); Core.toast(e.message, "err"); });
};
}
/* ==================== 项目资料动态栏目(维度格式) ==================== */
/*
* 栏目编辑器上下文:默认读写项目资料页的 state.pjiSections + #pji-sections。
* 通过 setPjiEditorTarget 可将「数据源 + 容器」指向别处(如客户画像弹窗内的编辑器),
* 复用同一套渲染/绑定/收集逻辑;传 null 复位为项目资料默认。
*/
var __pjiTarget = null;
function setPjiEditorTarget(root, get, set) {
__pjiTarget = root ? { root: root, get: get, set: set } : null;
}
function pjiContainer() {
if (__pjiTarget && __pjiTarget.root) return __pjiTarget.root.querySelector("#pji-sections");
return document.querySelector("#pji-sections");
}
function pjiGetSections() {
if (__pjiTarget && __pjiTarget.get) return __pjiTarget.get() || [];
return state.pjiSections || [];
}
function pjiSetSections(v) {
if (__pjiTarget && __pjiTarget.set) { __pjiTarget.set(v); return; }
state.pjiSections = v;
}
/** 将 MongoDB 返回的 [{Key,Value}] 或其他格式统一转为 [{k,v}] */
function normalizeKvPairs(data) {
if (!data) return [];
if (Array.isArray(data)) {
return data.map(function (item) {
if (item && typeof item === "object") {
// 支持 {k,v} 和 {Key,Value} 两种格式
if (item.Key !== undefined) return { k: String(item.Key), v: String(item.Value || "") };
if (item.k !== undefined) return { k: String(item.k), v: String(item.v || "") };
}
return { k: String(item), v: "" };
});
}
if (typeof data === "object") {
return Object.keys(data).map(function (k) { return { k: k, v: String(data[k] || "") }; });
}
return [];
}
/** 从数据值推断栏目类型 */
function inferSectionType(val) {
if (typeof val === "string") return "string";
if (Array.isArray(val)) {
if (val.length && typeof val[0] === "object" && val[0] !== null) return "list";
return "array";
}
if (typeof val === "object" && val !== null) return "object";
return "string";
}
/** 从接口返回的扁平数据还原栏目数组 */
function buildSectionsFromData(data) {
var result = [];
Object.keys(data).forEach(function (key) {
var val = data[key];
var type = inferSectionType(val);
result.push({ key: key, label: key, type: type, content: parseSectionContent(val, type) });
});
return result;
}
/** 解析栏目内容为编辑器所需格式 */
function parseSectionContent(content, type) {
if (content == null || content === "") return type === "string" ? "" : [];
if (type === "string") return typeof content === "string" ? content : JSON.stringify(content);
if (typeof content === "string") {
try { content = JSON.parse(content); } catch (e) { return type === "array" ? [content] : []; }
}
if (type === "object") return normalizeKvPairs(content);
if (type === "list") {
// list: 数组,每个元素是一组 [{k,v}] 或 [{Key,Value}]
if (Array.isArray(content)) {
return content.map(function (entry) { return normalizeKvPairs(entry); });
}
return typeof parseExample === "function" ? parseExample(content, "list") : [];
}
if (type === "array") return typeof parseExample === "function" ? parseExample(content, "array") : [];
return [];
}
/** 生成栏目内容编辑器 HTML */
function sectionContentHtml(type, content) {
if (type === "string") {
return '<input class="input pji-content" placeholder="\u8bf7\u8f93\u5165\u5185\u5bb9" value="' + Core.esc(content || "") + '">';
}
if (type === "object") {
var pairs = (Array.isArray(content) ? content : []).map(function (p) { return kvRowHtml(p.k, p.v); }).join("");
return '<div class="dr-kv-editor">' + pairs +
'<button type="button" class="btn ghost sm dr-kv-add">+ \u6dfb\u52a0\u5b57\u6bb5</button></div>';
}
if (type === "array") {
var items = (Array.isArray(content) ? content : []).map(function (v, i) {
return '<div class="dr-example-row"><input class="input sm dr-example-item" placeholder="\u6761\u76ee ' + (i + 1) + '" value="' + Core.esc(v || "") + '">' +
'<button type="button" class="btn danger sm dr-example-del" title="\u5220\u9664">\u2715</button></div>';
}).join("");
return '<div class="dr-example-multi">' + items +
'<button type="button" class="btn ghost sm dr-example-add">+ \u6dfb\u52a0\u6761\u76ee</button></div>';
}
if (type === "list") {
var entries = (Array.isArray(content) ? content : []).map(function (pairs, idx) {
var kvHtml = (Array.isArray(pairs) ? pairs : []).map(function (p) { return kvRowHtml(p.k, p.v); }).join("");
return '<div class="dr-list-entry" data-entry="' + idx + '">' +
'<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="\u5220\u9664\u6574\u6761">\u2715</button></div>' +
kvHtml +
'<button type="button" class="btn ghost sm dr-kv-add">+ \u6dfb\u52a0\u5b57\u6bb5</button></div>';
}).join("");
return '<div class="dr-list-editor">' + entries +
'<button type="button" class="btn ghost sm dr-list-entry-add">+ \u6dfb\u52a0\u6761\u76ee</button></div>';
}
return "";
}
/** 将任意格式内容统一转为字符串表示 */
function contentToString(content, type) {
if (content == null || content === "") return "";
if (type === "string") return typeof content === "string" ? content : JSON.stringify(content);
if (type === "object") return normalizeKvPairs(content).map(function (p) { return p.k + ": " + p.v; }).join("\n");
if (type === "array") {
if (Array.isArray(content)) return content.map(function (v) { return String(v || ""); }).join("\n");
if (typeof content === "string") return content;
return String(content);
}
if (type === "list") {
if (Array.isArray(content)) return content.map(function (entry) { return normalizeKvPairs(entry).map(function (p) { return p.k + ": " + p.v; }).join(", "); }).join("\n");
return typeof content === "string" ? content : JSON.stringify(content);
}
return "";
}
/** 将字符串内容解析为目标类型格式 */
function stringToType(str, toType) {
if (str == null || str === "") return toType === "string" ? "" : [];
if (toType === "string") return str;
if (toType === "object") {
var lines = str.split("\n").filter(function (s) { return s.trim(); });
if (!lines.length) return [];
return lines.map(function (line) {
var idx = line.indexOf(":");
if (idx > 0) return { k: line.substring(0, idx).trim(), v: line.substring(idx + 1).trim() };
return { k: line.trim(), v: "" };
});
}
if (toType === "array") {
return str.split("\n").filter(function (s) { return s.trim(); });
}
if (toType === "list") {
var listLines = str.split("\n").filter(function (s) { return s.trim(); });
if (!listLines.length) return [];
return [listLines.map(function (line) {
var idx = line.indexOf(":");
if (idx > 0) return { k: line.substring(0, idx).trim(), v: line.substring(idx + 1).trim() };
return { k: line.trim(), v: "" };
})];
}
return [];
}
/** 将内容从一种类型转换为另一种类型(统一走字符串中转) */
function convertSectionContent(content, fromType, toType) {
if (fromType === toType) return content;
// 先统一转为字符串,再从字符串转为目标类型
var str = contentToString(content, fromType);
return stringToType(str, toType);
}
/** 从单个卡片 DOM 直接读取内容(不依赖 state) */
function readCardContentFromDom(card, type) {
if (!card) return type === "string" ? "" : [];
if (type === "string") {
var inp = $(".pji-content", card);
return inp ? inp.value : "";
}
if (type === "object") {
var obj = {};
$$(".dr-kv-row", card).forEach(function (r) {
var k = $(".dr-kv-key", r).value.trim();
var v = $(".dr-kv-val", r).value.trim();
if (k) obj[k] = v;
});
return obj;
}
if (type === "array") {
var arr = [];
$$(".dr-example-item", card).forEach(function (inp) {
var v = inp.value.trim();
if (v) arr.push(v);
});
return arr;
}
if (type === "list") {
var list = [];
$$(".dr-list-entry", card).forEach(function (entry) {
var obj = {};
$$(".dr-kv-row", entry).forEach(function (r) {
var k = $(".dr-kv-key", r).value.trim();
var v = $(".dr-kv-val", r).value.trim();
if (k) obj[k] = v;
});
if (Object.keys(obj).length) list.push(obj);
});
return list;
}
return type === "string" ? "" : [];
}
/** 从 DOM 同步栏目内容到当前数据源(防止重绘丢失数据) */
function syncPjiSectionsFromDom() {
var sections = pjiGetSections();
var cont = pjiContainer();
var cards = $$(".pji-section-card", cont || document);
if (!cards.length || !sections.length) return;
cards.forEach(function (card) {
var idx = Number(card.dataset.idx);
var sec = sections[idx];
if (!sec) return;
if (sec.type === "string") {
var inp = $(".pji-content", card);
if (inp) sec.content = inp.value;
} else if (sec.type === "object") {
var obj = {};
$$(".dr-kv-row", card).forEach(function (r) {
var k = $(".dr-kv-key", r).value.trim();
var v = $(".dr-kv-val", r).value.trim();
if (k) obj[k] = v;
});
sec.content = obj;
} else if (sec.type === "array") {
var arr = [];
$$(".dr-example-item", card).forEach(function (inp) {
var v = inp.value.trim();
if (v) arr.push(v);
});
sec.content = arr;
} else if (sec.type === "list") {
var list = [];
$$(".dr-list-entry", card).forEach(function (entry) {
var obj = {};
$$(".dr-kv-row", entry).forEach(function (r) {
var k = $(".dr-kv-key", r).value.trim();
var v = $(".dr-kv-val", r).value.trim();
if (k) obj[k] = v;
});
if (Object.keys(obj).length) list.push(obj);
});
sec.content = list;
}
});
}
/** 绘制所有栏目(渲染到当前上下文容器) */
function drawPjiSections() {
var container = pjiContainer();
if (!container) return;
// 重绘前先从 DOM 同步数据,防止已填内容丢失
syncPjiSectionsFromDom();
var sections = pjiGetSections();
if (!sections.length) {
container.innerHTML = '<div class="empty" style="padding:30px 0"><div class="big">\u2726</div>\u6682\u65e0\u680f\u76ee\uff0c\u70b9\u51fb\u4e0a\u65b9\u300c+ \u65b0\u589e\u680f\u76ee\u300d\u6dfb\u52a0</div>';
return;
}
var typeOptsHtml = PJI_TYPE_OPTIONS.map(function (o) {
return '<option value="' + o[0] + '">' + o[1] + '</option>';
}).join("");
var html = "";
sections.forEach(function (sec, idx) {
var content = parseSectionContent(sec.content, sec.type);
html += '<div class="pji-section-card" data-idx="' + idx + '">' +
'<div class="pji-section-head">' +
'<span class="pji-section-label" data-idx="' + idx + '" title="\u70b9\u51fb\u4fee\u6539\u540d\u79f0" style="cursor:pointer">' + Core.esc(sec.label) + '</span>' +
'<select class="select sm pji-type-select" data-idx="' + idx + '" style="width:110px">' + typeOptsHtml + '</select>' +
'<span class="spacer"></span>' +
'<button class="btn danger sm pji-del" data-idx="' + idx + '">\u5220\u9664</button>' +
"</div>" +
'<div class="pji-section-body">' + sectionContentHtml(sec.type, content) + "</div>" +
"</div>";
});
container.innerHTML = html;
// 设置下拉框当前值
$$(".pji-type-select", container).forEach(function (sel) {
sel.value = sections[Number(sel.dataset.idx)].type;
});
bindPjiSectionEvents();
}
/** 绑定栏目内事件(增删改) */
function bindPjiSectionEvents() {
var container = pjiContainer();
if (!container) return;
var sections = pjiGetSections();
// 点击栏目名称 → 内联编辑
$$(".pji-section-label", container).forEach(function (span) {
span.onclick = function () {
var idx = Number(span.dataset.idx);
var sec = sections[idx];
if (!sec) return;
// 先同步当前 DOM 数据
syncPjiSectionsFromDom();
var input = document.createElement("input");
input.className = "input sm";
input.style.cssText = "width:160px;padding:2px 8px";
input.value = sec.label;
span.replaceWith(input);
input.focus();
input.select();
var finish = function () {
var newLabel = input.value.trim();
if (newLabel) {
sec.label = newLabel;
// 同步更新 key(存储字段名)
sec.key = newLabel.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, "").replace(/^[0-9]+/, "") || ("sec_" + Date.now());
}
pjiSetSections(sections);
drawPjiSections();
};
input.onblur = finish;
input.onkeydown = function (e) { if (e.key === "Enter") { e.preventDefault(); finish(); } if (e.key === "Escape") { pjiSetSections(sections); drawPjiSections(); } };
};
});
// 删除栏目
$$(".pji-del", container).forEach(function (btn) {
btn.onclick = function () {
var idx = Number(btn.dataset.idx);
Core.confirm("\u786e\u5b9a\u5220\u9664\u680f\u76ee\u300c" + sections[idx].label + "\u300d\uff1f").then(function (ok) {
if (!ok) return;
sections.splice(idx, 1);
pjiSetSections(sections);
drawPjiSections();
});
};
});
// 下拉框切换类型(保留内容转换)
$$(".pji-type-select", container).forEach(function (sel) {
sel.onchange = function () {
var idx = Number(sel.dataset.idx);
var sec = sections[idx];
if (!sec) return;
var oldType = sec.type;
var newType = sel.value;
if (oldType === newType) return;
// 直接从当前卡片 DOM 读取内容(不走 syncPjiSectionsFromDom,避免干扰)
var card = sel.closest(".pji-section-card");
var domContent = readCardContentFromDom(card, oldType);
// 转换内容格式
sec.content = convertSectionContent(domContent, oldType, newType);
sec.type = newType;
pjiSetSections(sections);
drawPjiSections();
};
});
// 每个栏目卡片内的编辑器事件
$$(".pji-section-card", container).forEach(function (card) {
var idx = Number(card.dataset.idx);
var sec = sections[idx];
if (!sec || sec.type === "string") return;
if (sec.type === "object") {
// KV 添加字段
$$(".dr-kv-add", card).forEach(function (b) {
b.onclick = function () {
b.insertAdjacentHTML("beforebegin", kvRowHtml("", ""));
};
});
// KV 删除字段
card.addEventListener("click", function (e) {
if (e.target.classList.contains("dr-kv-del")) {
e.target.closest(".dr-kv-row").remove();
}
});
}
if (sec.type === "array") {
// 添加条目
$$(".dr-example-add", card).forEach(function (b) {
b.onclick = function () {
var wrap = b.closest(".dr-example-multi");
var count = $$(".dr-example-item", wrap).length;
var div = document.createElement("div");
div.className = "dr-example-row";
div.innerHTML = '<input class="input sm dr-example-item" placeholder="\u6761\u76ee ' + (count + 1) + '" value="">' +
'<button type="button" class="btn danger sm dr-example-del" title="\u5220\u9664">\u2715</button>';
wrap.insertBefore(div, b);
};
});
// 删除条目
card.addEventListener("click", function (e) {
if (e.target.classList.contains("dr-example-del")) {
e.target.closest(".dr-example-row").remove();
}
});
}
if (sec.type === "list") {
// 添加条目
$$(".dr-list-entry-add", card).forEach(function (b) {
b.onclick = function () {
var editor = b.closest(".dr-list-editor");
var lastIdx = $$(".dr-list-entry", editor).length;
var div = document.createElement("div");
div.className = "dr-list-entry";
div.dataset.entry = lastIdx;
div.innerHTML = '<div class="dr-list-entry-head"><span class="dr-list-entry-idx">#' + (lastIdx + 1) + '</span>' +
'<button type="button" class="btn danger sm dr-list-entry-del" title="\u5220\u9664\u6574\u6761">\u2715</button></div>' +
kvRowHtml("", "") +
'<button type="button" class="btn ghost sm dr-kv-add">+ \u6dfb\u52a0\u5b57\u6bb5</button>';
editor.insertBefore(div, b);
// 绑定新条目内的事件
$$(".dr-kv-add", div).forEach(function (kvAdd) {
kvAdd.onclick = function () { kvAdd.insertAdjacentHTML("beforebegin", kvRowHtml("", "")); };
});
};
});
// 删除条目
card.addEventListener("click", function (e) {
if (e.target.classList.contains("dr-list-entry-del")) {
e.target.closest(".dr-list-entry").remove();
}
});
// 条目内 KV 添加(委托,排除非 list 区域)
card.addEventListener("click", function (e) {
if (e.target.classList.contains("dr-kv-add") && !e.target.classList.contains("dr-list-entry-add")) {
var entry = e.target.closest(".dr-list-entry");
if (entry) { e.preventDefault(); e.stopPropagation(); e.target.insertAdjacentHTML("beforebegin", kvRowHtml("", "")); }
}
});
// 条目内 KV 删除
card.addEventListener("click", function (e) {
if (e.target.classList.contains("dr-kv-del")) {
var entry = e.target.closest(".dr-list-entry");
if (entry) { e.preventDefault(); e.stopPropagation(); e.target.closest(".dr-kv-row").remove(); }
}
});
}
});
}
/** 收集所有栏目数据用于保存 */
function collectPjiSectionsData() {
var sections = pjiGetSections();
var cont = pjiContainer();
var result = [];
$$(".pji-section-card", cont || document).forEach(function (card) {
var idx = Number(card.dataset.idx);
var sec = sections[idx];
if (!sec) return;
var content;
if (sec.type === "string") {
var inp = $(".pji-content", card);
content = inp ? inp.value : "";
} else if (sec.type === "object") {
content = {};
$$(".dr-kv-row", card).forEach(function (r) {
var k = $(".dr-kv-key", r).value.trim();
var v = $(".dr-kv-val", r).value.trim();
if (k) content[k] = v;
});
} else if (sec.type === "array") {
content = [];
$$(".dr-example-item", card).forEach(function (inp) {
var v = inp.value.trim();
if (v) content.push(v);
});
} else if (sec.type === "list") {
content = [];
$$(".dr-list-entry", card).forEach(function (entry) {
var obj = {};
$$(".dr-kv-row", entry).forEach(function (r) {
var k = $(".dr-kv-key", r).value.trim();
var v = $(".dr-kv-val", r).value.trim();
if (k) obj[k] = v;
});
if (Object.keys(obj).length) content.push(obj);
});
}
result.push({ key: sec.key, label: sec.label, type: sec.type, content: content });
});
return result;
}
/** 新增栏目对话框 */
function addPjiSectionDialog() {
var typeOpts = PJI_TYPE_OPTIONS.map(function (o) {
return '<option value="' + o[0] + '">' + o[1] + "</option>";
}).join("");
var m = Core.modal({
title: "\u65b0\u589e\u680f\u76ee",
body:
'<div class="field"><label class="label">\u680f\u76ee\u540d\u79f0<span class="req">*</span></label><input id="ns-label" class="input" placeholder="\u5982\uff1a\u5468\u8fb9\u914d\u5957\u3001\u5b66\u533a\u4fe1\u606f"></div>' +
'<div class="field"><label class="label">\u5185\u5bb9\u683c\u5f0f</label><select id="ns-type" class="select">' + typeOpts + '</select></div>' +
'<div class="fs-12 c-dim">\u680f\u76ee\u6807\u8bc6\u7b26\u5c06\u6839\u636e\u540d\u79f0\u81ea\u52a8\u751f\u6210\uff0c\u5185\u5bb9\u683c\u5f0f\u4e0e\u7ef4\u5ea6\u683c\u5f0f\u4e00\u81f4\u3002</div>',
footer: '<button class="btn ghost mf-cancel">\u53d6\u6d88</button><button class="btn primary mf-ok">\u6dfb\u52a0</button>'
});
$(".mf-cancel", m.root).onclick = m.close;
$(".mf-ok", m.root).onclick = function () {
var label = $("#ns-label", m.root).value.trim();
if (!label) { Core.toast("\u8bf7\u8f93\u5165\u680f\u76ee\u540d\u79f0", "err"); return; }
var type = $("#ns-type", m.root).value;
var key = label.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, "").replace(/^[0-9]+/, "") || ("sec_" + Date.now());
var sections = pjiGetSections();
sections.push({ key: key, label: label, type: type, content: type === "string" ? "" : [] });
pjiSetSections(sections);
m.close();
drawPjiSections();
Core.toast("\u680f\u76ee\u300c" + label + "\u300d\u5df2\u6dfb\u52a0", "ok");
};
}
/** AI 预设项目资料对话框 */
function aiPresetDialog() {
var base = state.detail.Base || {};
// 拉取模型配置列表
Core.api("modelsup/list", { page: 1, page_size: 50 }).then(function (data) {
var sups = (data && data.list) || [];
if (sups.length === 0) {
Core.toast("尚未配置模型提供方,请先到「模型配置」添加", "err");
return;
}
var options = sups.map(function (s) {
var selected = (s.sup_id === base.model_sup_id) ? " selected" : "";
return '<option value="' + s.sup_id + '"' + selected + '>#' + s.sup_id + " " + Core.esc(s.sup_name) + " · " + Core.esc(s.json_model || "") + "</option>";
}).join("");
var m = Core.modal({
title: "✦ AI 预设项目资料",
width: "wide",
body:
'<div class="card-sub" style="margin-bottom:12px">AI 将根据项目名称和描述,自动生成 2~4 个项目资料栏目。生成后可在编辑器中继续调整。</div>' +
'<div class="field"><label class="label">模型配置<span class="req">*</span></label><select id="aip-sup" class="select">' + options + '</select></div>' +
'<div class="grid cols-2">' +
'<div class="field"><label class="label">项目名称</label><input class="input" disabled value="' + Core.esc(base.name || "") + '"></div>' +
'<div class="field"><label class="label">项目描述</label><input class="input" disabled value="' + Core.esc((base.desc || "").substring(0, 80)) + '"></div>' +
'</div>' +
'<div class="field"><label class="label">生成结果 <span class="c-dim fs-12">(AI 生成的栏目数据预览)</span></label>' +
'<textarea id="aip-result" class="textarea json" style="min-height:220px" placeholder="点击「开始生成」,AI 将根据项目信息自动生成资料栏目"></textarea></div>',
footer: '<button class="btn ghost mf-close">关闭</button>' +
'<button class="btn primary mf-gen">⟳ 开始生成</button>' +
'<button class="btn success mf-apply" disabled>应用到项目资料</button>'
});
var resultEl = $("#aip-result", m.root);
$(".mf-close", m.root).onclick = m.close;
$(".mf-gen", m.root).onclick = function () {
var supId = Number($("#aip-sup", m.root).value);
if (!supId) { Core.toast("请选择模型配置", "err"); return; }
Core.loading(true, "AI 生成中,请稍候…");
Core.api("project/data/generate", { projectId: state.project.projectId, supId: supId })
.then(function (data) {
Core.loading(false);
// data 是后端返回的展平 map,直接格式化显示
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);
// 将展平数据转为栏目数组
var newSections = buildSectionsFromData(dataObj);
if (!newSections.length) {
Core.toast("未解析到有效栏目", "err");
return;
}
// 先同步当前 DOM 数据,再追加新栏目
syncPjiSectionsFromDom();
var existing = state.pjiSections || [];
state.pjiSections = existing.concat(newSections);
drawPjiSections();
m.close();
Core.toast("已添加 " + newSections.length + " 个栏目,请检查后保存", "ok");
} catch (e) {
Core.toast("解析生成结果失败:" + e.message, "err");
}
};
}).catch(function (e) { Core.toast(e.message, "err"); });
}