第一周作业:对JSONEditor的二次开发
JSONEditor 的优点与局限
JSONEditor适合承担各类json编辑工作,其优点在于结构展示清晰、编辑方式直观、集成成本较低,并且便于对关键字段进行只读控制;但其本质上仍是json编辑工具,它在资产管理上仍有不足,例如在编辑大规模知识图谱时,用户往往难以快速找到并对应到需要编辑的具体节点,因此需要额外配合搜索与高亮定位机制来弥补这一不足。
代码引用与改良
该项目提供的是一个可以直接写于html前端模块,因此我通过对前端内容的重新组装,并搭建一个python服务端来补充功能。
引用JsonEditor
其中,对JsonEditor实例的引用为:
function initJsonEditor() {
const container = document.getElementById("jsoneditor");
editor = new JSONEditor(container, {
mode: "tree",
modes: ["tree", "form", "view", "code"],
mainMenuBar: false,
navigationBar: false,
statusBar: false,
onEditable: function(node) {
if (currentSelection.type !== "node") return true;
const field = node.field;
if (field === "id" || field === "table" || field === "entityType") {
return { field: false, value: false };
}
return true;
},
onError: function(error) {
console.error(error);
setEditorStatus("编辑器错误:" + error.message, true);
}
});
editor.set({ message: "请先在左侧选择一个节点" });
}
1、去除了上方搜索栏,单独制作搜索
mainMenuBar: false,
navigationBar: false,
statusBar: false,
2、保留了图谱中必要的保留字段
if (field === "id" || field === "table" || field === "entityType") {
return { field: false, value: false };
}
改良搜索
我总体使用levenshtein模糊搜索算法。通过修改前端页面、编写KuzuDB查询函数、编写服务端HTTP端口来实现这种效果
1、前端页面部分代码
新编写的搜索栏:
<div class="graph-tools">
<input id="searchInput" class="search-input" type="text" placeholder="按 name 模糊搜索节点..." />
<button id="btnSearch">搜索</button>
<button id="btnClearSearch">清空搜索</button>
<div id="searchResults" class="search-results" style="display:none;"></div>
</div>
与后端搜索的绑定:
async function searchNodes() {
const keyword = searchInput.value.trim();
if (!keyword) {
setGraphStatus("请输入搜索关键词。", true);
return;
}
try {
const results = await apiGet(`/api/search?keyword=${encodeURIComponent(keyword)}`);
renderSearchResults(results);
setGraphStatus(`搜索完成,共找到 ${results.length} 个候选结果。`);
} catch (error) {
console.error(error);
setGraphStatus("搜索失败:" + error.message, true);
}
}
搜索完成后高亮闪烁对应结点:
function renderSearchResults(results) {
if (!results || results.length === 0) {
searchResultsEl.style.display = "block";
searchResultsEl.innerHTML = `<div class="search-item">没有搜索到结果。</div>`;
return;
}
searchResultsEl.style.display = "block";
searchResultsEl.innerHTML = results.map(item => `
<div class="search-item" data-node-id="${escapeHtml(item.id)}" data-table="${escapeHtml(item.table)}">
${escapeHtml(item.name)} | ${escapeHtml(item.table)} | id=${escapeHtml(item.id)} | 距离=${item.distance}
</div>
`).join("");
searchResultsEl.querySelectorAll(".search-item").forEach(el => {
el.addEventListener("click", async () => {
const nodeId = el.getAttribute("data-node-id");
const table = el.getAttribute("data-table");
if (network && visNodes.get(nodeId)) {
network.selectNodes([nodeId]);
network.focus(nodeId, {
scale: 1.2,
animation: {
duration: 600,
easingFunction: "easeInOutQuad"
}
});
}
startBlinkHighlight(nodeId);
await handleNodeSelect(table, nodeId);
setGraphStatus(`已定位搜索结果:${table} / ${nodeId}`);
});
});
}
2、后端搜索功能实现
路由/api/search的调用如下:
def _api_search(self, query: str):
params = parse_qs(query)
keyword = self._get_required_query_param(params, "keyword")
client = KuzuClient()
data = client.search_nodes_by_name(keyword)
json_response(self, 200, {"ok": True, "data": data})
client.search_nodes_by_name函数的具体实现如下:
def search_nodes_by_name(self, keyword: str, limit: int = 20) -> List[Dict[str, Any]]:
if not isinstance(keyword, str) or not keyword.strip():
return []
keyword = keyword.strip()
results: List[Dict[str, Any]] = []
for table in self.get_node_tables():
columns = self.get_table_columns(table)
if "id" not in columns or "name" not in columns:
continue
cypher = f"MATCH (n:{table}) RETURN n.id AS id, n.name AS name;"
rows = self.fetch_all_dict(cypher)
for row in rows:
node_id = row.get("id")
name = row.get("name")
if not isinstance(node_id, str):
continue
if not isinstance(name, str):
continue
distance = self._levenshtein(keyword.lower(), name.lower())
results.append({
"id": node_id,
"name": name,
"table": table,
"distance": distance
})
results.sort(key=lambda x: (x["distance"], len(x["name"]), x["name"]))
return results[:limit]
def _levenshtein(self, a: str, b: str) -> int:
if a == b:
return 0
if len(a) == 0:
return len(b)
if len(b) == 0:
return len(a)
prev = list(range(len(b) + 1))
for i, ca in enumerate(a, start=1):
curr = [i]
for j, cb in enumerate(b, start=1):
cost = 0 if ca == cb else 1
curr.append(min(
prev[j] + 1,
curr[j - 1] + 1,
prev[j - 1] + cost
))
prev = curr
return prev[-1]
效果展示
总体效果
下图为本项目加载了一个图数据库的后的效果。左侧为图谱的总览图,左上为我添加的搜索功能。右侧为编辑页面,右上为JsonEditor自带的编辑框,右下为各类图编辑功能(下列截图没有翻到底,底部有更多我的图谱编辑功能)

特别功能演示
模糊搜索功能
1、搜索时按照levenshtein距离找到结点

2、通过搜索选中或直接选中的结点与相关关系会高亮闪烁,让编辑者更明显地找到相关结点。

json编辑功能
此为JSONEditor项目的自带功能。以温度结点为例,entityType、table、id均是只读,而min_temp等则可编辑(选中后可以看到黄色输入框)

图谱编辑功能
有常规的添加、删除各种类结点、关系的功能。通过配合搜索更便于找到内容。


浙公网安备 33010602011771号