|
|
<!DOCTYPE html>
|
|
|
<html lang="en">
|
|
|
|
|
|
<head>
|
|
|
<style>
|
|
|
.header-wrapper {
|
|
|
height: 88px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
font-size: 16px;
|
|
|
padding: 0 30px;
|
|
|
color: #333;
|
|
|
border-bottom: 1px solid #eee;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
|
|
|
#iframe-wrapper {
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
|
|
|
#zoomToFit-wrapper {
|
|
|
text-align: right;
|
|
|
color: #196efd;
|
|
|
text-decoration: underline;
|
|
|
padding-right: 16px;
|
|
|
padding-top: 4px;
|
|
|
}
|
|
|
|
|
|
#zoomToFit {
|
|
|
cursor: pointer;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
|
|
|
#editbtn {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
color: #196efd;
|
|
|
border: 1px solid transparent;
|
|
|
font-weight: 400;
|
|
|
padding: 3px 11px;
|
|
|
border-color: #196efd;
|
|
|
border-radius: 2px;
|
|
|
font-size: 14px;
|
|
|
box-shadow: 0 2px 0 rgb(0 0 0 / 5%);
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
|
|
|
#editbtn:hover {
|
|
|
color: #428eff;
|
|
|
border-color: #428eff;
|
|
|
}
|
|
|
|
|
|
.main-wrapper {
|
|
|
position: relative;
|
|
|
}
|
|
|
|
|
|
|
|
|
#tips {
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
z-index: 10;
|
|
|
color: #333;
|
|
|
font-size: 14px;
|
|
|
padding-left: 30px;
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
#tips div {
|
|
|
margin-bottom: 6px;
|
|
|
}
|
|
|
|
|
|
#tips span {
|
|
|
color: red;
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
|
|
|
<body id="iframe-wrapper">
|
|
|
<script src="./go.js"></script>
|
|
|
<!-- <script src="https://gojs.net/latest/release/go-debug.js"></script> -->
|
|
|
<div class="header-wrapper">
|
|
|
<div class="header-title">知识图谱</div>
|
|
|
<div id="editbtn">编辑图谱</div>
|
|
|
</div>
|
|
|
<div id="zoomToFit-wrapper">
|
|
|
<span id="zoomToFit">缩放至合适大小</span>
|
|
|
</div>
|
|
|
<div class="main-wrapper">
|
|
|
<div id="tips">
|
|
|
<div>新增节点:可点击节点上的“+”新增子节点。
|
|
|
</div>
|
|
|
<div>编辑节点:点击节点中的标题和链接,可以进行编辑,点击空白处保存编辑。
|
|
|
</div>
|
|
|
<div>删除节点:选中节点后,按“DELETE”键进行删除
|
|
|
</div>
|
|
|
<div><span>*</span>注意:删除节点会同时删除该节点下的子节点。</div>
|
|
|
</div>
|
|
|
<div id="myDiagramDiv" style="background-color: white;
|
|
|
width: 100%;
|
|
|
height: 660px;
|
|
|
position: relative;
|
|
|
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);">
|
|
|
|
|
|
<canvas tabindex="0" style="position: absolute;
|
|
|
top: 0px;
|
|
|
left: 0px;
|
|
|
z-index: 2;
|
|
|
user-select: none;
|
|
|
touch-action: none;
|
|
|
width: 1054px;
|
|
|
height: 660px;">
|
|
|
This text is displayed if your browser does not support the Canvas HTML
|
|
|
element.
|
|
|
</canvas>
|
|
|
<div style="position: absolute;
|
|
|
overflow: auto;
|
|
|
width: 1054px;
|
|
|
height: 660px;
|
|
|
z-index: 1;">
|
|
|
<div style="position: absolute; width: 1px; height: 1px"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<script src="./util.js"></script>
|
|
|
|
|
|
|
|
|
<script>
|
|
|
window.onload = async function () {
|
|
|
window.top.postMessage('iframeLoaded', '*')
|
|
|
|
|
|
};
|
|
|
|
|
|
window.addEventListener('message', receiveMessage, false);
|
|
|
|
|
|
|
|
|
//父页面传来的数据
|
|
|
let dataFromWindowTop = {
|
|
|
virtualSpaceId: '',
|
|
|
API_SERVER: '',
|
|
|
is_creator: false,
|
|
|
is_member: false,
|
|
|
isSuperAdmins: false
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const colors = [
|
|
|
'#E1F5FE',
|
|
|
'#B3E5FC',
|
|
|
'#81D4FA',
|
|
|
'#4FC3F7',
|
|
|
'#29B6F6',
|
|
|
'#03A9F4',
|
|
|
'#039BE5',
|
|
|
'#0288D1',
|
|
|
'#0277BD',
|
|
|
'#01579B',
|
|
|
];
|
|
|
|
|
|
|
|
|
const myDiagram = $(
|
|
|
go.Diagram,
|
|
|
diagramWrapperDom, // must name or refer to the DIV HTML element
|
|
|
{
|
|
|
// 去除边框颜色
|
|
|
nodeSelectionAdornmentTemplate: $(
|
|
|
go.Adornment,
|
|
|
'Auto',
|
|
|
$(go.Shape, 'Rectangle', { fill: 'black', stroke: 'black' })
|
|
|
),
|
|
|
// maxSelectionCount: 1, // 选中的节点最多为 1 个
|
|
|
initialContentAlignment: go.Spot.Center,
|
|
|
layout: $(go.ForceDirectedLayout),
|
|
|
// moving and copying nodes also moves and copies their subtrees
|
|
|
'allowDelete': false,
|
|
|
'commandHandler.copiesTree': false,
|
|
|
'commandHandler.deletesTree': false,
|
|
|
'draggingTool.dragsTree': false,
|
|
|
'undoManager.isEnabled': false,
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
//监听graph变化
|
|
|
myDiagram.addModelChangedListener(function (evt) {
|
|
|
if (!evt.isTransactionFinished) {
|
|
|
return;
|
|
|
}
|
|
|
var txn = evt.object; // a Transaction
|
|
|
if (txn === null) {
|
|
|
return;
|
|
|
}
|
|
|
txn.changes.each(async function (e) {
|
|
|
// ignore any kind of change other than adding/removing a node
|
|
|
|
|
|
if (e?.modelChange !== 'nodeDataArray') {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
|
|
|
// record node removals and insertions
|
|
|
if (e.change === go.ChangedEvent.Insert) {
|
|
|
if (evt.propertyName === 'FinishedUndo') {
|
|
|
//撤销插入,所以实际上是删除节点
|
|
|
deleteNodeRequest(dataFromWindowTop.virtualSpaceId, e.newValue.key)
|
|
|
}
|
|
|
|
|
|
|
|
|
}
|
|
|
if (e.change === go.ChangedEvent.Remove) {
|
|
|
if (evt.propertyName === 'FinishedUndo') {
|
|
|
//FinishedUndo说明是撤销删除,所以实际上是增加节点
|
|
|
// addNodeRequest(dataFromWindowTop.virtualSpaceId, e.oldValue)
|
|
|
} else {
|
|
|
const deleteResponse = await deleteNodeRequest(dataFromWindowTop.virtualSpaceId, e.oldValue.key)
|
|
|
//-2代表删除到根节点了,得加回去
|
|
|
if (deleteResponse.status === -2) {
|
|
|
try {
|
|
|
const response = await fetch(`${dataFromWindowTop.API_SERVER}/api/virtual_classrooms/${dataFromWindowTop.virtualSpaceId}/knowledge_lists.json`, { method: 'GET', credentials: 'include', mode: "cors" })
|
|
|
const res = await response.json()
|
|
|
if (!res.status) {
|
|
|
const { lists } = res
|
|
|
const rootNode = {
|
|
|
title: lists[0].title,
|
|
|
url: lists[0].url,
|
|
|
editable: true,
|
|
|
key: lists[0].id,
|
|
|
parent: -1,
|
|
|
color: colors[0]
|
|
|
}
|
|
|
myDiagram.model.addNodeData(rootNode)
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.log(error);
|
|
|
}
|
|
|
}
|
|
|
myDiagram.undoManager.clear()
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//接收消息并初始化
|
|
|
async function receiveMessage(event) {
|
|
|
const { id, API_SERVER, is_creator, isSuperAdmins, is_member } = event.data || {}
|
|
|
dataFromWindowTop.virtualSpaceId = id
|
|
|
dataFromWindowTop.API_SERVER = API_SERVER
|
|
|
dataFromWindowTop.isSuperAdmins = isSuperAdmins
|
|
|
dataFromWindowTop.is_creator = is_creator
|
|
|
dataFromWindowTop.is_member = is_member
|
|
|
getDataAndInit(id, API_SERVER)
|
|
|
|
|
|
const hasAuth = isSuperAdmins || is_creator || is_member
|
|
|
if (!hasAuth) {
|
|
|
editButton.style.display = 'none'
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
//初始化
|
|
|
async function getDataAndInit(id, ApiServer) {
|
|
|
try {
|
|
|
const response = await fetch(`${dataFromWindowTop.API_SERVER}/api/virtual_classrooms/${id}/knowledge_lists.json`, { method: 'GET', credentials: 'include', mode: "cors" })
|
|
|
const res = await response.json()
|
|
|
if (!res.status) {
|
|
|
const { lists } = res
|
|
|
const nodeDataList = lists.map(node => ({
|
|
|
key: node.id, title: node.title || '请输入标题', url: node.url || '请输入url', color: colors[0], parent: node.parent_id || -1,
|
|
|
everExpanded: true,
|
|
|
editable: false,
|
|
|
}))
|
|
|
init(nodeDataList)
|
|
|
myDiagram.zoomToFit()
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.log(error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//新增节点的请求
|
|
|
async function addNodeRequest(spaceId, nodeInfo) {
|
|
|
const requestBody = JSON.stringify({
|
|
|
title: nodeInfo.title,
|
|
|
url: nodeInfo.url,
|
|
|
knowledge_id: nodeInfo.parent
|
|
|
})
|
|
|
try {
|
|
|
const response = await fetch(`${dataFromWindowTop.API_SERVER}/api/virtual_classrooms/${spaceId}/save_knowledge.json`, {
|
|
|
method: 'POST', body: requestBody, headers: {
|
|
|
'Content-Type': 'application/json'
|
|
|
},
|
|
|
mode: 'cors',
|
|
|
credentials: 'include'
|
|
|
})
|
|
|
const res = await response.json()
|
|
|
return res
|
|
|
} catch (error) {
|
|
|
console.log(error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
//删除节点的请求
|
|
|
async function deleteNodeRequest(spaceId, nodeId) {
|
|
|
const requestBody = JSON.stringify({ knowledge_id: nodeId })
|
|
|
try {
|
|
|
const response = await fetch(`${dataFromWindowTop.API_SERVER}/api/virtual_classrooms/${spaceId}/del_knowledge.json`, {
|
|
|
method: 'DELETE', body: requestBody, headers: {
|
|
|
'Content-Type': 'application/json'
|
|
|
}, mode: "cors", credentials: "include"
|
|
|
})
|
|
|
const res = await response.json()
|
|
|
return res
|
|
|
} catch (error) {
|
|
|
console.log(error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
//更改文字的请求
|
|
|
async function updateNodeRequest(spaceId, nodeInfo) {
|
|
|
const requestBody = JSON.stringify(nodeInfo)
|
|
|
try {
|
|
|
const response = await fetch(`${dataFromWindowTop.API_SERVER}/api/virtual_classrooms/${spaceId}/update_knowledge.json`, {
|
|
|
method: 'PUT', body: requestBody, headers: {
|
|
|
'Content-Type': 'application/json'
|
|
|
}, mode: "cors", credentials: "include"
|
|
|
})
|
|
|
const res = await response.json()
|
|
|
return res
|
|
|
} catch (error) {
|
|
|
console.log(error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//初始化
|
|
|
function init(nodeData) {
|
|
|
|
|
|
//绑定model
|
|
|
|
|
|
const myModel = $(go.TreeModel);
|
|
|
// in the model data, each node is represented by a JavaScript object:
|
|
|
myModel.nodeDataArray = nodeData;
|
|
|
myDiagram.model = myModel;
|
|
|
|
|
|
myDiagram.nodeTemplate = $(
|
|
|
go.Node,
|
|
|
'Spot',
|
|
|
{
|
|
|
selectionObjectName: 'PANEL',
|
|
|
isTreeExpanded: true,
|
|
|
isTreeLeaf: false,
|
|
|
},
|
|
|
|
|
|
// the node's outer shape, which will surround the text
|
|
|
$(
|
|
|
go.Panel,
|
|
|
'Auto',
|
|
|
{ name: 'PANEL' },
|
|
|
$(
|
|
|
go.Shape,
|
|
|
'RoundedRectangle',
|
|
|
{
|
|
|
stroke: null,
|
|
|
fill: '#377dff' /* green */,
|
|
|
fromLinkable: true,
|
|
|
fromLinkableSelfNode: true,
|
|
|
fromLinkableDuplicates: true,
|
|
|
toLinkable: true,
|
|
|
toLinkableSelfNode: true,
|
|
|
toLinkableDuplicates: true,
|
|
|
cursor: 'pointer',
|
|
|
},
|
|
|
// new go.Binding('fill', 'rootdistance', (dist) => {
|
|
|
// const distance = Math.min(colors.length - 1, dist);
|
|
|
// return colors[distance];
|
|
|
// })
|
|
|
|
|
|
),
|
|
|
$(
|
|
|
go.Panel,
|
|
|
'Vertical',
|
|
|
$(
|
|
|
go.TextBlock,
|
|
|
{
|
|
|
font: '14pt sans-serif',
|
|
|
margin: 12,
|
|
|
stroke: 'white',
|
|
|
errorFunction: function (tool, olds, news) {
|
|
|
if (news.trim().length === 0) {
|
|
|
window.top.postMessage('emptyTitle', '*')
|
|
|
} else {
|
|
|
window.top.postMessage('longTitle', '*')
|
|
|
}
|
|
|
},
|
|
|
textValidation: function (instance, lastValue, currentValue) {
|
|
|
if (currentValue.length > 60 || currentValue.trim().length === 0) {
|
|
|
return false;
|
|
|
}
|
|
|
return true;
|
|
|
},
|
|
|
},
|
|
|
new go.Binding('text', 'title').makeTwoWay(),
|
|
|
new go.Binding('editable', 'editable')
|
|
|
),
|
|
|
$(
|
|
|
go.TextBlock,
|
|
|
{
|
|
|
font: '14pt sans-serif',
|
|
|
margin: 12,
|
|
|
stroke: 'white',
|
|
|
cursor: 'pointer',
|
|
|
},
|
|
|
new go.Binding('text', 'url').makeTwoWay(),
|
|
|
new go.Binding('editable', 'editable'),
|
|
|
{
|
|
|
click: function (e, obj) {
|
|
|
if (editable) {
|
|
|
return
|
|
|
}
|
|
|
if (!isValidUrl(obj.text)) {
|
|
|
window.top.postMessage('invalidUrl', '*')
|
|
|
return;
|
|
|
}
|
|
|
openNewWindow(obj.text);
|
|
|
},
|
|
|
}
|
|
|
)
|
|
|
)
|
|
|
),
|
|
|
// 添加按钮
|
|
|
$(
|
|
|
'Button',
|
|
|
$(go.Shape, 'PlusLine', { width: 10, height: 10 }),
|
|
|
|
|
|
{
|
|
|
name: 'TREEBUTTON',
|
|
|
width: 20,
|
|
|
height: 20,
|
|
|
alignment: go.Spot.TopRight,
|
|
|
alignmentFocus: go.Spot.Center,
|
|
|
// customize the expander behavior to
|
|
|
// create children if the node has never been expanded
|
|
|
click: (e, obj) => {
|
|
|
// OBJ is the Button
|
|
|
const node = obj.part; // get the Node containing this Button
|
|
|
if (node === null) return;
|
|
|
e.handled = true;
|
|
|
expandNode(node);
|
|
|
},
|
|
|
},
|
|
|
new go.Binding('visible', 'editable')
|
|
|
),
|
|
|
// 收起按钮
|
|
|
$(
|
|
|
'TreeExpanderButton',
|
|
|
{
|
|
|
name: 'BUTTONX',
|
|
|
alignment: go.Spot.Bottom,
|
|
|
opacity: 0, // initially not visible
|
|
|
_treeExpandedFigure: 'TriangleUp',
|
|
|
_treeCollapsedFigure: 'TriangleDown',
|
|
|
click: (e, obj) => {
|
|
|
const node = obj.part; // get the Node containing this Button
|
|
|
if (node === null) return;
|
|
|
e.handled = true;
|
|
|
toExpandNode(node);
|
|
|
},
|
|
|
},
|
|
|
// button is visible either when node is selected or on mouse-over
|
|
|
new go.Binding('visible', 'editable'),
|
|
|
new go.Binding('opacity', 'isSelected', (s) =>
|
|
|
s ? 1 : 0
|
|
|
).ofObject()
|
|
|
)
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
myModel.addChangedListener((e) => {
|
|
|
if (e.isTransactionFinished) {
|
|
|
const tx = e.object;
|
|
|
|
|
|
tx?.changes.each(changeObj => {
|
|
|
const changeName = changeObj.propertyName
|
|
|
const changeTarget = changeObj.object
|
|
|
if (changeName === 'title' || changeName === 'url') {
|
|
|
const nodeInfo = {
|
|
|
knowledge_id: changeTarget.key,
|
|
|
title: changeTarget.title,
|
|
|
url: changeTarget.url
|
|
|
}
|
|
|
updateNodeRequest(dataFromWindowTop.virtualSpaceId, nodeInfo)
|
|
|
}
|
|
|
})
|
|
|
|
|
|
}
|
|
|
})
|
|
|
|
|
|
|
|
|
document
|
|
|
.getElementById('zoomToFit')
|
|
|
.addEventListener('click', () => myDiagram.zoomToFit());
|
|
|
}
|
|
|
|
|
|
|
|
|
function toExpandNode(node) {
|
|
|
const diagram = node.diagram;
|
|
|
if (node.isTreeExpanded) {
|
|
|
diagram.commandHandler.collapseTree(node);
|
|
|
} else {
|
|
|
diagram.commandHandler.expandTree(node);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
//editable是在util.js中声明的全局变量
|
|
|
function changeMode() {
|
|
|
editable = !editable;
|
|
|
myDiagram.allowDelete = editable
|
|
|
myDiagram.commandHandler.copiesTree = editable
|
|
|
myDiagram.commandHandler.deletesTree = editable
|
|
|
// myDiagram.draggingTool.dragsTree = editable
|
|
|
myDiagram.undoManager.isEnabled = editable
|
|
|
myDiagram.model.commit(function (m) {
|
|
|
for (const node of m.nodeDataArray) {
|
|
|
m.set(node, 'editable', !node.editable);
|
|
|
}
|
|
|
}, '');
|
|
|
|
|
|
//每次切换模式时,清空undo栈,否则本身这个changeMode的改动也能被撤销
|
|
|
myDiagram.undoManager.clear()
|
|
|
|
|
|
|
|
|
editButton.innerHTML = editable ? '保存图谱' : '编辑图谱';
|
|
|
tipsDom.style.display = editable ? 'block' : 'none'
|
|
|
}
|
|
|
|
|
|
editButton.addEventListener('click', () => changeMode());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function createSubTree(parentData) {
|
|
|
|
|
|
const numchildren = 1;
|
|
|
|
|
|
const model = myDiagram.model;
|
|
|
const parent = myDiagram.findNodeForData(parentData);
|
|
|
|
|
|
let degrees = 1;
|
|
|
let grandparent = parent.findTreeParentNode();
|
|
|
while (grandparent) {
|
|
|
degrees++;
|
|
|
grandparent = grandparent.findTreeParentNode();
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const res = await addNodeRequest(dataFromWindowTop.virtualSpaceId, {
|
|
|
title: '请输入标题',
|
|
|
url: '请输入url', parent: parentData.key
|
|
|
})
|
|
|
if (!res.status) {
|
|
|
const childData = {
|
|
|
title: '请输入标题',
|
|
|
url: '请输入url',
|
|
|
editable: parentData.editable,
|
|
|
key: res?.knowledge_id,
|
|
|
parent: parentData.key,
|
|
|
rootdistance: degrees,
|
|
|
color: colors[0],
|
|
|
};
|
|
|
// add to model.nodeDataArray and create a Node
|
|
|
model.addNodeData(childData);
|
|
|
// position the new child node close to the parent
|
|
|
const child = myDiagram.findNodeForData(childData);
|
|
|
child.location = parent.location;
|
|
|
}
|
|
|
return numchildren;
|
|
|
}
|
|
|
|
|
|
|
|
|
//新增节点操作
|
|
|
async function expandNode(node) {
|
|
|
const diagram = node.diagram;
|
|
|
diagram.startTransaction('CollapseExpandTree');
|
|
|
// this behavior is specific to this incrementalTree sample:
|
|
|
const data = node.data;
|
|
|
|
|
|
// if (!data.everExpanded) {
|
|
|
// 创建节点
|
|
|
diagram.model.setDataProperty(data, 'everExpanded', true);
|
|
|
const numchildren = await createSubTree(data);
|
|
|
if (numchildren === 0) {
|
|
|
node.findObject('TREEBUTTON').visible = false;
|
|
|
}
|
|
|
// }
|
|
|
// 展开收起
|
|
|
if (node.isTreeExpanded) {
|
|
|
// diagram.commandHandler.collapseTree(node);
|
|
|
} else {
|
|
|
diagram.commandHandler.expandTree(node);
|
|
|
}
|
|
|
diagram.commitTransaction('CollapseExpandTree');
|
|
|
// myDiagram.zoomToFit();
|
|
|
}
|
|
|
|
|
|
|
|
|
</script>
|
|
|
</body>
|
|
|
|
|
|
</html> |