mirror of
https://github.com/robonen/eulerian-cycle.git
synced 2026-03-20 02:44:47 +00:00
Added pop-ups, guide, fixed selection when dragging and duplicating links
This commit is contained in:
@@ -4,4 +4,6 @@
|
||||
|
||||
<style>
|
||||
@import url("~@/assets/css/formular.css");
|
||||
@import "~@/assets/css/creation.css";
|
||||
@import "~@/assets/css/graph.css";
|
||||
</style>
|
||||
|
||||
@@ -308,7 +308,7 @@ header {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0.6;
|
||||
transition: .2s;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.menu-icon:last-child {
|
||||
@@ -394,7 +394,8 @@ p {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.menu-prompt, .prompt {
|
||||
.menu-prompt,
|
||||
.prompt {
|
||||
left: 100%;
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
@@ -410,7 +411,7 @@ p {
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
box-shadow: 0px 1px 5px 1px 0px 1px 5px 1px rgb(56 58 63 / 35%);
|
||||
transition: .2s;
|
||||
transition: 0.2s;
|
||||
display: block;
|
||||
width: -webkit-max-content;
|
||||
width: -moz-max-content;
|
||||
@@ -457,10 +458,11 @@ p {
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.menu-icon:hover .menu-prompt, .control-button:hover .prompt {
|
||||
.menu-icon:hover .menu-prompt,
|
||||
.control-button:hover .prompt {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition-delay: .5s;
|
||||
transition-delay: 0.5s;
|
||||
}
|
||||
|
||||
.error {
|
||||
@@ -474,6 +476,7 @@ p {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
box-shadow: 0px 1px 5px 1px rgb(56 58 63 / 15%);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.popup-el.popup-button-cont {
|
||||
@@ -505,3 +508,22 @@ p {
|
||||
color: gray;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.fade-enter-active {
|
||||
animation: fade-in 0.5s;
|
||||
}
|
||||
|
||||
.fade-leave-active {
|
||||
animation: fade-in 0.5s reverse;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
transform: translateY(-40px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
ref="renderer"
|
||||
@dblclick="createNode"
|
||||
@click.left.stop="unselectAllNodes"
|
||||
@contextmenu.prevent
|
||||
>
|
||||
<g>
|
||||
<component
|
||||
@@ -66,6 +67,9 @@ export default {
|
||||
// Const
|
||||
const RADIUS = 25;
|
||||
|
||||
// Vars
|
||||
let draggableNode = false;
|
||||
|
||||
// Reactive
|
||||
const renderer = ref(null);
|
||||
|
||||
@@ -104,6 +108,7 @@ export default {
|
||||
// }
|
||||
// );
|
||||
|
||||
// Methods
|
||||
const loopPosition = (coords) => {
|
||||
const node = nodes.value[coords];
|
||||
|
||||
@@ -117,7 +122,6 @@ export default {
|
||||
},${y}`;
|
||||
};
|
||||
|
||||
// Methods
|
||||
const hasntIntersections = (node) => {
|
||||
return nodes.value.every((current) => {
|
||||
return (
|
||||
@@ -127,13 +131,13 @@ export default {
|
||||
});
|
||||
};
|
||||
|
||||
// Nodes
|
||||
const activateNodes = (ids) =>
|
||||
ids.forEach((e) => (nodes.value[e].selected = true));
|
||||
|
||||
const deactivateNodes = (ids) =>
|
||||
ids.forEach((e) => (nodes.value[e].selected = false));
|
||||
|
||||
// Nodes
|
||||
const createNode = ({ offsetX, offsetY }) => {
|
||||
if (nodes.value.length >= 99) return;
|
||||
|
||||
@@ -167,6 +171,11 @@ export default {
|
||||
};
|
||||
|
||||
const selectNode = (id) => {
|
||||
if (draggableNode) {
|
||||
draggableNode = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (linker.sourceEmpty()) {
|
||||
linker.setSource(id);
|
||||
activateNodes([id]);
|
||||
@@ -207,19 +216,36 @@ export default {
|
||||
const nodeMove = ({ offsetX, offsetY }) => {
|
||||
const d = drag.value;
|
||||
|
||||
draggableNode = true;
|
||||
|
||||
nodes.value[d.id].x = offsetX - d.offsetX;
|
||||
nodes.value[d.id].y = offsetY - d.offsetY;
|
||||
};
|
||||
|
||||
// Links
|
||||
linker.onLink((source, target) => {
|
||||
let duplicateLink = null;
|
||||
|
||||
links.value.forEach((e, idx) => {
|
||||
if (
|
||||
(e.source === source && e.target === target) ||
|
||||
(e.source === target && e.target === source)
|
||||
)
|
||||
duplicateLink = idx;
|
||||
});
|
||||
|
||||
if (duplicateLink !== null) {
|
||||
links.value.splice(duplicateLink, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
links.value.push({
|
||||
selected: false,
|
||||
source,
|
||||
target,
|
||||
});
|
||||
|
||||
euler.loadLinks([...links.value]);
|
||||
euler.loadLinks(Object.values(links.value));
|
||||
|
||||
if (euler.check()) emit("isEuler", euler.find());
|
||||
else emit("isEuler", []);
|
||||
@@ -254,7 +280,8 @@ svg {
|
||||
}
|
||||
|
||||
circle,
|
||||
line {
|
||||
line,
|
||||
path {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
67
src/components/Guide.vue
Normal file
67
src/components/Guide.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<popup
|
||||
leftBtnText="Назад"
|
||||
:rightBtnText="currentStep + 1 === steps.length ? 'Завершить' : 'Далее'"
|
||||
@left="stepDown"
|
||||
@right="stepUp"
|
||||
@close="close"
|
||||
>
|
||||
<template v-slot:title>
|
||||
{{ steps[currentStep].name }}
|
||||
<span class="version">{{ currentStep + 1 }} / {{ steps.length }}</span>
|
||||
</template>
|
||||
<template v-slot:content>
|
||||
{{ steps[currentStep].content }}
|
||||
</template>
|
||||
</popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from "vue";
|
||||
import Popup from "./Popup";
|
||||
|
||||
export default {
|
||||
name: "Guide",
|
||||
components: {
|
||||
Popup,
|
||||
},
|
||||
props: {
|
||||
steps: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
// Reactive
|
||||
const currentStep = ref(0);
|
||||
|
||||
// Methods
|
||||
const stepDown = () => {
|
||||
if (currentStep.value <= 0) return;
|
||||
|
||||
currentStep.value -= 1;
|
||||
};
|
||||
|
||||
const stepUp = () => {
|
||||
if (currentStep.value + 1 >= props.steps.length) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
currentStep.value += 1;
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
currentStep.value = 0;
|
||||
emit("close");
|
||||
};
|
||||
|
||||
return {
|
||||
currentStep,
|
||||
stepDown,
|
||||
stepUp,
|
||||
close,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,24 +1,37 @@
|
||||
<template>
|
||||
<div class="adjacency_matrix-cont">
|
||||
<div class="adjacency_matrix">
|
||||
<div class="adjacency_matrix-menu">
|
||||
<div class="header header-matrix">Матрица смежности</div>
|
||||
<div class="menu-icon close-icon"></div>
|
||||
</div>
|
||||
<div class="matrix-cont">
|
||||
<div class="value-rows-matrix-cont">
|
||||
<div class="value-row-matrix" v-for="ni in size" :key="ni">
|
||||
{{ ni }}
|
||||
</div>
|
||||
<div class="value-row-matrix">0</div>
|
||||
<div class="value-row-matrix">1</div>
|
||||
<div class="value-row-matrix">2</div>
|
||||
</div>
|
||||
<div class="body-matrix">
|
||||
<div class="matrix">
|
||||
<div class="matrix-column" v-for="nj in size" :key="nj">
|
||||
<div class="value-matrix-column">{{ nj }}</div>
|
||||
<div
|
||||
class="matrix-cell"
|
||||
v-for="ni in size"
|
||||
:key="ni"
|
||||
:class="{
|
||||
'active-cell': isActive(ni - 1, nj - 1),
|
||||
'auto-matrix-cell': isAbove(ni - 1, nj - 1),
|
||||
}"
|
||||
@click="change(ni - 1, nj - 1)"
|
||||
></div>
|
||||
<div class="matrix-column">
|
||||
<div class="value-matrix-column">0</div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
</div>
|
||||
<div class="matrix-column">
|
||||
<div class="value-matrix-column">1</div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
</div>
|
||||
<div class="matrix-column">
|
||||
<div class="value-matrix-column">2</div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -26,67 +39,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// active-cell - one with background
|
||||
// matrix-cell - default
|
||||
// auto-matrix-cell - without background
|
||||
// active-auto-cell - one
|
||||
|
||||
const newArray = (size) =>
|
||||
Array(size)
|
||||
.fill(0)
|
||||
.map(() => Array(size).fill(0));
|
||||
|
||||
export default {
|
||||
name: "Matrix",
|
||||
props: {
|
||||
size: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
matrix: newArray(this.size),
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
size(val) {
|
||||
this.matrix = newArray(val);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isActive(i, j) {
|
||||
return this.matrix[i][j] === 1;
|
||||
},
|
||||
isAbove(i, j) {
|
||||
return i > j;
|
||||
},
|
||||
change(i, j) {
|
||||
const val = this.matrix[i][j] === 1 ? 0 : 1;
|
||||
|
||||
this.matrix[i][j] = val;
|
||||
|
||||
if (i != j) this.matrix[j][i] = val;
|
||||
|
||||
this.checkEuler();
|
||||
},
|
||||
checkEuler() {
|
||||
const isValid = this.matrix.reduce(
|
||||
(res, current) => {
|
||||
const relations = current.reduce((sum, i) => sum + i);
|
||||
|
||||
res.sum += relations;
|
||||
res.even &= !(relations % 2);
|
||||
|
||||
return res;
|
||||
},
|
||||
{ sum: 0, even: 1 }
|
||||
);
|
||||
|
||||
const result = isValid.sum && isValid.even;
|
||||
|
||||
this.$emit("valid", result);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
58
src/components/Popup.vue
Normal file
58
src/components/Popup.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="popup-cont">
|
||||
<div class="popup">
|
||||
<div class="popup-el popup-header">
|
||||
<div class="header">
|
||||
<slot name="title"></slot>
|
||||
</div>
|
||||
<div class="menu-icon close-icon" @click="closeBtn"></div>
|
||||
</div>
|
||||
<div class="popup-el popup-text">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
<!-- <div class="popup-el popup-text gray">by Robonen</div> -->
|
||||
<div class="popup-el popup-button-cont">
|
||||
<div v-if="leftBtnText" class="popup-button" @click="leftBtn">
|
||||
{{ leftBtnText }}
|
||||
</div>
|
||||
<div v-if="rightBtnText" class="main-popup-button" @click="rightBtn">
|
||||
{{ rightBtnText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "View",
|
||||
props: {
|
||||
leftBtnText: {
|
||||
type: String,
|
||||
},
|
||||
rightBtnText: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
setup(_, { emit }) {
|
||||
// Methods
|
||||
const closeBtn = () => {
|
||||
emit("close");
|
||||
};
|
||||
|
||||
const leftBtn = () => {
|
||||
emit("left");
|
||||
};
|
||||
|
||||
const rightBtn = () => {
|
||||
emit("right");
|
||||
};
|
||||
|
||||
return {
|
||||
closeBtn,
|
||||
leftBtn,
|
||||
rightBtn,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -6,15 +6,16 @@ const app = createApp(App).use(router);
|
||||
|
||||
app.directive("click-outside", {
|
||||
beforeMount(el, binding) {
|
||||
el.clickOutsideEvent = function (event) {
|
||||
if (!(el === event.target || el.contains(event.target))) {
|
||||
const ourClickEventHandler = (event) => {
|
||||
if (!el.contains(event.target) && el !== event.target) {
|
||||
binding.value(event, el);
|
||||
}
|
||||
};
|
||||
document.body.addEventListener("click", el.clickOutsideEvent);
|
||||
el.__vueClickEventHandler__ = ourClickEventHandler;
|
||||
document.addEventListener("click", ourClickEventHandler);
|
||||
},
|
||||
unmounted(el) {
|
||||
document.body.removeEventListener("click", el.clickOutsideEvent);
|
||||
document.removeEventListener("click", el.__vueClickEventHandler__);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<div class="input-cont">
|
||||
<div class="text-input-cont">размер матрицы:</div>
|
||||
<div class="number">
|
||||
<button class="number-minus" type="button" @click="sub">-</button>
|
||||
<input type="number" v-model="size" />
|
||||
<button class="number-plus" type="button" @click="add">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<matrix
|
||||
:size="size"
|
||||
@valid="isValid = $event"
|
||||
@save="matrix = $event"
|
||||
></matrix>
|
||||
<router-link
|
||||
to="/view"
|
||||
class="creation-button"
|
||||
:class="{ 'disabled-button': !isValid }"
|
||||
>
|
||||
СОЗДАТЬ ГРАФ
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Matrix from "../components/Matrix.vue";
|
||||
|
||||
export default {
|
||||
name: "Home",
|
||||
components: {
|
||||
Matrix,
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener("keydown", (evt) => {
|
||||
if (evt.keyCode === 38) this.add();
|
||||
if (evt.keyCode === 40) this.sub();
|
||||
});
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
size: 5,
|
||||
isValid: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
add() {
|
||||
if (this.size < 30) this.size++;
|
||||
},
|
||||
sub() {
|
||||
if (this.size > 1) this.size--;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
size(val) {
|
||||
this.size = Math.min(Math.max(1, val), 30);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "~@/assets/css/creation.css";
|
||||
</style>
|
||||
@@ -1,78 +1,38 @@
|
||||
<template>
|
||||
<!--<div class="adjacency_matrix-cont">
|
||||
<div class="adjacency_matrix">
|
||||
<div class="adjacency_matrix-menu">
|
||||
<div class="header header-matrix">Матрица смежности</div>
|
||||
<div class="menu-icon close-icon"></div>
|
||||
</div>
|
||||
<div class="matrix-cont">
|
||||
<div class="value-rows-matrix-cont">
|
||||
<div class="value-row-matrix">0</div>
|
||||
<div class="value-row-matrix">1</div>
|
||||
<div class="value-row-matrix">2</div>
|
||||
</div>
|
||||
<div class="body-matrix">
|
||||
<div class="matrix">
|
||||
<div class="matrix-column">
|
||||
<div class="value-matrix-column">0</div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
</div>
|
||||
<div class="matrix-column">
|
||||
<div class="value-matrix-column">1</div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
</div>
|
||||
<div class="matrix-column">
|
||||
<div class="value-matrix-column">2</div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
<div class="auto-matrix-cell"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>-->
|
||||
<!--<div class="popup-cont">
|
||||
<div class="popup">
|
||||
<div class="popup-el popup-header">
|
||||
<div class="header">Эйлеров граф <span class="version">v0.1</span></div>
|
||||
<div class="menu-icon close-icon"></div>
|
||||
</div>
|
||||
<div class="popup-el popup-text">
|
||||
<guide v-show="showGuide" :steps="guide" @close="showGuide = false"></guide>
|
||||
<popup v-show="showInfo" @close="showInfo = false">
|
||||
<template v-slot:title>
|
||||
Циклы в эйлером графе
|
||||
<span class="version">v0.2</span>
|
||||
</template>
|
||||
<template v-slot:content>
|
||||
<p>
|
||||
Граф как математический объект есть совокупность двух множеств —
|
||||
множества самих объектов, называемого множеством вершин, и множества
|
||||
их парных связей, называемого множеством рёбер.
|
||||
<b>Эйлеров цикл</b>
|
||||
— путь, проходящий по всем ребрам графа, и при этом только по
|
||||
одному разу.
|
||||
</p>
|
||||
<p>Элемент множества рёбер есть пара элементов множества вершин.</p>
|
||||
</div>
|
||||
<div class="popup-el popup-text gray">by Robonen</div>
|
||||
<div class="popup-el popup-button-cont">
|
||||
<div class="popup-button">Уволиться</div>
|
||||
<div class="main-popup-button">Поставить 10 баллов</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>-->
|
||||
</template>
|
||||
</popup>
|
||||
<div class="addition-cont menu">
|
||||
<div class="menu-cont">
|
||||
<div class="menu-icon matrix-icon">
|
||||
<div class="menu-prompt">Матрица смежности</div>
|
||||
</div>
|
||||
<div class="menu-icon help-icon">
|
||||
<div class="menu-icon help-icon" @click="showGuide = true">
|
||||
<div class="menu-prompt">Обучение управлению</div>
|
||||
</div>
|
||||
<div class="menu-icon info-icon">
|
||||
<div class="menu-icon info-icon" @click="showInfo = true">
|
||||
<div class="menu-prompt">О программе</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<header>
|
||||
<!--<div class="error">Эйлерова цикла в этом графе нет</div>-->
|
||||
<transition name="fade">
|
||||
<div v-if="errorText" class="error" @click="errorText = ''">
|
||||
{{ errorText }}
|
||||
</div>
|
||||
</transition>
|
||||
<div class="header-step-cont inaccessible">
|
||||
<div class="header-step-text">
|
||||
<div class="header-vertex">
|
||||
@@ -142,23 +102,54 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, ref } from "vue";
|
||||
import Graph from "../components/Graph.vue";
|
||||
// Frontend не выдержит ещё одних правок. Тут и так сейчас много говна
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import Graph from "../components/Graph";
|
||||
import Guide from "../components/Guide";
|
||||
import Popup from "../components/Popup";
|
||||
|
||||
export default {
|
||||
name: "View",
|
||||
components: {
|
||||
Graph,
|
||||
Guide,
|
||||
Popup,
|
||||
},
|
||||
setup() {
|
||||
// Const
|
||||
let timer = null;
|
||||
const guide = [
|
||||
{
|
||||
name: "Создание вершин",
|
||||
content:
|
||||
"Для создания новой вершины необходимо дважды нажать левую кнопку мыши",
|
||||
video: "/video/create.mp4",
|
||||
},
|
||||
{
|
||||
name: "Связывание вершин",
|
||||
content:
|
||||
"Чтобы связать вершины, необходимо кликнуть левой кнопкой мыши по вершине, которую необходимо связать. Далее выбираются вершины, с которыми необходимо связать",
|
||||
video: "/video/linking.mp4",
|
||||
},
|
||||
];
|
||||
|
||||
// Reactive
|
||||
const steps = ref([]);
|
||||
const currentStep = ref(0);
|
||||
const currentStepData = ref({});
|
||||
const played = ref(false);
|
||||
const showInfo = ref(false);
|
||||
const showGuide = ref(false);
|
||||
const errorText = ref("");
|
||||
|
||||
// Mounted
|
||||
onMounted(() => {
|
||||
const key = "first_start";
|
||||
|
||||
if (JSON.parse(localStorage.getItem(key)) !== true) {
|
||||
localStorage.setItem(key, true);
|
||||
showGuide.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Computed
|
||||
const stepExists = computed(() => {
|
||||
@@ -170,6 +161,10 @@ export default {
|
||||
});
|
||||
|
||||
// Methods
|
||||
const log = () => {
|
||||
showInfo.value = true;
|
||||
};
|
||||
|
||||
const getSteps = (data) => {
|
||||
steps.value = data;
|
||||
currentStep.value = 0;
|
||||
@@ -215,12 +210,12 @@ export default {
|
||||
prevStep,
|
||||
play,
|
||||
stop,
|
||||
log,
|
||||
showInfo,
|
||||
showGuide,
|
||||
guide,
|
||||
errorText,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "~@/assets/css/creation.css";
|
||||
@import "~@/assets/css/graph.css";
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user