You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
NewEduCoderBuild/knowledgegraph/graph.html

639 lines
18 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!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>