generated from VLADIMIR/template_frontend
644 lines
16 KiB
Vue
644 lines
16 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, GraphApplication, GraphDoor, GraphEdge, GraphNode } from './models';
|
|
|
|
const network = ref<HTMLElement>()
|
|
|
|
const graph = ref<Graph>({
|
|
nodes: [],
|
|
edges: []
|
|
})
|
|
|
|
const emptyNode: GraphNode = {
|
|
code: "",
|
|
name: "",
|
|
text: "",
|
|
image: "",
|
|
applications: [],
|
|
hidden: false,
|
|
doors: [],
|
|
|
|
id: "",
|
|
label: "",
|
|
links: [],
|
|
}
|
|
|
|
const updatedNodeID = ref("")
|
|
const selectedNode = ref<GraphNode>({
|
|
code: "",
|
|
name: "",
|
|
text: "",
|
|
image: "",
|
|
applications: [],
|
|
hidden: false,
|
|
doors: [],
|
|
|
|
id: "",
|
|
label: "",
|
|
links: [],
|
|
})
|
|
|
|
const focusedNode = ref<GraphNode>({
|
|
code: "",
|
|
name: "",
|
|
text: "",
|
|
image: "",
|
|
applications: [],
|
|
hidden: false,
|
|
doors: [],
|
|
|
|
id: "",
|
|
label: "",
|
|
links: [],
|
|
})
|
|
|
|
let net = <Network>{}
|
|
let data = <Data>{}
|
|
|
|
const displayEdges = ref(0)
|
|
const allEdges = ref(0)
|
|
const isShowGraph = ref(false)
|
|
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 = '#aaaaaa'
|
|
}
|
|
})
|
|
graph.value.nodes = graph.value.nodes.map(function (it: GraphNode) {
|
|
it.id = it.code
|
|
it.label = it.name
|
|
return it
|
|
})
|
|
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) {
|
|
console.log("click graph:", params)
|
|
if (params.nodes.length > 0) {
|
|
const clickNode = graph.value.nodes.find(function (it: GraphNode): boolean { return it.code == params.nodes[0] })
|
|
if (clickNode !== undefined) {
|
|
selectNode(clickNode)
|
|
}
|
|
} else if (params.edges.length > 0) {
|
|
console.log("Clicked edge:", params.edges[0]);
|
|
}
|
|
});
|
|
|
|
await loadGraph()
|
|
selectNode(graph.value.nodes[0])
|
|
})
|
|
|
|
function addApplicationToSelectedNode() {
|
|
selectedNode.value.applications.push({ name: "" })
|
|
}
|
|
|
|
function removeApplicationToSelectedNode(name: string) {
|
|
selectedNode.value.applications = selectedNode.value.applications.filter(function (it: GraphApplication): boolean { return it.name != name })
|
|
}
|
|
|
|
function addDoorToSelectedNode() {
|
|
selectedNode.value.doors.push({ code: "", name: "", show: false })
|
|
}
|
|
|
|
function removeDoorToSelectedNode(code: string) {
|
|
selectedNode.value.doors = selectedNode.value.doors.filter(function (it: GraphDoor): boolean { return it.code != code })
|
|
}
|
|
|
|
function selectNode(node: GraphNode) {
|
|
console.log("Select node:", node)
|
|
updatedNodeID.value = node.code
|
|
selectedNode.value = node
|
|
const links = graph.value.edges.filter(function (it: GraphEdge) {
|
|
return it.from == node.code
|
|
}).map(function (it: GraphEdge): GraphNode {
|
|
const id = it.to
|
|
const linkNode = graph.value.nodes.filter(function (it: GraphNode) { return it.code == id })
|
|
return linkNode[0]
|
|
})
|
|
|
|
selectedNode.value.links = links
|
|
net.selectNodes([selectedNode.value.code])
|
|
if (!isShowGraph.value) {
|
|
window.document.getElementById(node.code)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
}
|
|
}
|
|
|
|
function focusNode(node: GraphNode) {
|
|
console.log("Focus node:", node.code)
|
|
focusedNode.value = node
|
|
}
|
|
|
|
function copyLink(node: GraphNode) {
|
|
console.log("Focus node:", node.code)
|
|
navigator.clipboard.writeText("([" + node.code + "])")
|
|
focusedNode.value = emptyNode
|
|
}
|
|
|
|
async function updateSelectedNode() {
|
|
console.log("Update node:", selectedNode.value)
|
|
await updateNode(updatedNodeID.value, selectedNode.value)
|
|
await loadGraph()
|
|
|
|
const nodes = graph.value.nodes.filter(function (it: GraphNode) {
|
|
return it.code == selectedNode.value.code
|
|
})
|
|
selectNode(nodes[0])
|
|
}
|
|
|
|
async function deleteSelectedNode() {
|
|
console.log("Delete node:", selectedNode.value)
|
|
selectedNode.value.code = ""
|
|
await updateNode(updatedNodeID.value, selectedNode.value)
|
|
await loadGraph()
|
|
}
|
|
|
|
async function addSelectedNode() {
|
|
console.log("Add node:", selectedNode.value)
|
|
selectedNode.value.code = updatedNodeID.value
|
|
await updateNode("", selectedNode.value)
|
|
await loadGraph()
|
|
|
|
const nodes = graph.value.nodes.filter(function (it: GraphNode) {
|
|
return it.code == selectedNode.value.code
|
|
})
|
|
selectNode(nodes[0])
|
|
}
|
|
|
|
function nodeHeader(node: GraphNode): string {
|
|
return "[" + node.code + "] - " + node.name
|
|
}
|
|
|
|
function showGraph(show: boolean) {
|
|
isShowGraph.value = show
|
|
if (isShowGraph.value) {
|
|
loadGraph()
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<HeaderBlock>
|
|
<div>
|
|
Редактор сценариев
|
|
</div>
|
|
</HeaderBlock>
|
|
|
|
<div :class="[isShowGraph ? 'text-container-disable' : '']">
|
|
<div class="data-container text-container">
|
|
<!-- Действия -->
|
|
<div class="messages-block" ref="scrollContainer">
|
|
<div class="center-block-custom">
|
|
<div v-for="action in graph.nodes" :key="action.id" v-on:click="selectNode(action)" :id="action.code">
|
|
<div class="message-cloud" :class="[action.code == selectedNode.code ? 'selected-message-cloud' : '']">
|
|
<div class="message-header" :class="[action.code == selectedNode.code ? 'selected-message-header' : '']">
|
|
{{ action.code }}: {{ action.name }}
|
|
</div>
|
|
<div v-if="action.image !== ''">
|
|
{{ action.image }}
|
|
</div>
|
|
<hr class="hr" />
|
|
<div class="message-content">
|
|
{{ action.text }}
|
|
</div>
|
|
<hr class="hr" v-if="action.applications.length" />
|
|
<div class="message-footer" v-for="door in action.doors" :key="door.code">
|
|
<span v-if="door.show">Кнопка: </span>
|
|
<span v-if="!door.show">Дверь: </span>
|
|
[{{ door.code }}] - "{{ door.name }}"
|
|
</div>
|
|
<div class="message-footer" v-for="application in action.applications" :key="application.name">
|
|
Приложение: {{ application.name }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div :class="[!isShowGraph ? 'graph-container-disable' : '']">
|
|
<div ref="network" class="data-container graph-container"></div>
|
|
</div>
|
|
|
|
<div class="nodes-container">
|
|
<h2>Точки</h2>
|
|
<div>Всего точек: {{ graph.nodes.length }}</div>
|
|
<div>
|
|
Отображать граф:
|
|
<label class="checkbox-green">
|
|
<input type="checkbox" v-on:click="showGraph(!isShowGraph)">
|
|
<span class="checkbox-green-switch" data-label-on="Да" data-label-off="Нет"></span>
|
|
</label>
|
|
</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 class="scroll-y">
|
|
<div v-bind:key="node.code" v-for="node in graph.nodes">
|
|
<span v-on:mouseenter="focusNode(node)" v-on:mouseleave="focusNode(emptyNode)">
|
|
<span :class="[node.code == selectedNode.code ? 'selected-node' : '']" class="node-select-button"
|
|
v-on:click="selectNode(node)">{{ nodeHeader(node) }}</span>
|
|
<span v-if="node.applications.length > 0"> ({{ node.applications.length }})</span>
|
|
<span v-if="node.code == focusedNode.code" class="copy-node-link" v-on:click="copyLink(node)">
|
|
Ссылка
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="edit-node-container">
|
|
<div class="scroll-y-right">
|
|
<h2>Редактирование точки</h2>
|
|
<div>
|
|
<input v-model="updatedNodeID" type="text" class="node-code-edit-field" maxlength="5" /> - <input
|
|
v-model="selectedNode.name" type="text" class="node-name-edit-field" />
|
|
</div>
|
|
<div>
|
|
<textarea class="node-text-edit-field" rows="15" v-model="selectedNode.text"></textarea>
|
|
</div>
|
|
<div>
|
|
<h3>Двери: {{ selectedNode.doors.length }}
|
|
<button class="editor-button application-add-button" v-on:click="addDoorToSelectedNode()">+</button>
|
|
</h3>
|
|
<div class="tb-5" v-if="selectedNode.doors.length > 0">
|
|
<div v-bind:key="index" v-for="(door, index) in selectedNode.doors">
|
|
<button class="editor-button application-remove-button"
|
|
v-on:click="removeDoorToSelectedNode(door.code)">-</button>
|
|
<input class="node-code-edit-field" v-model="door.code" type="text" maxlength="5" /> -
|
|
<input class="node-name-edit-field" v-model="door.name" type="text" /> -
|
|
<input class="node-code-edit-field" v-model="door.show" type="text" maxlength="5" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3>Приложения: {{ selectedNode.applications.length }}
|
|
<button class="editor-button application-add-button" v-on:click="addApplicationToSelectedNode()">+</button>
|
|
</h3>
|
|
<div v-bind:key="index" v-for="(application, index) in selectedNode.applications">
|
|
<button class="editor-button application-remove-button"
|
|
v-on:click="removeApplicationToSelectedNode(application.name)">-</button>
|
|
<textarea class="node-text-edit-field" rows="2" v-model="application.name"></textarea>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3>Ссылки: {{ selectedNode.links.length }}</h3>
|
|
<div v-bind:key="node.code" v-for="node in selectedNode.links">
|
|
<div class="node-select-button" v-on:click="selectNode(node)">
|
|
- {{ nodeHeader(node) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<hr class="hr">
|
|
<div>
|
|
<button class="editor-button" v-on:click="updateSelectedNode()">Сохранить</button>
|
|
<button class="editor-button" v-on:click="addSelectedNode()">Добавить</button>
|
|
<button class="editor-button" v-on:click="deleteSelectedNode()">Удалить</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.data-container {
|
|
width: 100%;
|
|
height: calc(100vh - 50px);
|
|
border: 1px solid #e0e0e0;
|
|
}
|
|
|
|
.graph-container {
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.graph-container-disable {
|
|
position: absolute;
|
|
right: 10000px;
|
|
}
|
|
|
|
.text-container {
|
|
padding: 10px;
|
|
/* background-color: red; */
|
|
}
|
|
|
|
.text-container-disable {
|
|
position: absolute;
|
|
left: 10000px;
|
|
}
|
|
|
|
.nodes-container {
|
|
position: fixed;
|
|
left: 5px;
|
|
top: 55px;
|
|
height: calc(100vh - 100px);
|
|
padding: 3px 10px;
|
|
}
|
|
|
|
.edit-node-container {
|
|
position: fixed;
|
|
right: 5px;
|
|
top: 55px;
|
|
height: calc(100vh - 100px);
|
|
padding: 3px 10px;
|
|
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%;
|
|
}
|
|
|
|
.editor-button {
|
|
padding: 3px 7px;
|
|
margin: 5px 5px 5px 0;
|
|
background-color: #ffffff;
|
|
border: 1px solid #777777;
|
|
}
|
|
|
|
.editor-button:hover {
|
|
background-color: #eeeeee;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.hr {
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.copy-node-link {
|
|
margin-left: 5px;
|
|
}
|
|
|
|
.copy-node-link:hover {
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.scroll-y {
|
|
overflow-y: auto;
|
|
max-height: calc(100vh - 250px);
|
|
}
|
|
|
|
.scroll-y-right {
|
|
overflow-y: auto;
|
|
max-height: calc(100vh - 70px);
|
|
}
|
|
|
|
.scroll-y::-webkit-scrollbar,
|
|
.scroll-y-right::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.node-code-edit-field {
|
|
width: 50px;
|
|
}
|
|
|
|
.node-name-edit-field {
|
|
width: 200px;
|
|
}
|
|
|
|
.application-add-button {
|
|
display: inline;
|
|
margin-left: 5px;
|
|
width: 23px;
|
|
height: 23px;
|
|
}
|
|
|
|
.application-remove-button {
|
|
position: absolute;
|
|
left: -15px;
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
|
|
|
|
.messages-block {
|
|
height: 100%;
|
|
overflow-y: auto;
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
@media (min-width: 1025px) {
|
|
.center-block-custom {
|
|
width: calc(100vw - 750px);
|
|
margin: 0 auto;
|
|
}
|
|
}
|
|
|
|
.message-cloud {
|
|
border: 1px solid #444444;
|
|
border-radius: 15px;
|
|
margin: 12px 10px;
|
|
padding: 16px;
|
|
background-color: var(--main-back-item-color);
|
|
}
|
|
|
|
.message-cloud:hover {
|
|
background-color: #eeeeee;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.selected-message-cloud {
|
|
border: 2px solid #960000;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.message-header {
|
|
font-size: large;
|
|
font-weight: 200;
|
|
}
|
|
|
|
.selected-message-header {
|
|
font-size: large;
|
|
font-weight: 500;
|
|
color: #960000;
|
|
}
|
|
|
|
.message-content {
|
|
font-weight: 500;
|
|
/* white-space: pre-line; */
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.message-footer {
|
|
font-weight: 400;
|
|
color: var(--second-color);
|
|
}
|
|
|
|
|
|
.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: #444444;
|
|
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: #777777;
|
|
}
|
|
|
|
.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: #777777;
|
|
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;
|
|
}
|
|
|
|
.tb-5 {
|
|
margin: 5px 0;
|
|
}
|
|
</style>
|