338 lines
7.9 KiB
Vue
338 lines
7.9 KiB
Vue
<script setup lang="ts">
|
||
|
||
import { ref, onMounted } from 'vue'
|
||
import HeaderBlock from './HeaderBlock.vue';
|
||
import { Network, type Data } from 'vis-network'
|
||
import { getGraph, updateNode } from './client';
|
||
import type { Graph, GraphEdge, GraphNode } from './models';
|
||
|
||
const network = ref<HTMLElement>()
|
||
|
||
const graph = ref<Graph>({
|
||
nodes: [],
|
||
edges: []
|
||
})
|
||
|
||
const selectedNode = ref<GraphNode>({
|
||
id: 0,
|
||
label: "",
|
||
name: "",
|
||
text: "",
|
||
applications: [],
|
||
links: [],
|
||
})
|
||
|
||
let net = <Network>{}
|
||
let data = <Data>{}
|
||
|
||
const displayEdges = ref(0)
|
||
const allEdges = ref(0)
|
||
const onApplicationEdges = ref(true)
|
||
|
||
async function loadGraph() {
|
||
graph.value = await getGraph()
|
||
allEdges.value = graph.value.edges.length
|
||
if (onApplicationEdges.value) {
|
||
graph.value.edges = graph.value.edges.filter(function (edge: GraphEdge) { return edge.type !== 'application' })
|
||
}
|
||
graph.value.edges.map(function (edge: GraphEdge) {
|
||
if (edge.type == 'application') {
|
||
edge.color = '#cccccc'
|
||
}
|
||
})
|
||
displayEdges.value = graph.value.edges.length
|
||
data = {
|
||
nodes: graph.value.nodes,
|
||
edges: graph.value.edges.sort(function (a: GraphEdge, b: GraphEdge) {
|
||
if (a.type == 'application') {
|
||
return 1
|
||
}
|
||
if (b.type == 'application') {
|
||
return -1
|
||
}
|
||
return 0
|
||
})
|
||
}
|
||
net.setData(data)
|
||
console.log(graph.value.edges)
|
||
}
|
||
|
||
onMounted(async () => {
|
||
if (!network.value) return
|
||
|
||
const options = {
|
||
interaction: {
|
||
selectable: true,
|
||
},
|
||
nodes: {
|
||
color: {
|
||
border: '#2B7CE9', // Normal border color
|
||
background: '#97C2FC', // Normal background color
|
||
highlight: { // Selection state
|
||
border: '#960000',
|
||
background: '#ff9494'
|
||
},
|
||
hover: { // Hover state
|
||
border: '#2B7CE9',
|
||
background: '#D2E5FF'
|
||
}
|
||
}
|
||
},
|
||
}
|
||
net = new Network(network.value, data, options)
|
||
net.on("click", function (params) {
|
||
if (params.nodes.length > 0) {
|
||
selectNode(graph.value.nodes[params.nodes[0]])
|
||
} else if (params.edges.length > 0) {
|
||
console.log("Clicked edge:", params.edges[0]);
|
||
}
|
||
});
|
||
|
||
await loadGraph()
|
||
selectNode(graph.value.nodes[0])
|
||
})
|
||
|
||
function selectNode(node: GraphNode) {
|
||
console.log("Select node:", node.id)
|
||
selectedNode.value = node
|
||
const links = graph.value.edges.filter(function (it: GraphEdge) {
|
||
return it.from == node.id
|
||
}).map(function (it: GraphEdge): GraphNode {
|
||
const id = it.to
|
||
const linkNode = graph.value.nodes.filter(function (it: GraphNode) { return it.id == id })
|
||
return linkNode[0]
|
||
})
|
||
|
||
selectedNode.value.links = links
|
||
net.selectNodes([selectedNode.value.id])
|
||
}
|
||
|
||
async function updateSelectedNode() {
|
||
console.log("Update node:", selectedNode.value)
|
||
await updateNode(selectedNode.value)
|
||
await loadGraph()
|
||
}
|
||
|
||
function nodeHeader(node: GraphNode): string {
|
||
return "[" + node.label + "] - " + node.name
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<HeaderBlock>
|
||
<div>
|
||
Редактор сценариев
|
||
</div>
|
||
</HeaderBlock>
|
||
|
||
<div ref="network" class="graph-container"></div>
|
||
|
||
<div class="nodes-container">
|
||
<h2>Точки</h2>
|
||
<div>Всего точек: {{ graph.nodes.length }}</div>
|
||
<div>
|
||
Всего связей: {{ allEdges }}, показано: {{ displayEdges }}
|
||
<div>
|
||
Показать все связи:
|
||
<label class="checkbox-green">
|
||
<input type="checkbox" v-on:click="onApplicationEdges = !onApplicationEdges, loadGraph()">
|
||
<span class="checkbox-green-switch" data-label-on="Вкл" data-label-off="Выкл"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<hr class="hr">
|
||
|
||
<div v-bind:key="node.id" v-for="node in graph.nodes">
|
||
<div :class="[node.id == selectedNode.id ? 'selected-node' : '']" class="node-select-button"
|
||
v-on:click="selectNode(node)">
|
||
{{ nodeHeader(node) }}
|
||
<span v-if="node.applications.length > 0">({{ node.applications.length }})</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="edit-node-container">
|
||
<h2>Редактирование точки</h2>
|
||
<div>
|
||
{{ nodeHeader(selectedNode) }}
|
||
</div>
|
||
<div>
|
||
<textarea class="node-text-edit-field" rows="30" v-model="selectedNode.text"></textarea>
|
||
</div>
|
||
<div>
|
||
<h3>Приложения: {{ selectedNode.applications.length }}</h3>
|
||
<div v-bind:key="index" v-for="(application, index) in selectedNode.applications">
|
||
<textarea class="node-text-edit-field" rows="5" v-model="application.name"></textarea>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<h3>Ссылки: {{ selectedNode.links.length }}</h3>
|
||
<div v-bind:key="node.id" v-for="node in selectedNode.links">
|
||
<div class="node-select-button" v-on:click="selectNode(node)">
|
||
- {{ nodeHeader(node) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<button class="node-edit-save-button" v-on:click="updateSelectedNode()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.graph-container {
|
||
width: 100%;
|
||
height: calc(100vh - 50px);
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.nodes-container {
|
||
position: fixed;
|
||
left: 5px;
|
||
top: 55px;
|
||
height: calc(100vh - 100px);
|
||
padding: 10px 20px;
|
||
}
|
||
|
||
.edit-node-container {
|
||
position: fixed;
|
||
right: 5px;
|
||
top: 55px;
|
||
height: calc(100vh - 100px);
|
||
padding: 10px 20px;
|
||
min-width: 350px;
|
||
max-width: 400px;
|
||
}
|
||
|
||
.node-select-button {
|
||
color: #373737;
|
||
}
|
||
|
||
.node-select-button:hover {
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.selected-node {
|
||
font-weight: bold;
|
||
color: #960000;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.node-text-edit-field {
|
||
padding: 7px;
|
||
margin: 5px 0;
|
||
width: 100%;
|
||
}
|
||
|
||
.node-edit-save-button {
|
||
padding: 3px 7px;
|
||
margin: 5px;
|
||
background-color: #ffffff;
|
||
border-radius: 7px;
|
||
border: 1px solid #373737;
|
||
}
|
||
|
||
.node-edit-save-button:hover {
|
||
background-color: #dddddd;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.hr {
|
||
margin: 10px 0;
|
||
}
|
||
|
||
|
||
|
||
.checkbox-green {
|
||
display: inline-block;
|
||
height: 20px;
|
||
line-height: 28px;
|
||
margin-right: 10px;
|
||
position: relative;
|
||
vertical-align: middle;
|
||
font-size: 14px;
|
||
user-select: none;
|
||
}
|
||
.checkbox-green .checkbox-green-switch {
|
||
display: inline-block;
|
||
height: 20px;
|
||
width: 90px;
|
||
box-sizing: border-box;
|
||
position: relative;
|
||
border-radius: 2px;
|
||
background: #848484;
|
||
transition: background-color 0.3s cubic-bezier(0, 1, 0.5, 1);
|
||
}
|
||
.checkbox-green .checkbox-green-switch:before {
|
||
content: attr(data-label-on);
|
||
display: inline-block;
|
||
box-sizing: border-box;
|
||
width: 45px;
|
||
padding: 0 8px;
|
||
position: absolute;
|
||
top: 0;
|
||
left: 45px;
|
||
text-transform: uppercase;
|
||
text-align: center;
|
||
color: rgba(255, 255, 255, 0.5);
|
||
font-size: 10px;
|
||
line-height: 20px;
|
||
}
|
||
.checkbox-green .checkbox-green-switch:after {
|
||
content: attr(data-label-off);
|
||
display: inline-block;
|
||
box-sizing: border-box;
|
||
width: 44px;
|
||
border-radius: 1px;
|
||
position: absolute;
|
||
top: 1px;
|
||
left: 1px;
|
||
z-index: 5;
|
||
text-transform: uppercase;
|
||
text-align: center;
|
||
background: white;
|
||
line-height: 18px;
|
||
font-size: 10px;
|
||
color: #777;
|
||
transition: transform 0.3s cubic-bezier(0, 1, 0.5, 1);
|
||
}
|
||
.checkbox-green input[type="checkbox"] {
|
||
display: block;
|
||
width: 0;
|
||
height: 0;
|
||
position: absolute;
|
||
z-index: -1;
|
||
opacity: 0;
|
||
}
|
||
.checkbox-green input[type="checkbox"]:checked + .checkbox-green-switch {
|
||
background-color: #848484;
|
||
}
|
||
.checkbox-green input[type="checkbox"]:checked + .checkbox-green-switch:before {
|
||
content: attr(data-label-off);
|
||
left: 0;
|
||
}
|
||
.checkbox-green input[type="checkbox"]:checked + .checkbox-green-switch:after {
|
||
content: attr(data-label-on);
|
||
color: #848484;
|
||
transform: translate3d(44px, 0, 0);
|
||
}
|
||
|
||
/* Hover */
|
||
.checkbox-green input[type="checkbox"]:not(:disabled) + .checkbox-green-switch:hover {
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* Disabled */
|
||
.checkbox-green input[type=checkbox]:disabled + .checkbox-green-switch {
|
||
opacity: 0.6;
|
||
filter: grayscale(50%);
|
||
}
|
||
|
||
/* Focus */
|
||
.checkbox-green.focused .checkbox-green-switch:after {
|
||
box-shadow: inset 0px 0px 4px #ff5623;
|
||
}
|
||
</style>
|