1017 lines
43 KiB
JavaScript
1017 lines
43 KiB
JavaScript
const STORAGE_KEY = "traceability-mvp-data-v3";
|
||
const FIELD_TYPES = ["string", "integer", "char", "datetime", "json", "image", "video_url", "link", "select", "multi_select"];
|
||
|
||
const seedState = {
|
||
templates: [
|
||
{
|
||
id: "tpl-silk-v1",
|
||
name: "蚕丝被标准模板 V1",
|
||
description: "适用于蚕丝被产品的一批一码溯源模板。",
|
||
nodes: [
|
||
{ id: "tn-1", source: "library", nodeId: "biz-order" },
|
||
{ id: "tn-2", source: "library", nodeId: "biz-farmer" },
|
||
{ id: "tn-3", source: "library", nodeId: "biz-grow" },
|
||
{ id: "tn-4", source: "library", nodeId: "biz-buy" },
|
||
{ id: "tn-5", source: "library", nodeId: "biz-silk" },
|
||
{ id: "tn-6", source: "library", nodeId: "biz-manufacture" },
|
||
{ id: "tn-7", source: "library", nodeId: "biz-inspection" },
|
||
{ id: "tn-8", source: "library", nodeId: "biz-package" },
|
||
{ id: "tn-9", source: "library", nodeId: "pub-company" },
|
||
{ id: "tn-10", source: "library", nodeId: "pub-county" },
|
||
{ id: "tn-11", source: "library", nodeId: "pub-organic" }
|
||
]
|
||
}
|
||
],
|
||
nodeLibrary: [
|
||
{
|
||
id: "biz-order",
|
||
category: "business",
|
||
name: "订种信息",
|
||
description: "记录订种单、所属乡镇、蚕品种等基础信息。",
|
||
consumerVisible: true,
|
||
fields: [
|
||
{ key: "town", label: "所属乡镇", type: "string", required: true, visible: true, defaultValue: "河西乡" },
|
||
{ key: "silkwormBreed", label: "蚕品种", type: "string", required: true, visible: true, defaultValue: "桂蚕 8 号" },
|
||
{ key: "orderNo", label: "订种单号", type: "string", required: true, visible: true, defaultValue: "DZ-2026-001" },
|
||
{ key: "orderDate", label: "订种日期", type: "datetime", required: true, visible: true, defaultValue: "2026-03-05T09:00" }
|
||
]
|
||
},
|
||
{
|
||
id: "biz-farmer",
|
||
category: "business",
|
||
name: "农户信息",
|
||
description: "记录蚕农、合作社、所在村镇等信息。",
|
||
consumerVisible: true,
|
||
fields: [
|
||
{ key: "farmerName", label: "蚕农姓名", type: "string", required: true, visible: true, defaultValue: "周志远" },
|
||
{ key: "coopName", label: "合作社名称", type: "string", required: true, visible: true, defaultValue: "锦绣桑蚕合作社" },
|
||
{ key: "village", label: "所在村镇", type: "string", required: false, visible: true, defaultValue: "新桥村" },
|
||
{ key: "contact", label: "联系电话", type: "char", required: false, visible: false, defaultValue: "13800001234" }
|
||
]
|
||
},
|
||
{
|
||
id: "biz-grow",
|
||
category: "business",
|
||
name: "共育情况",
|
||
description: "记录共育基地、规模、环境与照片。",
|
||
consumerVisible: true,
|
||
fields: [
|
||
{ key: "baseName", label: "基地名称", type: "string", required: true, visible: true, defaultValue: "春晖共育基地" },
|
||
{ key: "scale", label: "规模", type: "integer", required: true, visible: true, defaultValue: "320" },
|
||
{ key: "environment", label: "环境说明", type: "string", required: false, visible: true, defaultValue: "恒温恒湿,独立消杀管理" },
|
||
{ key: "baseImage", label: "基地照片", type: "image", required: false, visible: true, defaultValue: "https://images.unsplash.com/photo-1501004318641-b39e6451bec6?auto=format&fit=crop&w=900&q=80" }
|
||
]
|
||
},
|
||
{
|
||
id: "biz-buy",
|
||
category: "business",
|
||
name: "收购信息",
|
||
description: "记录收购单、质量等级、采收时间和现场图片。",
|
||
consumerVisible: true,
|
||
fields: [
|
||
{ key: "purchaseNo", label: "收购单号", type: "string", required: true, visible: true, defaultValue: "SG-2026-0331" },
|
||
{ key: "grade", label: "质量等级", type: "select", required: true, visible: true, defaultValue: "A", options: ["A", "B", "C"] },
|
||
{ key: "harvestTime", label: "采收时间", type: "datetime", required: true, visible: true, defaultValue: "2026-03-31T11:20" },
|
||
{ key: "purchaseImage", label: "收购照片", type: "image", required: false, visible: true, defaultValue: "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?auto=format&fit=crop&w=900&q=80" }
|
||
]
|
||
},
|
||
{
|
||
id: "biz-silk",
|
||
category: "business",
|
||
name: "制丝/家纺",
|
||
description: "记录原料领用、操作人员、批次和庄口。",
|
||
consumerVisible: true,
|
||
fields: [
|
||
{ key: "materialTime", label: "原料领用时间", type: "datetime", required: true, visible: true, defaultValue: "2026-04-02T08:40" },
|
||
{ key: "operator", label: "操作人员", type: "string", required: true, visible: true, defaultValue: "韩海燕" },
|
||
{ key: "lotNo", label: "批次", type: "string", required: true, visible: true, defaultValue: "ZS-2026-010" },
|
||
{ key: "station", label: "庄口", type: "string", required: false, visible: true, defaultValue: "一庄口" }
|
||
]
|
||
},
|
||
{
|
||
id: "biz-manufacture",
|
||
category: "business",
|
||
name: "制造信息",
|
||
description: "记录缝制、填充量、规格和工艺。",
|
||
consumerVisible: true,
|
||
fields: [
|
||
{ key: "sewInfo", label: "缝制信息", type: "string", required: true, visible: true, defaultValue: "双针锁边,分区定格" },
|
||
{ key: "fillWeight", label: "蚕丝被填充量(g)", type: "integer", required: true, visible: true, defaultValue: "3000" },
|
||
{ key: "craft", label: "工艺", type: "string", required: false, visible: true, defaultValue: "手工拉网 + 定位绗缝" },
|
||
{ key: "size", label: "规格", type: "string", required: false, visible: true, defaultValue: "220cm × 240cm" }
|
||
]
|
||
},
|
||
{
|
||
id: "biz-inspection",
|
||
category: "business",
|
||
name: "质检信息",
|
||
description: "记录检测项目、报告、合格证明和质检时间。",
|
||
consumerVisible: true,
|
||
fields: [
|
||
{ key: "testItems", label: "检测项目", type: "multi_select", required: true, visible: true, defaultValue: ["纤维含量", "含水率"], options: ["纤维含量", "含水率", "外观", "清洁度"] },
|
||
{ key: "report", label: "检测报告", type: "link", required: false, visible: true, defaultValue: "https://example.com/report/2026-silk-01" },
|
||
{ key: "certificateImage", label: "合格证明", type: "image", required: false, visible: true, defaultValue: "https://images.unsplash.com/photo-1455390582262-044cdead277a?auto=format&fit=crop&w=900&q=80" },
|
||
{ key: "testDate", label: "质检日期", type: "datetime", required: true, visible: true, defaultValue: "2026-04-05T14:10" }
|
||
]
|
||
},
|
||
{
|
||
id: "biz-package",
|
||
category: "business",
|
||
name: "包装信息",
|
||
description: "记录包装时间、包装方式和成品图片。",
|
||
consumerVisible: true,
|
||
fields: [
|
||
{ key: "packageTime", label: "包装时间", type: "datetime", required: true, visible: true, defaultValue: "2026-04-06T16:00" },
|
||
{ key: "packageStyle", label: "包装方式", type: "string", required: false, visible: true, defaultValue: "礼盒包装 + 防潮袋" },
|
||
{ key: "productImage", label: "成品照片", type: "image", required: false, visible: true, defaultValue: "https://images.unsplash.com/photo-1616627547584-bf28cee262db?auto=format&fit=crop&w=900&q=80" }
|
||
]
|
||
},
|
||
{
|
||
id: "pub-company",
|
||
category: "public",
|
||
name: "企业信息",
|
||
description: "固定展示品牌主体、工厂信息和联系方式。",
|
||
consumerVisible: true,
|
||
fields: [
|
||
{ key: "factoryName", label: "加工厂名称", type: "string", required: true, visible: true, defaultValue: "广西锦绣桑蚕实业有限公司" },
|
||
{ key: "address", label: "地址", type: "string", required: true, visible: true, defaultValue: "广西象州县工业园桑蚕家纺产业区 8 号" },
|
||
{ key: "qualification", label: "资质", type: "string", required: false, visible: true, defaultValue: "ISO9001 / 农产品加工示范企业" },
|
||
{ key: "contact", label: "联系方式", type: "string", required: false, visible: true, defaultValue: "0772-8888666" }
|
||
]
|
||
},
|
||
{
|
||
id: "pub-county",
|
||
category: "public",
|
||
name: "县域情况",
|
||
description: "展示县域产业特色和区域优势。",
|
||
consumerVisible: true,
|
||
fields: [
|
||
{ key: "countyName", label: "县域名称", type: "string", required: true, visible: true, defaultValue: "象州县" },
|
||
{ key: "countyIntro", label: "县域介绍", type: "string", required: true, visible: true, defaultValue: "桑蚕养殖基础扎实,形成从蚕种、养殖、制丝到家纺的完整链条。" },
|
||
{ key: "countyTag", label: "特色标签", type: "string", required: false, visible: true, defaultValue: "国家现代农业示范区" }
|
||
]
|
||
},
|
||
{
|
||
id: "pub-organic",
|
||
category: "public",
|
||
name: "有机证书",
|
||
description: "展示证书编号、发证机构和有效期。",
|
||
consumerVisible: true,
|
||
fields: [
|
||
{ key: "certName", label: "证书名称", type: "string", required: true, visible: true, defaultValue: "有机产品认证证书" },
|
||
{ key: "certNo", label: "证书编号", type: "string", required: true, visible: true, defaultValue: "ORG-2026-99881" },
|
||
{ key: "issuer", label: "发证机构", type: "string", required: false, visible: true, defaultValue: "中国质量认证中心" },
|
||
{ key: "validUntil", label: "有效期至", type: "datetime", required: false, visible: true, defaultValue: "2027-12-31T00:00" }
|
||
]
|
||
}
|
||
],
|
||
traces: [
|
||
{
|
||
id: "trace-2026-silk-001",
|
||
templateId: "tpl-silk-v1",
|
||
name: "2026 春季蚕丝被 001 批",
|
||
code: "TR-2026-001",
|
||
status: "已发布",
|
||
currentIndex: 10,
|
||
scans: 286,
|
||
nodeData: {}
|
||
}
|
||
]
|
||
};
|
||
|
||
let state = loadState();
|
||
seedState.traces[0].nodeData = createNodeData(seedState.templates[0], seedState.nodeLibrary);
|
||
state = normalizeState(state);
|
||
|
||
let ui = {
|
||
page: "templates",
|
||
libraryTab: "business",
|
||
consumerTab: "trace",
|
||
templateId: state.templates[0]?.id || "",
|
||
traceId: state.traces[0]?.id || "",
|
||
templateNodeId: state.templates[0]?.nodes[0]?.id || "",
|
||
libraryNodeId: state.nodeLibrary.find((item) => item.category === "business")?.id || "",
|
||
consumerQuery: state.traces[0]?.code || "",
|
||
dragTemplateNodeId: ""
|
||
};
|
||
|
||
function loadState() {
|
||
try {
|
||
const raw = localStorage.getItem(STORAGE_KEY);
|
||
const parsed = raw ? JSON.parse(raw) : null;
|
||
if (parsed?.templates && parsed?.nodeLibrary && parsed?.traces) return parsed;
|
||
} catch (error) {
|
||
console.warn("load failed", error);
|
||
}
|
||
return structuredClone(seedState);
|
||
}
|
||
|
||
function normalizeState(input) {
|
||
const cloned = structuredClone(input);
|
||
cloned.templates.forEach((template) => {
|
||
template.nodes.forEach((item) => {
|
||
if (!item.id) item.id = `tn-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
||
});
|
||
});
|
||
cloned.traces.forEach((trace) => {
|
||
const template = cloned.templates.find((item) => item.id === trace.templateId);
|
||
if (!trace.nodeData || Object.keys(trace.nodeData).length === 0) {
|
||
trace.nodeData = createNodeData(template, cloned.nodeLibrary);
|
||
}
|
||
});
|
||
return cloned;
|
||
}
|
||
|
||
function saveState() {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||
}
|
||
|
||
function resetDemo() {
|
||
localStorage.removeItem(STORAGE_KEY);
|
||
state = normalizeState(structuredClone(seedState));
|
||
ui = {
|
||
page: "templates",
|
||
libraryTab: "business",
|
||
consumerTab: "trace",
|
||
templateId: state.templates[0]?.id || "",
|
||
traceId: state.traces[0]?.id || "",
|
||
templateNodeId: state.templates[0]?.nodes[0]?.id || "",
|
||
libraryNodeId: state.nodeLibrary.find((item) => item.category === "business")?.id || "",
|
||
consumerQuery: state.traces[0]?.code || "",
|
||
dragTemplateNodeId: ""
|
||
};
|
||
saveState();
|
||
render();
|
||
}
|
||
|
||
const $ = (selector, root = document) => root.querySelector(selector);
|
||
const $$ = (selector, root = document) => Array.from(root.querySelectorAll(selector));
|
||
|
||
function getTemplate(id) {
|
||
return state.templates.find((item) => item.id === id);
|
||
}
|
||
|
||
function getTrace(id) {
|
||
return state.traces.find((item) => item.id === id);
|
||
}
|
||
|
||
function getLibraryNode(id) {
|
||
return state.nodeLibrary.find((item) => item.id === id);
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return String(value ?? "")
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'");
|
||
}
|
||
|
||
function formatValue(value, field) {
|
||
if (value === undefined || value === null || value === "") return "未填写";
|
||
if (Array.isArray(value)) return value.join("、");
|
||
if (field?.type === "datetime") return String(value).replace("T", " ");
|
||
return String(value);
|
||
}
|
||
|
||
function totalScans() {
|
||
return state.traces.reduce((sum, item) => sum + (item.scans || 0), 0);
|
||
}
|
||
|
||
function nodeRefToModel(nodeRef) {
|
||
if (!nodeRef) return null;
|
||
return nodeRef.source === "library" ? getLibraryNode(nodeRef.nodeId) : nodeRef.node;
|
||
}
|
||
|
||
function isPublicNode(nodeRef) {
|
||
return nodeRefToModel(nodeRef)?.category === "public";
|
||
}
|
||
|
||
function createNodeData(template, library = state.nodeLibrary) {
|
||
const data = {};
|
||
if (!template) return data;
|
||
template.nodes.forEach((nodeRef) => {
|
||
const node = nodeRef.source === "library"
|
||
? library.find((item) => item.id === nodeRef.nodeId)
|
||
: nodeRef.node;
|
||
if (!node) return;
|
||
data[nodeRef.id] = {};
|
||
node.fields.forEach((field) => {
|
||
data[nodeRef.id][field.key] = Array.isArray(field.defaultValue) ? [...field.defaultValue] : (field.defaultValue ?? "");
|
||
});
|
||
});
|
||
return data;
|
||
}
|
||
|
||
function createEmptyNode(category = "business") {
|
||
return {
|
||
category,
|
||
name: category === "public" ? "新公共资料块" : "新业务节点",
|
||
description: "请完善节点说明与字段。",
|
||
consumerVisible: true,
|
||
fields: [
|
||
{ key: "field1", label: "字段一", type: "string", required: true, visible: true, defaultValue: "" }
|
||
]
|
||
};
|
||
}
|
||
|
||
function cloneNode(node) {
|
||
return structuredClone(node);
|
||
}
|
||
|
||
function templateNodeLabel(nodeRef) {
|
||
const node = nodeRefToModel(nodeRef);
|
||
return node?.name || "未命名节点";
|
||
}
|
||
|
||
function templateNodeTypeLabel(nodeRef) {
|
||
if (!nodeRef) return "未选择节点";
|
||
if (nodeRef.source === "custom") return "临时节点";
|
||
return isPublicNode(nodeRef) ? "公共资料块" : "业务节点";
|
||
}
|
||
|
||
function nextId(prefix) {
|
||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||
}
|
||
|
||
function createTemplate() {
|
||
const template = {
|
||
id: nextId("tpl"),
|
||
name: "新模板",
|
||
description: "请完善模板说明并编排节点。",
|
||
nodes: []
|
||
};
|
||
state.templates.unshift(template);
|
||
ui.templateId = template.id;
|
||
ui.templateNodeId = "";
|
||
saveState();
|
||
render();
|
||
}
|
||
|
||
function createTrace() {
|
||
const template = getTemplate(ui.templateId);
|
||
if (!template) return;
|
||
const code = `TR-${new Date().getFullYear()}-${String(Date.now()).slice(-6)}`;
|
||
const trace = {
|
||
id: nextId("trace"),
|
||
templateId: template.id,
|
||
name: `${template.name} 新批次`,
|
||
code,
|
||
status: "进行中",
|
||
currentIndex: 0,
|
||
scans: 0,
|
||
nodeData: createNodeData(template)
|
||
};
|
||
state.traces.unshift(trace);
|
||
ui.traceId = trace.id;
|
||
ui.consumerQuery = trace.code;
|
||
saveState();
|
||
render();
|
||
}
|
||
|
||
function addLibraryNodeToTemplate(category) {
|
||
const template = getTemplate(ui.templateId);
|
||
const selectedId = $(`#template-add-${category}`)?.value;
|
||
if (!template || !selectedId) return;
|
||
template.nodes.push({ id: nextId("tn"), source: "library", nodeId: selectedId });
|
||
ui.templateNodeId = template.nodes[template.nodes.length - 1].id;
|
||
state.traces.filter((trace) => trace.templateId === template.id).forEach((trace) => {
|
||
trace.nodeData[ui.templateNodeId] = {};
|
||
const node = getLibraryNode(selectedId);
|
||
node.fields.forEach((field) => {
|
||
trace.nodeData[ui.templateNodeId][field.key] = Array.isArray(field.defaultValue) ? [...field.defaultValue] : (field.defaultValue ?? "");
|
||
});
|
||
});
|
||
saveState();
|
||
render();
|
||
}
|
||
|
||
function addCustomTemplateNode() {
|
||
const template = getTemplate(ui.templateId);
|
||
if (!template) return;
|
||
const nodeRef = { id: nextId("tn"), source: "custom", node: createEmptyNode("business") };
|
||
template.nodes.push(nodeRef);
|
||
ui.templateNodeId = nodeRef.id;
|
||
state.traces.filter((trace) => trace.templateId === template.id).forEach((trace) => {
|
||
trace.nodeData[nodeRef.id] = { field1: "" };
|
||
});
|
||
saveState();
|
||
render();
|
||
}
|
||
|
||
function addLibraryNode(category) {
|
||
const node = { id: nextId(category === "public" ? "pub" : "biz"), ...createEmptyNode(category) };
|
||
state.nodeLibrary.unshift(node);
|
||
ui.libraryNodeId = node.id;
|
||
ui.libraryTab = category;
|
||
saveState();
|
||
render();
|
||
}
|
||
|
||
function addFieldToNode(target) {
|
||
const node = target();
|
||
if (!node) return;
|
||
const index = node.fields.length + 1;
|
||
node.fields.push({ key: `field${index}`, label: `字段 ${index}`, type: "string", required: false, visible: true, defaultValue: "" });
|
||
saveState();
|
||
render();
|
||
}
|
||
|
||
function removeTemplateNode() {
|
||
const template = getTemplate(ui.templateId);
|
||
if (!template || !ui.templateNodeId) return;
|
||
template.nodes = template.nodes.filter((item) => item.id !== ui.templateNodeId);
|
||
ui.templateNodeId = template.nodes[0]?.id || "";
|
||
saveState();
|
||
render();
|
||
}
|
||
|
||
function saveTemplateMeta() {
|
||
const template = getTemplate(ui.templateId);
|
||
if (!template) return;
|
||
template.name = ($("#template-name")?.value || "").trim() || template.name;
|
||
template.description = ($("#template-description")?.value || "").trim();
|
||
saveState();
|
||
render();
|
||
}
|
||
|
||
function currentTemplateNodeRef() {
|
||
const template = getTemplate(ui.templateId);
|
||
return template?.nodes.find((item) => item.id === ui.templateNodeId) || null;
|
||
}
|
||
|
||
function currentLibraryNode() {
|
||
return getLibraryNode(ui.libraryNodeId);
|
||
}
|
||
|
||
function saveNodeMeta(target) {
|
||
const node = target();
|
||
if (!node) return;
|
||
node.name = ($("#node-name")?.value || "").trim() || node.name;
|
||
node.description = ($("#node-description")?.value || "").trim();
|
||
node.consumerVisible = Boolean($("#node-visible")?.checked);
|
||
saveState();
|
||
render();
|
||
}
|
||
|
||
function saveTraceBase() {
|
||
const trace = getTrace(ui.traceId);
|
||
if (!trace) return;
|
||
trace.name = ($("#trace-name")?.value || "").trim() || trace.name;
|
||
trace.code = ($("#trace-code")?.value || "").trim() || trace.code;
|
||
ui.consumerQuery = trace.code;
|
||
saveState();
|
||
render();
|
||
}
|
||
|
||
function saveCurrentTraceNode() {
|
||
const trace = getTrace(ui.traceId);
|
||
const template = trace ? getTemplate(trace.templateId) : null;
|
||
const nodeRef = template?.nodes[trace.currentIndex];
|
||
const node = nodeRefToModel(nodeRef);
|
||
if (!trace || !nodeRef || !node) return;
|
||
const values = trace.nodeData[nodeRef.id] || {};
|
||
let valid = true;
|
||
node.fields.forEach((field) => {
|
||
const nextValue = field.type === "multi_select"
|
||
? $$(`[data-field-key="${field.key}"]:checked`).map((item) => item.value)
|
||
: ($(`[data-field-key="${field.key}"]`)?.value ?? "");
|
||
if (field.required && (!nextValue || (Array.isArray(nextValue) && nextValue.length === 0))) valid = false;
|
||
values[field.key] = nextValue;
|
||
});
|
||
if (!valid) return alert("请先补全当前节点的必填字段。");
|
||
trace.nodeData[nodeRef.id] = values;
|
||
if (trace.currentIndex < template.nodes.length - 1) trace.currentIndex += 1;
|
||
trace.status = trace.currentIndex >= template.nodes.length - 1 ? "待发布" : "进行中";
|
||
saveState();
|
||
render();
|
||
}
|
||
|
||
function publishTrace() {
|
||
const trace = getTrace(ui.traceId);
|
||
if (!trace) return;
|
||
trace.status = "已发布";
|
||
trace.scans += 1;
|
||
saveState();
|
||
render();
|
||
}
|
||
|
||
function moveTemplateNode(fromId, toId) {
|
||
const template = getTemplate(ui.templateId);
|
||
if (!template || fromId === toId) return;
|
||
const fromIndex = template.nodes.findIndex((item) => item.id === fromId);
|
||
const toIndex = template.nodes.findIndex((item) => item.id === toId);
|
||
if (fromIndex < 0 || toIndex < 0) return;
|
||
const [moved] = template.nodes.splice(fromIndex, 1);
|
||
template.nodes.splice(toIndex, 0, moved);
|
||
saveState();
|
||
render();
|
||
}
|
||
|
||
function updateField(target, index, patch) {
|
||
const node = target();
|
||
if (!node?.fields[index]) return;
|
||
node.fields[index] = { ...node.fields[index], ...patch };
|
||
if (["select", "multi_select"].includes(node.fields[index].type) && !node.fields[index].options) {
|
||
node.fields[index].options = ["选项一", "选项二"];
|
||
}
|
||
saveState();
|
||
}
|
||
|
||
function renderFieldEditor(node, editable) {
|
||
return node.fields.map((field, index) => `
|
||
<div class="field-card">
|
||
<div class="form-grid compact">
|
||
<div class="field">
|
||
<label>字段编码</label>
|
||
<input ${editable ? "" : "disabled"} data-field-index="${index}" data-field-prop="key" value="${escapeHtml(field.key)}" />
|
||
</div>
|
||
<div class="field">
|
||
<label>字段名称</label>
|
||
<input ${editable ? "" : "disabled"} data-field-index="${index}" data-field-prop="label" value="${escapeHtml(field.label)}" />
|
||
</div>
|
||
<div class="field">
|
||
<label>字段类型</label>
|
||
<select ${editable ? "" : "disabled"} data-field-index="${index}" data-field-prop="type">
|
||
${FIELD_TYPES.map((type) => `<option value="${type}" ${field.type === type ? "selected" : ""}>${type}</option>`).join("")}
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<label>默认值</label>
|
||
<input ${editable ? "" : "disabled"} data-field-index="${index}" data-field-prop="defaultValue" value="${escapeHtml(Array.isArray(field.defaultValue) ? field.defaultValue.join("、") : (field.defaultValue ?? ""))}" />
|
||
</div>
|
||
<div class="field">
|
||
<label>必填</label>
|
||
<label class="switch"><input ${editable ? "" : "disabled"} type="checkbox" data-field-index="${index}" data-field-prop="required" ${field.required ? "checked" : ""} /><span></span></label>
|
||
</div>
|
||
<div class="field">
|
||
<label>消费者可见</label>
|
||
<label class="switch"><input ${editable ? "" : "disabled"} type="checkbox" data-field-index="${index}" data-field-prop="visible" ${field.visible ? "checked" : ""} /><span></span></label>
|
||
</div>
|
||
${["select", "multi_select"].includes(field.type) ? `
|
||
<div class="field full">
|
||
<label>选项</label>
|
||
<input ${editable ? "" : "disabled"} data-field-index="${index}" data-field-prop="options" value="${escapeHtml((field.options || []).join("、"))}" />
|
||
</div>
|
||
` : ""}
|
||
</div>
|
||
</div>
|
||
`).join("");
|
||
}
|
||
|
||
function renderNodeMetaForm(node, editable, actionsHtml = "") {
|
||
return `
|
||
<div class="editor-meta">
|
||
<div class="form-grid">
|
||
<div class="field">
|
||
<label>节点名称</label>
|
||
<input id="node-name" ${editable ? "" : "disabled"} value="${escapeHtml(node.name)}" />
|
||
</div>
|
||
<div class="field">
|
||
<label>消费者可见</label>
|
||
<label class="switch"><input id="node-visible" ${editable ? "" : "disabled"} type="checkbox" ${node.consumerVisible ? "checked" : ""} /><span></span></label>
|
||
</div>
|
||
<div class="field full">
|
||
<label>节点说明</label>
|
||
<textarea id="node-description" ${editable ? "" : "disabled"}>${escapeHtml(node.description)}</textarea>
|
||
</div>
|
||
</div>
|
||
<div class="editor-actions">${actionsHtml}</div>
|
||
<div class="field-list">${renderFieldEditor(node, editable)}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderTemplatePage() {
|
||
const template = getTemplate(ui.templateId);
|
||
const currentRef = currentTemplateNodeRef();
|
||
const currentNode = nodeRefToModel(currentRef);
|
||
const businessNodes = state.nodeLibrary.filter((item) => item.category === "business");
|
||
const publicNodes = state.nodeLibrary.filter((item) => item.category === "public");
|
||
|
||
return `
|
||
<section class="content-shell two-col">
|
||
<aside class="left-pane">
|
||
<div class="pane-head">
|
||
<h2>模板</h2>
|
||
<button class="primary-btn" id="btn-new-template">新建模板</button>
|
||
</div>
|
||
<div class="scroll-list">
|
||
${state.templates.map((item) => `
|
||
<button class="list-card ${ui.templateId === item.id ? "active" : ""}" data-template-id="${item.id}">
|
||
<strong>${escapeHtml(item.name)}</strong>
|
||
<span>${item.nodes.length} 个节点</span>
|
||
</button>
|
||
`).join("")}
|
||
</div>
|
||
</aside>
|
||
<section class="right-pane">
|
||
${!template ? `<div class="empty-panel">请选择模板。</div>` : `
|
||
<div class="editor-card">
|
||
<div class="pane-head">
|
||
<h2>模板编排</h2>
|
||
<button class="ghost-btn" id="btn-save-template-meta">保存模板信息</button>
|
||
</div>
|
||
<div class="form-grid compact">
|
||
<div class="field"><label>模板名称</label><input id="template-name" value="${escapeHtml(template.name)}" /></div>
|
||
<div class="field full"><label>模板说明</label><input id="template-description" value="${escapeHtml(template.description || "")}" /></div>
|
||
</div>
|
||
<div class="template-toolbar">
|
||
<div class="inline-select"><select id="template-add-business">${businessNodes.map((item) => `<option value="${item.id}">${escapeHtml(item.name)}</option>`).join("")}</select><button class="ghost-btn" id="btn-add-business-node">添加业务节点</button></div>
|
||
<div class="inline-select"><select id="template-add-public">${publicNodes.map((item) => `<option value="${item.id}">${escapeHtml(item.name)}</option>`).join("")}</select><button class="ghost-btn" id="btn-add-public-node">添加公共资料块</button></div>
|
||
<button class="primary-btn" id="btn-add-custom-node">新增临时节点</button>
|
||
</div>
|
||
<div class="node-strip">
|
||
${template.nodes.map((nodeRef, index) => `
|
||
<button class="node-pill ${ui.templateNodeId === nodeRef.id ? "active" : ""}" draggable="true" data-template-node-id="${nodeRef.id}">
|
||
<span class="pill-order">${index + 1}</span>
|
||
<span class="pill-name">${escapeHtml(templateNodeLabel(nodeRef))}</span>
|
||
<span class="pill-tag">${templateNodeTypeLabel(nodeRef)}</span>
|
||
</button>
|
||
`).join("")}
|
||
</div>
|
||
</div>
|
||
<div class="editor-card">
|
||
<div class="pane-head">
|
||
<h2>节点信息</h2>
|
||
<div class="pane-actions">
|
||
${currentRef ? `<button class="ghost-btn danger-btn" id="btn-remove-template-node">移除节点</button>` : ""}
|
||
</div>
|
||
</div>
|
||
${!currentRef || !currentNode
|
||
? `<div class="empty-panel">点击上方节点查看详情,支持拖动切换先后顺序。</div>`
|
||
: currentRef.source === "library"
|
||
? `<div class="readonly-tip">当前节点来自节点库,只能查看,不能在模板内直接修改。</div>${renderNodeMetaForm(currentNode, false)}`
|
||
: renderNodeMetaForm(currentNode, true, `<button class="ghost-btn" id="btn-save-custom-node">保存临时节点</button><button class="ghost-btn" id="btn-add-custom-field">新增字段</button>`)}
|
||
</div>
|
||
`}
|
||
</section>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderLibraryPage() {
|
||
const currentTabNodes = state.nodeLibrary.filter((item) => item.category === ui.libraryTab);
|
||
const currentNode = currentLibraryNode();
|
||
return `
|
||
<section class="content-shell two-col">
|
||
<aside class="left-pane">
|
||
<div class="pane-head">
|
||
<div class="sub-tabs">
|
||
<button class="sub-tab ${ui.libraryTab === "business" ? "active" : ""}" data-library-tab="business">业务节点</button>
|
||
<button class="sub-tab ${ui.libraryTab === "public" ? "active" : ""}" data-library-tab="public">公共资料块</button>
|
||
</div>
|
||
<button class="primary-btn" id="btn-new-library-node">新建</button>
|
||
</div>
|
||
<div class="scroll-list">
|
||
${currentTabNodes.map((item) => `
|
||
<button class="list-card ${ui.libraryNodeId === item.id ? "active" : ""}" data-library-node-id="${item.id}">
|
||
<strong>${escapeHtml(item.name)}</strong>
|
||
<span>${item.fields.length} 个字段</span>
|
||
</button>
|
||
`).join("")}
|
||
</div>
|
||
</aside>
|
||
<section class="right-pane">
|
||
<div class="editor-card">
|
||
<div class="pane-head">
|
||
<h2>${ui.libraryTab === "business" ? "业务节点编辑器" : "公共资料块编辑器"}</h2>
|
||
<div class="pane-actions">
|
||
<button class="ghost-btn" id="btn-save-library-node">保存节点</button>
|
||
<button class="ghost-btn" id="btn-add-library-field">新增字段</button>
|
||
</div>
|
||
</div>
|
||
${currentNode ? renderNodeMetaForm(currentNode, true) : `<div class="empty-panel">请选择左侧节点开始编辑。</div>`}
|
||
</div>
|
||
</section>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderTraceField(field, value) {
|
||
if (field.type === "select") {
|
||
return `<div class="field"><label>${escapeHtml(field.label)}${field.required ? " *" : ""}</label><select data-field-key="${field.key}">${(field.options || []).map((option) => `<option value="${escapeHtml(option)}" ${value === option ? "selected" : ""}>${escapeHtml(option)}</option>`).join("")}</select></div>`;
|
||
}
|
||
if (field.type === "multi_select") {
|
||
const selected = Array.isArray(value) ? value : [];
|
||
return `<div class="field full"><label>${escapeHtml(field.label)}${field.required ? " *" : ""}</label><div class="chips">${(field.options || []).map((option) => `<label class="chip-check"><input type="checkbox" data-field-key="${field.key}" value="${escapeHtml(option)}" ${selected.includes(option) ? "checked" : ""} /> ${escapeHtml(option)}</label>`).join("")}</div></div>`;
|
||
}
|
||
const type = field.type === "datetime" ? "datetime-local" : field.type === "integer" ? "number" : "text";
|
||
return `<div class="field ${["image", "link", "video_url", "json"].includes(field.type) ? "full" : ""}"><label>${escapeHtml(field.label)}${field.required ? " *" : ""}</label>${field.type === "json" ? `<textarea data-field-key="${field.key}">${escapeHtml(typeof value === "string" ? value : JSON.stringify(value ?? "", null, 2))}</textarea>` : `<input type="${type}" data-field-key="${field.key}" value="${escapeHtml(Array.isArray(value) ? value.join("、") : (value ?? ""))}" />`}</div>`;
|
||
}
|
||
|
||
function renderOperatorPage() {
|
||
const trace = getTrace(ui.traceId) || state.traces[0];
|
||
const template = trace ? getTemplate(trace.templateId) : null;
|
||
const currentNodeRef = template?.nodes[trace?.currentIndex || 0];
|
||
const currentNode = nodeRefToModel(currentNodeRef);
|
||
const currentValues = currentNodeRef ? trace.nodeData[currentNodeRef.id] || {} : {};
|
||
return `
|
||
<section class="content-shell operator-layout">
|
||
<aside class="left-pane">
|
||
<div class="pane-head">
|
||
<h2>批次列表</h2>
|
||
<button class="primary-btn" id="btn-new-trace">新建批次</button>
|
||
</div>
|
||
<div class="scroll-list">
|
||
${state.traces.map((item) => `
|
||
<button class="list-card ${ui.traceId === item.id ? "active" : ""}" data-trace-id="${item.id}">
|
||
<strong>${escapeHtml(item.name)}</strong>
|
||
<span>${escapeHtml(item.code)} · ${escapeHtml(item.status)}</span>
|
||
</button>
|
||
`).join("")}
|
||
</div>
|
||
</aside>
|
||
<section class="right-pane">
|
||
${!trace || !template ? `<div class="empty-panel">请先新建一个批次。</div>` : `
|
||
<div class="editor-card">
|
||
<div class="pane-head">
|
||
<h2>批次信息</h2>
|
||
<div class="pane-actions">
|
||
<button class="ghost-btn" id="btn-save-trace-base">保存基础信息</button>
|
||
<button class="primary-btn" id="btn-publish-trace">发布</button>
|
||
</div>
|
||
</div>
|
||
<div class="form-grid compact">
|
||
<div class="field"><label>批次名称</label><input id="trace-name" value="${escapeHtml(trace.name)}" /></div>
|
||
<div class="field"><label>批次码</label><input id="trace-code" value="${escapeHtml(trace.code)}" /></div>
|
||
</div>
|
||
<div class="progress-strip">
|
||
${template.nodes.map((nodeRef, index) => `
|
||
<button class="progress-step ${trace.currentIndex === index ? "active" : ""} ${index < trace.currentIndex ? "done" : ""}" data-progress-index="${index}">
|
||
<span class="step-index">${index + 1}</span>
|
||
<span class="step-name">${escapeHtml(templateNodeLabel(nodeRef))}</span>
|
||
</button>
|
||
`).join("")}
|
||
</div>
|
||
</div>
|
||
<div class="editor-card">
|
||
<div class="pane-head">
|
||
<div>
|
||
<h2>当前节点</h2>
|
||
<p class="muted-line">${escapeHtml(currentNode?.name || "")} · ${escapeHtml(templateNodeTypeLabel(currentNodeRef))}</p>
|
||
</div>
|
||
<button class="primary-btn" id="btn-save-trace-node">保存并推进</button>
|
||
</div>
|
||
${currentNode ? `<div class="form-grid">${currentNode.fields.map((field) => renderTraceField(field, currentValues[field.key])).join("")}</div>` : `<div class="empty-panel">暂无节点。</div>`}
|
||
</div>
|
||
`}
|
||
</section>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderConsumerTimeline(trace, template) {
|
||
const refs = template.nodes.filter((item) => !isPublicNode(item));
|
||
return `
|
||
<div class="timeline-v2">
|
||
${refs.map((nodeRef, index) => {
|
||
const node = nodeRefToModel(nodeRef);
|
||
const values = trace.nodeData[nodeRef.id] || {};
|
||
return `
|
||
<div class="timeline-row">
|
||
<div class="timeline-rail">
|
||
<span class="timeline-dot ${index <= trace.currentIndex ? "active" : ""}"></span>
|
||
${index < refs.length - 1 ? `<span class="timeline-line"></span>` : ""}
|
||
</div>
|
||
<div class="timeline-body">
|
||
<div class="timeline-head">
|
||
<h3>${escapeHtml(node.name)}</h3>
|
||
<span>${escapeHtml(node.description)}</span>
|
||
</div>
|
||
<div class="timeline-grid">
|
||
${node.fields.filter((field) => field.visible).map((field) => {
|
||
const value = values[field.key];
|
||
const rendered = field.type === "image" && value
|
||
? `<a href="${escapeHtml(String(value))}" target="_blank" rel="noreferrer">查看图片</a>`
|
||
: (field.type === "link" || field.type === "video_url") && value
|
||
? `<a href="${escapeHtml(String(value))}" target="_blank" rel="noreferrer">打开链接</a>`
|
||
: escapeHtml(formatValue(value, field));
|
||
return `<div class="kv-card"><span>${escapeHtml(field.label)}</span><strong>${rendered}</strong></div>`;
|
||
}).join("")}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join("")}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderConsumerMaterials(trace, template) {
|
||
const refs = template.nodes.filter((item) => isPublicNode(item));
|
||
return `
|
||
<div class="materials-grid">
|
||
${refs.map((nodeRef) => {
|
||
const node = nodeRefToModel(nodeRef);
|
||
const values = trace.nodeData[nodeRef.id] || {};
|
||
return `
|
||
<div class="material-card">
|
||
<div class="timeline-head">
|
||
<h3>${escapeHtml(node.name)}</h3>
|
||
<span>${escapeHtml(node.description)}</span>
|
||
</div>
|
||
<div class="timeline-grid">
|
||
${node.fields.filter((field) => field.visible).map((field) => `<div class="kv-card"><span>${escapeHtml(field.label)}</span><strong>${escapeHtml(formatValue(values[field.key], field))}</strong></div>`).join("")}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join("")}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderConsumerPage() {
|
||
const trace = state.traces.find((item) => item.code === ui.consumerQuery) || getTrace(ui.traceId) || state.traces[0];
|
||
const template = trace ? getTemplate(trace.templateId) : null;
|
||
return `
|
||
<section class="content-shell single-col">
|
||
<div class="editor-card">
|
||
<div class="pane-head">
|
||
<h2>消费者端</h2>
|
||
<div class="query-bar">
|
||
<input id="consumer-query" value="${escapeHtml(ui.consumerQuery)}" placeholder="请输入批次码,例如 TR-2026-001" />
|
||
<button class="primary-btn" id="btn-search-consumer">查询</button>
|
||
</div>
|
||
</div>
|
||
${!trace || !template ? `<div class="empty-panel">没有查到对应批次。</div>` : `
|
||
<div class="consumer-tabs">
|
||
<button class="sub-tab ${ui.consumerTab === "trace" ? "active" : ""}" data-consumer-tab="trace">溯源</button>
|
||
<button class="sub-tab ${ui.consumerTab === "materials" ? "active" : ""}" data-consumer-tab="materials">资料</button>
|
||
</div>
|
||
<div class="consumer-shell">
|
||
<div class="consumer-topbar">
|
||
<div>
|
||
<h2>${escapeHtml(trace.name)}</h2>
|
||
<p>${escapeHtml(trace.code)} · ${escapeHtml(trace.status)} · 累计扫码 ${trace.scans} 次</p>
|
||
</div>
|
||
</div>
|
||
${ui.consumerTab === "trace" ? renderConsumerTimeline(trace, template) : renderConsumerMaterials(trace, template)}
|
||
</div>
|
||
`}
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderStatsPage() {
|
||
return `
|
||
<section class="content-shell single-col">
|
||
<div class="stats-grid">
|
||
<div class="stats-card"><span>模板数量</span><strong>${state.templates.length}</strong></div>
|
||
<div class="stats-card"><span>节点定义</span><strong>${state.nodeLibrary.length}</strong></div>
|
||
<div class="stats-card"><span>累计扫码</span><strong>${totalScans()}</strong></div>
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function render() {
|
||
const pageHtml = {
|
||
templates: renderTemplatePage(),
|
||
library: renderLibraryPage(),
|
||
operator: renderOperatorPage(),
|
||
consumer: renderConsumerPage(),
|
||
stats: renderStatsPage()
|
||
}[ui.page];
|
||
|
||
$("#app").innerHTML = `
|
||
<main class="system-shell">
|
||
<aside class="main-nav">
|
||
<div class="brand">Trace Demo</div>
|
||
<button class="nav-btn ${ui.page === "templates" ? "active" : ""}" data-page="templates">模板</button>
|
||
<button class="nav-btn ${ui.page === "library" ? "active" : ""}" data-page="library">节点库</button>
|
||
<button class="nav-btn ${ui.page === "operator" ? "active" : ""}" data-page="operator">业务员端</button>
|
||
<button class="nav-btn ${ui.page === "consumer" ? "active" : ""}" data-page="consumer">消费者端</button>
|
||
<button class="nav-btn ${ui.page === "stats" ? "active" : ""}" data-page="stats">统计</button>
|
||
<button class="nav-btn danger" id="btn-reset-demo">重置</button>
|
||
</aside>
|
||
<section class="main-panel">${pageHtml}</section>
|
||
</main>
|
||
`;
|
||
bindEvents();
|
||
}
|
||
|
||
function bindEvents() {
|
||
$$("[data-page]").forEach((item) => {
|
||
item.onclick = () => {
|
||
ui.page = item.dataset.page;
|
||
render();
|
||
};
|
||
});
|
||
|
||
$("#btn-reset-demo")?.addEventListener("click", resetDemo);
|
||
$("#btn-new-template")?.addEventListener("click", createTemplate);
|
||
$("#btn-save-template-meta")?.addEventListener("click", saveTemplateMeta);
|
||
$("#btn-add-business-node")?.addEventListener("click", () => addLibraryNodeToTemplate("business"));
|
||
$("#btn-add-public-node")?.addEventListener("click", () => addLibraryNodeToTemplate("public"));
|
||
$("#btn-add-custom-node")?.addEventListener("click", addCustomTemplateNode);
|
||
$("#btn-remove-template-node")?.addEventListener("click", removeTemplateNode);
|
||
$("#btn-save-custom-node")?.addEventListener("click", () => saveNodeMeta(() => currentTemplateNodeRef()?.node));
|
||
$("#btn-add-custom-field")?.addEventListener("click", () => addFieldToNode(() => currentTemplateNodeRef()?.node));
|
||
|
||
$("#btn-new-library-node")?.addEventListener("click", () => addLibraryNode(ui.libraryTab));
|
||
$("#btn-save-library-node")?.addEventListener("click", () => saveNodeMeta(currentLibraryNode));
|
||
$("#btn-add-library-field")?.addEventListener("click", () => addFieldToNode(currentLibraryNode));
|
||
|
||
$("#btn-new-trace")?.addEventListener("click", createTrace);
|
||
$("#btn-save-trace-base")?.addEventListener("click", saveTraceBase);
|
||
$("#btn-save-trace-node")?.addEventListener("click", saveCurrentTraceNode);
|
||
$("#btn-publish-trace")?.addEventListener("click", publishTrace);
|
||
$("#btn-search-consumer")?.addEventListener("click", () => {
|
||
ui.consumerQuery = ($("#consumer-query")?.value || "").trim();
|
||
const trace = state.traces.find((item) => item.code === ui.consumerQuery);
|
||
if (trace) {
|
||
ui.traceId = trace.id;
|
||
trace.scans += 1;
|
||
saveState();
|
||
}
|
||
render();
|
||
});
|
||
|
||
$$("[data-template-id]").forEach((item) => {
|
||
item.onclick = () => {
|
||
ui.templateId = item.dataset.templateId;
|
||
ui.templateNodeId = getTemplate(ui.templateId)?.nodes[0]?.id || "";
|
||
render();
|
||
};
|
||
});
|
||
|
||
$$("[data-template-node-id]").forEach((item) => {
|
||
item.onclick = () => {
|
||
ui.templateNodeId = item.dataset.templateNodeId;
|
||
render();
|
||
};
|
||
item.addEventListener("dragstart", () => {
|
||
ui.dragTemplateNodeId = item.dataset.templateNodeId;
|
||
});
|
||
item.addEventListener("dragover", (event) => event.preventDefault());
|
||
item.addEventListener("drop", (event) => {
|
||
event.preventDefault();
|
||
moveTemplateNode(ui.dragTemplateNodeId, item.dataset.templateNodeId);
|
||
});
|
||
});
|
||
|
||
$$("[data-library-tab]").forEach((item) => {
|
||
item.onclick = () => {
|
||
ui.libraryTab = item.dataset.libraryTab;
|
||
ui.libraryNodeId = state.nodeLibrary.find((node) => node.category === ui.libraryTab)?.id || "";
|
||
render();
|
||
};
|
||
});
|
||
|
||
$$("[data-library-node-id]").forEach((item) => {
|
||
item.onclick = () => {
|
||
ui.libraryNodeId = item.dataset.libraryNodeId;
|
||
render();
|
||
};
|
||
});
|
||
|
||
$$("[data-trace-id]").forEach((item) => {
|
||
item.onclick = () => {
|
||
ui.traceId = item.dataset.traceId;
|
||
ui.consumerQuery = getTrace(ui.traceId)?.code || ui.consumerQuery;
|
||
render();
|
||
};
|
||
});
|
||
|
||
$$("[data-progress-index]").forEach((item) => {
|
||
item.onclick = () => {
|
||
const trace = getTrace(ui.traceId);
|
||
if (!trace) return;
|
||
trace.currentIndex = Number(item.dataset.progressIndex);
|
||
saveState();
|
||
render();
|
||
};
|
||
});
|
||
|
||
$$("[data-consumer-tab]").forEach((item) => {
|
||
item.onclick = () => {
|
||
ui.consumerTab = item.dataset.consumerTab;
|
||
render();
|
||
};
|
||
});
|
||
|
||
$$("[data-field-index]").forEach((item) => {
|
||
const eventName = item.type === "checkbox" ? "change" : "input";
|
||
item.addEventListener(eventName, () => {
|
||
const index = Number(item.dataset.fieldIndex);
|
||
const prop = item.dataset.fieldProp;
|
||
let value = item.type === "checkbox" ? item.checked : item.value;
|
||
const target = ui.page === "templates"
|
||
? () => currentTemplateNodeRef()?.node
|
||
: currentLibraryNode;
|
||
if (prop === "defaultValue") value = value;
|
||
if (prop === "options") value = value.split("、").map((part) => part.trim()).filter(Boolean);
|
||
if (prop === "defaultValue" && target()?.fields[index]?.type === "multi_select") {
|
||
value = value.split("、").map((part) => part.trim()).filter(Boolean);
|
||
}
|
||
updateField(target, index, { [prop]: value });
|
||
if (prop === "type" || prop === "options") render();
|
||
});
|
||
});
|
||
}
|
||
|
||
render();
|