init report && move live_monitor_vue

This commit is contained in:
2024-03-22 13:27:00 +03:00
parent b215f88b2c
commit 4e123e6555
255 changed files with 79351 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
<script>
import {mapGetters, mapMutations} from 'vuex'
import MenuContainer from "@frames/Menu/MenuContainer.vue"
export default {
name: 'App',
components: {
MenuContainer
},
data() {
return {}
},
computed: {
...mapGetters('auth', ['inited']),
pageName() {
return this.$route.name
},
outerPages () {
return ['auth', '404']
},
innerPage () {
return !this.outerPages.includes(this.pageName) && this.inited
},
outerPage () {
return this.outerPages.includes(this.pageName) && this.inited
}
},
mounted() {
this.initAuth(localStorage.getItem('token'))
},
methods: {
...mapMutations('auth', ['initAuth'])
}
}
</script>
<template>
<div
v-if="innerPage"
class="flex min-h-[100vh]"
>
<MenuContainer>
<router-view />
</MenuContainer>
</div>
<div
v-if="outerPage"
class="relative"
>
<router-view />
</div>
</template>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,53 @@
<!-- eslint-disable -->
<script>
import Modal from '@molecules/Modal/index.vue'
export default {
name: 'Accordion',
components: {
Modal,
},
props: {
isOpen: {
type: Boolean,
default: false
},
id: {
type: String,
default: ""
},
},
setup(props, {slots}) {
const hasHeader = slots.header != undefined
const hasBody = slots.body != undefined
return {
hasHeader,
hasBody
}
},
computed: {
},
methods: {
}
}
</script>
<!-- eslint-disable -->
<template>
<div class="relative w-full">
<input type="checkbox" :id="id" value="" :checked="isOpen" class="hidden peer" required="">
<label :for="id" class="inline-flex items-center justify-between w-full px-5 h-[50px] text-gray-600 bg-white border border-gray-200 rounded-lg cursor-pointer dark:hover:text-gray-300 dark:border-gray-700 peer-checked:border-slate-300 peer-checked:rounded-b-none peer-checked:border-slate-200 hover:text-gray-600 dark:peer-checked:text-gray-300 peer-checked:text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:bg-gray-800 transition-all">
<div v-if="hasHeader"><slot name="header" /></div>
</label>
<i class="ri-arrow-down-s-line absolute right-[10px] top-[15px] peer-checked:rotate-180 transition-all pointer-events-none"></i>
<div v-if="hasBody" class="hidden peer-checked:block transition-all w-full px-5 py-4 text-gray-600 bg-white border border-gray-200 rounded-b-lg cursor-default">
<slot name="body" />
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,53 @@
<!-- eslint-disable -->
<template>
<button @click="setSelected()" :type="type" :class="classBtn" class="w-full relative col-span-4 md:col-span-2 items-center justify-center text-center border-solid border-blue-1 z-10 cursor-pointer">
<i v-if="classIcon" :class="classIcon" class="text-blue-1"></i>
{{ title }}
</button>
</template>
<script>
export default {
components: {
},
props: {
onClick: {
type: Function,
default: () => {
},
},
selected: {
type: String,
default: "",
},
type: {
type: String,
default: "",
},
minWidth: {
type: String,
default: "80px",
},
title: {
type: String,
default: "",
},
classBtn: {
type: Array,
default: () => [],
},
classIcon: {
type: String,
default: "",
}
},
methods: {
setSelected: function() {
this.onClick(this.selected)
}
}
}
</script>

View File

@@ -0,0 +1,59 @@
<template>
<fwb-button
color="dark"
:onclick="onClick"
outline
square
>
<svg
class="feather feather-x-square"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<rect
height="18"
rx="2"
ry="2"
width="18"
x="3"
y="3"
/>
<line
x1="9"
x2="15"
y1="9"
y2="15"
/>
<line
x1="15"
x2="9"
y1="9"
y2="15"
/>
</svg>
</fwb-button>
</template>
<script>
import {FwbButton} from "flowbite-vue";
export default {
components: {
FwbButton
},
props: {
onClick: {
type: Function,
default: () => {
},
}
}
}
</script>

View File

@@ -0,0 +1,41 @@
<template>
<fwb-button
color="dark"
:onclick="onClick"
outline
square
>
<svg
class="feather feather-edit"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
</fwb-button>
</template>
<script>
import {FwbButton} from "flowbite-vue";
export default {
components: {
FwbButton
},
props: {
onClick: {
type: Function,
default: () => {
},
}
}
}
</script>

View File

@@ -0,0 +1,42 @@
<template>
<fwb-button
color="dark"
:onclick="onClick"
outline
square
>
<svg
class="feather feather-save"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
<polyline points="17 21 17 13 7 13 7 21" />
<polyline points="7 3 7 8 15 8" />
</svg>
</fwb-button>
</template>
<script>
import {FwbButton} from "flowbite-vue";
export default {
components: {
FwbButton
},
props: {
onClick: {
type: Function,
default: () => {
},
}
}
}
</script>

View File

@@ -0,0 +1,87 @@
<!-- eslint-disable -->
<script>
// import Button from "@/components/1_atoms/Button.vue";
export default {
name: 'DoubleSwitch',
components: {
// Button,
},
props: {
machine: {
type: Object,
default: () => ({})
},
firstTitle: {
type: String,
default: ''
},
secondTitle: {
type: String,
default: ''
},
firstColor: {
type: String,
default: ''
},
secondColor: {
type: String,
default: ''
},
},
data() {
return {
// isChecked: false
}
},
computed: {
},
mounted () {
},
inject: ['isChecked'],
// watch: {
// isChecked: {
// handler(newValue, oldValue) {
// this.$emit('switched', newValue)
// },
// deep: true
// },
// },
emits: ['switched'],
methods: {
buttonClass: function(value) {
return `border border-slate-400 flex items-center justify-center cursor-pointer rounded transition-all text-xs text-center py-[5px] px-2 ${value}`
},
setChecked(e) {
// this.isChecked = e.target.checked
this.$emit('switched', e.target.checked)
}
}
}
</script>
<!-- eslint-disable -->
<template>
<label class="relative inline-flex items-center cursor-pointer">
<input
v-bind="$attrs"
class="input sr-only peer/checkbox"
type="checkbox"
:checked="isChecked"
@change="setChecked"
/>
<div :class="`w-11 h-6 rounded-full peer peer-focus:ring-4 peer-focus:ring-${secondColor}-300 dark:peer-focus:ring-${secondColor}-800 dark:bg-${firstColor}-700 peer-checked/checkbox:after:translate-x-full peer-checked/checkbox:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-${firstColor}-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-${firstColor}-600`" :style="{background: !isChecked ? firstColor : secondColor}"></div>
<span class="block peer-checked/checkbox:hidden ml-2 text-sm font-medium text-gray-900 dark:text-gray-300"> {{ firstTitle }}</span>
<span class="hidden peer-checked/checkbox:block ml-2 text-sm font-medium text-gray-900 dark:text-gray-300"> {{ secondTitle }} </span>
</label>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,109 @@
<template>
<div class="flex border-8 w-1/2 flex-col py-1 gap-y-5">
<div class="flex flex-row gap-y-5 w-full justify-between">
<fwb-p
v-if="!isEdit"
class="self-center px-5"
>
{{ label }}
</fwb-p>
<div
class="flex flex-row gap-x-5 justify-between"
:class="{'w-full': isEdit}"
>
<fwb-input
v-if="isEdit"
class="w-full"
:model-value="realLink"
@update:model-value="args => {tempLink = args}"
/>
<ButtonEdit
v-if="!isEdit"
:on-click="() => {isEdit = true}"
/>
<ButtonSave
v-if="isEdit"
:on-click="() => {clickSave()}"
/>
<ButtonDiscard
v-if="isEdit"
:on-click="() => {clickDiscard()}"
/>
</div>
</div>
<YouTube
v-if="isLinkNormal && isReady"
ref="youtube"
:src="realLink"
width="100%"
/>
</div>
</template>
<script>
import ButtonSave from "@atoms/ButtonSave.vue";
import YouTube from "vue3-youtube";
import ButtonEdit from "@atoms/ButtonEdit.vue";
import ButtonDiscard from "@atoms/ButtonDiscard.vue";
import {FwbInput, FwbP} from "flowbite-vue";
export default {
components: {
FwbInput,
FwbP,
YouTube, ButtonDiscard, ButtonSave, ButtonEdit
},
props: {
videoLink: {
type: String,
default: '',
},
label: {
type: String,
default: ''
},
timeout: {
type: Number,
default: 0
},
},
data() {
return {
isEdit: false,
isReady: false,
tempLink: this.$props.videoLink,
realLink: this.$props.videoLink,
}
},
computed: {
isLinkNormal() {
return this.$data.realLink.toString().includes("https://www.youtube.com/watch?")
}
},
mounted() {
if (this.timeout) {
setTimeout(() => {
this.isReady = true
}, this.timeout);
} else {
this.isReady = true
}
},
methods: {
updateLink(newLink) {
console.log(newLink)
},
clickSave() {
this.$data.isEdit = false
let l = this.$data.tempLink
if (l.toString().includes("https://www.youtube.com/watch?")) {
this.$data.realLink = l
}
},
clickDiscard() {
this.$data.isEdit = false
console.log(`discard ${this.$data.tempLink}`)
}
}
}
</script>

View File

@@ -0,0 +1,75 @@
<!-- eslint-disable vue/prop-name-casing -->
<script>
import Modal from '../Modal/index.vue'
export default {
name: 'ButtonModal',
components: {Modal},
props: {
text: {
default: "",
type: String
},
"btnClass": {
default: "",
type: String,
},
containerClass: {
default: "",
type: String,
},
forcedBtnClass: {
default: "",
type: String
},
onToggle: {
default: () => null,
type: Function
}
},
data() {
return {
open: false,
}
},
computed: {
__btnClass__() {
if (this.forcedBtnClass) return this.forcedBtnClass
return `${this.btnClass} cursor-pointer w-full bg-transparent hover:bg-primary text-primary font-semibold hover:text-white py-1 text-ssm px-4 border border-primary hover:border-transparent rounded-lg text-center transition_up`
}
},
methods: {
toggle () {
this.open = !this.open
this.onToggle({isOpen: this.open})
}
}
}
</script>
<template>
<div
v-if="text"
:class="__btnClass__"
@click="toggle"
>
{{ text }}
</div>
<div
v-if="!text"
:class="__btnClass__"
@click="toggle"
>
<slot name="button" />
</div>
<Modal
v-if="open"
:close="toggle"
:isOpen="true"
:containerClass="containerClass"
>
<slot />
</Modal>
</template>

View File

@@ -0,0 +1,112 @@
<!-- eslint-disable -->
<script>
import { toRefs } from 'vue'
import {mapGetters, mapMutations, mapActions, useStore} from "vuex";
import Button from "@/components/1_atoms/Button.vue";
import ECharts from '@store/hooks/Echarts';
export default {
name: 'Chart',
components: {
Button,
},
props: {
data: {
type: Object,
default: () => {}
},
sizes: {
type: Object,
default: () => {}
},
id: {
type: String,
required: true,
default: ''
},
type: {
type: String,
required: true,
default: ''
},
height: {
type: String,
default: ''
},
maxWidth: {
type: String,
default: ''
},
maxHeight: {
type: String,
default: ''
},
},
data() {
return {
chartStyles: {
maxWidth: `${this.maxWidth}`,
maxHeight: `${this.maxHeight}`,
top: '15px;',
padding: '10px',
margin: '10px',
boxShadow: 'rgb(0 0 0 / 12%) 2px 3px 7px',
border: '1px solid rgba(0, 0, 0, 0.15)',
}
}
},
setup(props) {
},
watch: {
},
computed: {
// ...mapGetters('main', ['activeChartData', 'prevGroupByData', 'activeMenuChartData']),
},
mounted () {
this.$nextTick(function () {
const chart = new ECharts(this.id, this.sizes)
this.setTypeFunc(this.type, chart, this.data)
chart.onClickedBarCharts(this.updateCharts)
})
},
updated () {
this.$nextTick(function () {
const chart = new ECharts(this.id, this.sizes)
this.setTypeFunc(this.type, chart, this.data)
chart.onClickedBarCharts(this.updateCharts)
})
},
methods: {
...mapActions('machines', ['updateCharts']),
buttonClass: function(value) {
return `border border-slate-400 flex items-center justify-center cursor-pointer rounded transition-all text-xs text-center py-[5px] px-2 ${value}`
},
setTypeFunc: function(type, chart, data) {
switch(type) {
case 'pie': return chart.makePieCharts({...data, type: type})
case 'mount': return chart.makeBarMountCharts({...data, type: type, height: this.height})
case 'mainMl': return chart.makeBarCharts({...data, type: type})
case 'default': return chart.makeBarTypesCharts({...data, type: type})
case 'types': return chart.makeBarTypesCharts({...data, type: type})
case 'without_first_layout': return chart.makeBarTypesCharts({...data, type: type})
case 'ml': return chart.makeBarTypesCharts({...data, type: type})
case 'packLines': return chart.makeLinesPackCharts({...data, type: type})
}
}
}
}
</script>
<!-- eslint-disable -->
<template>
<div :id="id" class="flex grow justify-center relative overflow-hidden bg-white" :style="chartStyles">
</div>
<div id="button_close" class="flex absolute h-max top-[50px] right-[45px]">
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,72 @@
<template>
<input
ref="datepicker"
type="text"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full py-1 pl-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 flatpickr-input"
>
</template>
<script>
import flatpickr from "flatpickr";
import { Russian } from "flatpickr/dist/l10n/ru";
export default {
name: 'Datepicker',
components: {},
inject: ['defaultDate'],
props: {
enableTime: {
type: Boolean,
default: false
},
dateFormat: {
type: String,
default: null
},
onchange: {
type: Function,
default: () => null
},
},
data() {
return {
datepicker: null,
value: this.defaultDate
}
},
computed: {
_dateFormat () {
if (this.dateFormat) return this.dateFormat
if (this.enableTime) return "d.m.Y H:i"
return "d.m.Y"
}
},
mounted () {
this.datepicker = flatpickr(this.$refs.datepicker, {
allowInput: true,
enableTime: this.enableTime,
dateFormat: this._dateFormat,
locale: Russian,
onChange: (a,_b,_c) => {
this.onchange(a[0])
}
});
this.datepicker.setDate(this.defaultDate)
},
updated() {
this.$nextTick(function () {
this.datepicker = flatpickr(this.$refs.datepicker, {
allowInput: true,
enableTime: this.enableTime,
dateFormat: this._dateFormat,
locale: Russian,
});
this.datepicker.setDate(this.value)
})
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,300 @@
import Map from 'ol/Map.js';
import OSM from 'ol/source/OSM.js';
import TileLayer from 'ol/layer/Tile.js';
import View from 'ol/View.js';
import { transform, fromLonLat, toLonLat } from "ol/proj";
import Point from "ol/geom/Point";
import {Fill, Stroke, Style, Text, Icon} from 'ol/style.js';
import { Vector as VectorLayer } from "ol/layer";
import { Vector as VectorSource } from "ol/source";
import Feature from "ol/Feature";
import Overlay from "ol/Overlay";
import {toStringHDMS} from 'ol/coordinate.js';
const svg = (color = "black", course) => {
const screen_width = document.documentElement.clientWidth;
let svg_width = 25;
let svg_height = 45;
let text_size = "30px";
if (
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
)
) {
svg_width = screen_width < 700 ? 30 : 50;
}
return `
<svg xmlns="http://www.w3.org/2000/svg" fill="rgb(59 130 246)" width="${svg_width}" height="${svg_height}" viewBox="0 0 24 24" ><path fill="none" d="M0 0h24v24H0z"/><path d="M18.364 17.364L12 23.728l-6.364-6.364a9 9 0 1 1 12.728 0zM12 15a4 4 0 1 0 0-8 4 4 0 0 0 0 8zm0-2a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></svg>
`
};
const createMap = (elId, zoom, coordinates = [37.619829, 55.752603]) => {
return new Map(
{
target: elId,
layers: [
new TileLayer({
source: new OSM(),
}),
],
view: new View({
projection: "EPSG:3857",
center: fromLonLat(coordinates),
zoom: zoom,
}),
}
)
}
const mapFeature = (coord) => {
return new Feature({
geometry: new Point(coord).transform("EPSG:4326", "EPSG:900913"),
projection: "EPSG:4326",
});
}
const iconFeature = (point) => {
return new Feature({
geometry: new Point(point).transform("EPSG:4326", "EPSG:900913"),
projection: "EPSG:4326",
});
};
const iconStyle = (color, text, course = null) =>
new Style({
image: new Icon({
scale: 1.4,
anchor: [0.45, 32],
anchorXUnits: "fraction",
anchorYUnits: "pixels",
src: "data:image/svg+xml;utf8," + svg(color, course),
}),
text: new Text({
text: text,
offsetY: -14,
offsetX: -18,
textBaseline: 'middle',
backgroundFill: new Fill({color: [90, 90, 60, 0.8]}),
backgroundStroke: new Stroke({color: [25, 230, 115, 0]}),
padding: [3, 3, 3, 3],
textAlign: 'right',
justify: 'left',
font: '12px bold Calibri,sans-serif',
fill: new Fill({
color: 'white',
}),
}),
});
const labelStyle = (text) => new Style({
text: new Text({
text: text,
font: '25px Calibri,sans-serif',
fill: new Fill({
color: '#fff',
}),
}),
});
const addLayerIcon = (map, coordinates, text) => {
return map.addLayer(
new VectorLayer({
type: "vector_layer",
source: new VectorSource({
features: [iconFeature(coordinates)],
}),
style: [
iconStyle("green", text),
],
})
)
}
const addLayer = (map, coordinates, text) => {
return map.addLayer(
new VectorLayer({
type: "vector_layer",
source: new VectorSource({
features: [mapFeature(coordinates), iconFeature(coordinates)],
}),
style: [
iconStyle("green", text),
],
})
)
}
const setView = (map, zoom, coordinates) => {
return map.setView(new View({
projection: "EPSG:3857",
center: fromLonLat(coordinates),
zoom: zoom,
}))
}
const addLayerWithEmptyCoords = (map, labelText) => {
return map.addLayer(
new VectorLayer({
type: "vector_layer",
background: 'rgba(0,0,0,0.7)',
source: new VectorSource({
features: [mapFeature([37.619829, 55.752603])]
}),
style: [labelStyle(labelText)]
})
)
}
const removeLayers = (map) => {
return map.getLayers().forEach(layer => {
if(layer?.values_?.type == "vector_layer") {
map.removeLayer(layer)
}
})
}
const setTooltip = (map, coordinates, machine, tooltip_id, content_id, closer_id) => {
const tooltipSelector = document.getElementById(tooltip_id)
const contentSelector = document.getElementById(content_id)
const onClose = document.getElementById(closer_id)
const tooltip = new Overlay({
element: tooltipSelector,
})
map.addOverlay(tooltip)
const tooltipContent = new Overlay({
position: coordinates,
positioning: 'center-center',
element: contentSelector,
stopEvent: false,
});
map.addOverlay(tooltipContent)
const onClosed = () => {
if(onClose) {
tooltip.setPosition(undefined)
onClose.blur()
const tooltipText = tooltipSelector.querySelector(`.${content_id}`)
if(tooltipText) {
tooltipText.remove()
}
}
}
map.on('click', function (e) {
const feature = map.forEachFeatureAtPixel(e.pixel, function (feature) {
return feature;
})
onClosed()
const tooltipText = document.createElement('div')
tooltipText.classList.add(content_id)
if (feature) {
const container = tooltip.getElement();
const props = feature.getProperties()
const coordsFormated = feature.getGeometry()
const coordsFormat = feature.getGeometry().getFlatCoordinates()
// const hdms = toLonLat(coordinates)
// setTimeout(() => {
container.style.opacity = "1"
container.style.display = "flex"
container.style.minWidth = "max-content"
tooltipText.innerHTML = `
<div class="flex flex-col justify-items-center">
<div class="tooltip-title flex text-lg text-blue-500"> ${machine.machine_type} </div>
<code> ${coordinates} </code>
<hr class="mt-1 mb-2">
<div class="tooltip-title flex text-sm"><div class="w-20">Imei:</div> <div>${machine.imei}</div> </div>
<div class="tooltip-title flex text-sm"><div class="w-20">Дорога:</div> <div>${machine.railway_name}</div> </div>
<div class="tooltip-title flex text-sm"><div class="w-20">Компания:</div> <div>${machine.org_name}</div> </div>
<div class="tooltip-title flex text-sm"><div class="w-20">8зн номер:</div> <div>${machine.nomer_zn8}</div> </div>
<div class="tooltip-title flex text-sm"><div class="w-20">Устройство:</div> <div>${machine.device_number}</div> </div>
<a class="flex justify-center items-center bg-blue-300 hover:bg-blue-400 text-gray-800 p-1 mt-2 rounded" target="_blank" href="/html/askr_devices/analyze_comm/${machine.imei}"}">Детализация</a>
</div>`
container.prepend(tooltipText)
// }, 200)
tooltip.setPosition(e.coordinate)
}
});
map.on('pointermove', function (e) {
const pixel = map.getEventPixel(e.originalEvent);
const hit = map.hasFeatureAtPixel(pixel);
// console.log('hit', hit)
// console.log('pixel', pixel)
// console.log('map', map)
// map.getTarget().style.cursor = hit ? 'pointer' : '';
})
onClose.addEventListener('click', onClosed)
}
export class CoordsMap {
constructor(id) {
this.id = id
}
loadMap(coordinates, machine) {
if(!coordinates) {
if(!this.map) {
console.log('coordinates', coordinates)
console.log('machine', machine)
this.map = createMap(this.id, 5)
addLayerWithEmptyCoords(this.map, 'Данных о местоположении машины нет')
} else {
removeLayers(this.map)
addLayerWithEmptyCoords(this.map, 'Данных о местоположении машины нет')
setView(this.map, 5, [37.619829, 55.752603])
}
}
if(coordinates) {
console.log('coordinates', coordinates)
console.log('machine', machine)
if(!this.map) {
this.map = createMap(this.id, 8, coordinates)
addLayerIcon(this.map, coordinates, `${coordinates[1] ? +coordinates[1].toFixed(5) : ''} ${coordinates[0] ? +coordinates[0].toFixed(5) : ''}`)
} else {
removeLayers(this.map)
addLayerIcon(this.map, coordinates, `${coordinates[1] ? +coordinates[1].toFixed(5) : ''} ${coordinates[0] ? +coordinates[0].toFixed(5) : ''}`)
setView(this.map, 8, coordinates)
}
}
if(coordinates && machine) {
if(!this.map) {
this.map = createMap(this.id, 8, coordinates)
addLayerIcon(this.map, coordinates, machine.machine_type)
setTooltip(this.map, coordinates, machine, 'popup', 'popup-content', 'popup-closer')
// this.map.addOverlay(popup)
// this.map.addOverlay(marker(coordinates))
// setOverlay(this.map)
} else if (machine) {
removeLayers(this.map)
addLayer(this.map, coordinates, machine.machine_type)
setView(this.map, 8, coordinates)
setTooltip(this.map, coordinates, machine, 'popup', 'popup-content', 'popup-closer')
// this.map.addOverlay(popup)
// this.map.addOverlay(marker(coordinates))
// setOverlay(this.map)
}
}
}
}

View File

@@ -0,0 +1,76 @@
<!-- eslint-disable -->
<script>
import Modal from '@molecules/Modal/index.vue'
import {CoordsMap} from './helpers'
export default {
name: 'MapModal',
components: {
Modal,
},
props: {
open: {
type: Boolean,
default: false
},
close: {
type: Function,
default: () => {}
},
id: {
type: String,
default: ""
},
data: {
type: Object,
default: () => ({})
},
text: {
type: String,
default: ""
},
headerClass: {
type: String,
default: ""
},
containerClass: {
type: String,
default: ""
}
},
updated () {
this.$nextTick(function () {
if (this.data && this.id && this.open) {
const newMap = new CoordsMap(this.id)
newMap.loadMap(this.data.coords, this.data.machine)
}
})
},
computed: {
},
methods: {
}
}
</script>
<!-- eslint-disable -->
<template>
<Modal
v-if="open"
:close="close"
:isOpen="true"
:containerClass="containerClass"
>
<div :class="headerClass">
<slot name="header" />
</div>
<slot />
</Modal>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,87 @@
<template>
<div
v-if="isOpen"
class="absolute"
>
<div class="absolute">
<div
id="select_machines_modal"
class="fixed z-[10000] inset-0"
>
<!-- Modal container -->
<div class="h-screen flex items-center justify-center p-4">
<!-- Overlay -->
<div
class="absolute z-0 inset-0 bg-gray-500 opacity-75"
aria-hidden="true"
/>
<!-- Modal box -->
<div
class="relative max-h-full overflow-y-auto bg-white rounded-lg shadow-xl "
role="dialog"
aria-modal="true"
tabindex="0"
autofocus=""
>
<button
type="button"
class="absolute top-2 right-2 text-gray-400 flex space-x-1 items-center"
aria_label="close modal"
@click="close"
>
<span class="text-sm">(esc)</span>
<i class="ri ri-close-line text-2xl" />
</button>
<div
class="min-w-[350px] min-h-[400px] py"
:class="containerClass"
>
<slot />
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Modal',
components: {},
props: {
isOpen: {
default: false,
type: Boolean
},
close: {
default: () => null,
type: Function
},
containerClass: {
default: "",
type: String,
}
},
data() {
},
computed: {
},
mounted () {
document.addEventListener("keydown", this.onEsc);
},
unmounted() {
document.removeEventListener("keydown", this.onEsc);
},
methods: {
onEsc (e) {
if (e.key === "Escape") {
this.close()
}
}
}
}
</script>

View File

@@ -0,0 +1,41 @@
<template>
<svg
aria-hidden="true"
:width="size"
:height="size"
class="text-gray-200 animate-spin dark:text-gray-600 fill-blue-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</template>
<script>
export default {
name: 'RawSpinner',
components: {},
props: {
size: {
type: Number,
default: 25
},
},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,37 @@
<template>
<div class="w-fit flex items-center">
<RawSpinner
class="mr-2"
/>
<span
v-if="!disableText"
class="text-lg text-slate-700"
>{{ text }}</span>
</div>
</template>
<script>
import RawSpinner from './Spinner.vue'
export default {
name: 'Spinner',
components: {RawSpinner},
props: {
text: {
default: "Загрузка",
type: String
},
disableText: {
default: false,
type: Boolean
}
},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,184 @@
<!-- eslint-disable -->
<script>
import {mapGetters, mapMutations, mapActions, useStore} from "vuex"
import { validation } from './helper'
import { Alert } from "@store/hooks/Alert"
export default {
name: 'Pagination',
components: {
},
props: {
params: {
type: Object,
default: () => ({})
},
},
data() {
return {
inputValue: null,
initLocalStorage: null,
}
},
computed: {
},
watch: {
params: {
handler(value, oldValue) {
if(value) {
const loader = document.querySelector(`#table_loder_${this.params.id}`)
loader.style.display = 'none'
}
},
},
},
mounted () {
},
methods: {
selectOptions: (selectOption, paginationOptions) => {
const options = selectOption ? selectOption : optionsDefault
const currentOptions = options.filter((el) => {
if(el <= Math.ceil(paginationOptions.total / 10) * 10) {
return el
}
})
if(paginationOptions.show_all) {
currentOptions.push('Все')
}
return currentOptions
},
currentEntries: (pagination) => {
if (pagination) {
const endPageDataIdx = pagination.page_size * pagination.page_number
const isEndPageDataIdxMore = endPageDataIdx > pagination.total
if(isEndPageDataIdxMore || pagination.page_size > pagination.total) {
return (`${pagination.page_size * (pagination.page_number - 1) + 1} ... ${pagination.total}`)
} else {
return (`${pagination.page_size * (pagination.page_number - 1) + 1} ... ${pagination.page_size * pagination.page_number}`)
}
}
},
setPagination: function ({pageNumber, pageSize}) {
const loader = document.querySelector(`#table_loder_${this.params.id}`)
loader.style.display = 'block'
this.params.updatePagination({pageNumber: pageNumber, pageSize: pageSize})
},
updateSelectedValue: function (e) {
const loader = document.querySelector(`#table_loder_${this.params.id}`)
loader.style.display = 'block'
this.setPagination({
pageNumber: 1, // this.params.paginationOptions.page_number,
pageSize: e.target.value
})
},
updateInputValue: function (e) {
const pageNumber = e.target.value.trim()
const buttonPageInput = document.querySelector(`.page-button-${this.params.id}`)
if(pageNumber) {
this.inputValue = pageNumber
buttonPageInput.removeAttribute('disabled')
buttonPageInput.classList.remove('opacity-50')
} else {
buttonPageInput.setAttribute('disabled', 'disabled')
buttonPageInput.classList.add('opacity-50')
}
},
setPaginationButton: function (e) {
if(this.inputValue) {
const error = validation(this.inputValue, this.params.paginationOptions)
if(Number(this.inputValue) === this.params.paginationOptions.page_number) {
return false
}
if(!error) {
e.target.removeAttribute('disabled')
e.target.classList.remove('opacity-50')
const loader = document.querySelector(`#table_loder_${this.params.id}`)
loader.style.display = 'block'
e.target.focus()
const params = {pageNumber: this.inputValue, pageSize: this.params.paginationOptions.page_size}
return this.params.updatePagination(params)
} else {
e.target.setAttribute('disabled', 'disabled')
e.target.classList.add('opacity-50')
const errorObj = {
id: this.params.id,
color: "red",
icon: "<i class='ri-alert-line'></i>",
message: error,
delay: 5000,
}
return new Alert(errorObj)
}
}
}
}
}
</script>
<!-- eslint-disable -->
<template>
<div class="pagination w-full flex flex-row items-center justify-between gap-3 mt-2 mb-8 xl:mb-0 pb-8 xl:p-2.5 bg-slate-100 border-2 border-solid border-slate-300 rounded-lg flex-col 2xl:flex-row">
<div class="pagination-left flex flex-row gap-1">
<div class="current-entries flex text-sm font-bold">Показаны {{currentEntries(params?.paginationOptions)}}</div>
<div class="count-entries flex text-sm font-bold">(Всего записей - {{params?.paginationOptions?.total}})</div>
</div>
<div class="pagination-right flex flex-col xl:flex-row items-center gap-3">
<div :id="`table_loder_${params.id}`" class="hidden w-full h-auto bg-light flex items-center justify-center px-2"><div type-node="loader" class="flex my_margin-3 items-center justify-center">
<div role="status">
<svg aria-hidden="true" class="mr-2 w-6 h-6 text-gray-400 animate-spin dark:text-gray-400 fill-white" viewBox="0 0 100 101" fill="none" >
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
</svg>
</div>
</div></div>
<div class="buttons flex h-fit gap-1 sm:gap-3 text-sm">
<div class="flex gap-1 sm:gap-3" v-if="params.paginationOptions.page_number > 1">
<div v-if="params.paginationOptions.page_number - 1 !== 1">
<button :table_btn="`${params.id}`" class="previous flex items-center text-white font-bold bg-slate-600 hover:bg-slate-700 py-1 px-3 gap-2 rounded" @click="setPagination({pageNumber: 1, pageSize: params.paginationOptions.page_size})">
<span>1</span>
</button>
</div>
<button :table_btn="`${params.id}`" class="previous flex items-center text-white font-bold bg-slate-600 hover:bg-slate-700 py-1 px-3 gap-2 rounded" @click="setPagination({pageNumber: params.paginationOptions.page_number - 1, pageSize: params.paginationOptions.page_size})">
<span class="hidden sm:flex">Назад</span><i class="ri-arrow-left-line sm:hidden"></i>
</button>
<button :table_btn="`${params.id}`" class="previous flex items-center text-white font-bold bg-slate-600 hover:bg-slate-700 py-1 px-3 gap-2 rounded" @click="setPagination({pageNumber: params.paginationOptions.page_number - 1, pageSize: params.paginationOptions.page_size})">
<span>{{params.paginationOptions.page_number - 1}}</span>
</button>
</div>
<button :table_btn="`${params.id}`" class="active flex items-center text-blue-700 font-bold bg-white py-1 px-3 gap-2 rounded cursor-default">
<span>{{params.paginationOptions.page_number}}</span>
</button>
<div class="flex gap-1 sm:gap-3" v-if="params.paginationOptions.page_number < params.paginationOptions.total_pages">
<button :table_btn="`${params.id}`" class="next flex items-center text-white font-bold bg-slate-600 hover:bg-slate-700 py-1 px-3 gap-2 rounded" @click="setPagination({pageNumber: params.paginationOptions.page_number + 1, pageSize: params.paginationOptions.page_size})">
<span>{{params.paginationOptions.page_number + 1}}</span>
</button>
<button :table_btn="`${params.id}`" class="next flex items-center text-white font-bold bg-slate-600 hover:bg-slate-700 py-1 px-3 gap-2 rounded" @click="setPagination({pageNumber: params.paginationOptions.page_number + 1, pageSize: params.paginationOptions.page_size})">
<span class="hidden sm:flex">Вперёд</span><i class="ri-arrow-right-line sm:hidden"></i>
</button>
<div v-if="params.paginationOptions.page_number + 1 !== params.paginationOptions.total_pages">
<button :table_btn="`${params.id}`" class="next flex items-center text-white font-bold bg-slate-600 hover:bg-slate-700 py-1 px-3 gap-2 rounded" @click="setPagination({pageNumber: params.paginationOptions.total_pages, pageSize: params.paginationOptions.page_size})">
<span>{{params.paginationOptions.total_pages}}</span>
</button>
</div>
</div>
</div>
<div class="flex flex-row items-center justify-center gap-2"><input v-on:input="updateInputValue" :class="`page-input-${params.id}`" class="flex max-w-[80px] items-center text-slate-700 text-sm font-bold focus-visible:outline-blue-300 bg-white py-1 px-2 gap-2 rounded" value="" placeholder="Страница"/><button @click="setPaginationButton" disabled :class="`page-button-${params.id}`" class="opacity-50 flex items-center text-white font-bold bg-slate-600 hover:bg-slate-700 py-1 px-1 rounded">Перейти</button></div>
<div id="select-on-page" class="select-on-page flex items-center justify-center gap-3 p-1 text-sm text-slate-700 font-bold">
Показать
<select name="on_page" v-bind:value="params.paginationOptions.page_size" v-on:change="updateSelectedValue" :table_option="`${params.id}`" class="flex text-gray-700 pl-3 pr-9 py-0.5 rounded cursor-pointer border-none" style="--tw-ring-color:focus: none" >
<option :key={idx} v-for="(el, idx) in selectOptions(params.selectOption, params.paginationOptions)" :value="`${el === 'Все' ? params.paginationOptions.total : el}`">
{{ el }}
</option>`
</select>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,356 @@
import { always, cond, equals, is, T, mergeLeft } from "ramda";
import Handlebars from "handlebars/dist/cjs/handlebars";
import moment from "moment";
import presetConfig from './presetConfig'
import {parseFunc, parseFunc3} from './stringFunction'
const tablefy = (list = []) => {
const defaultData = (title) => ({
headerFilter: "input",
minWidth: 80,
headerFilterPlaceholder: title,
download: false,
print: false,
vertAlign: "middle",
sorter: "string",
responsive: 0
})
if (!is(Array, list)) return []
return list.map((el) => mergeLeft(el, defaultData(el?.title || "")))
}
const math = (value, operator, operand, options) => {
var left = parseFloat(value);
var right = parseFloat(operand);
switch(operator) {
case "+":
return left + right;
case "-":
return left - right;
case "*":
return left * right;
case "/":
return left / right;
case "%":
return left % right;
default:
return "";
}
}
const link = (col) => ({
...col,
formatter: (cell, { prefix = "", text, key = "id" }, onRendered) => {
onRendered(function () {
let row = cell.getRow().getData();
if (!text && !cell.getValue()) {
return null;
}
if (!row[key]) return null;
cell.getElement().innerHTML = `<a target="_blank" href="${prefix}/${
row[key]
}">${text || cell.getValue()}</a>`;
});
},
});
[
['renderFunc', (strFunc, record, strDataIndex) => {
const pFunc = strFunc.replace(/##/gm, "'")
return parseFunc(pFunc)(record[strDataIndex], record)
}],
['math', math],
['presetConfig', (record, pack) => presetConfig(pack, record)],
[
"dt_shift",
(date, shift, param) => {
return date ? moment(date).add(shift, param) : "нд";
},
],
[
"dt_format",
(date) => {
return date ? moment(date).format("DD.MM.YYYY HH:mm") : "нд";
},
],
[
"dt_db_format",
(date) => {
return date ? moment(date).format("YYYY-MM-DDTHH:mm:ss") : "нд";
},
],
[
"from_now",
(date) => {
return moment(date).fromNow()
},
],
[
"json_parse",
(item) => {
return `
<div>
${item}
</div>
`
},
],
[
"time_from_sec",
(ms) => {
if (!ms) return "н/д"
return `${moment(moment().subtract(Math.round(ms), 'milliseconds')).fromNow()}`.replace("назад", '')
},
],
[
"lenght",
(list) => {
return is(Array, list) ? list.length : 0;
},
],
[
"more",
(one, two) => {
return one > two;
},
],
[
"less",
(one, two) => {
return one < two;
},
],
[
"or",
(one, two) => {
return one || two;
},
],
[
"eq",
(one, two) => {
return one == two;
},
],
[
"from_list",
(list, param_string) => {
}
],
[
"endOf",
(date, param) => moment(date).endOf(param)
],
[
"startOf",
(date, param) => moment(date).startOf(param)
],
[
"render",
(list, key) => {
return list.map(el => el[key]).join(", ")
}
],
[
"get",
(map, key) => {
return map[key] ? `${map[key]}` : ""
}
],
[
"get_from",
(map, key) => {
return map[key] ? `${map[key]}` : ""
}
],
[
"uint8parse",
(list) => pipe(
(x) => Uint8Array.from(x, e => e.charCodeAt(0)),
Array.from
)(list)
],
[
"filter_aoo",
(list = [], key, _availible_list) => {
const availible_list = pipe(
(x) => Uint8Array.from(x, e => e.charCodeAt(0)),
Array.from
)(_availible_list)
return list.filter((el) =>
availible_list.includes(el[key]))
}
]
].forEach(([name, func]) => {
Handlebars.registerHelper(name, func);
});
const render = (col) => ({
...col,
formatter: (cell, _, onRendered) => {
onRendered(function () {
const record = cell.getRow().getData()
// console.log('col.onCell', parseFunc(col.onCell))
if(col.onCell && record.status) {
// cell.getElement().style.backgroundColor = setColorCellFromStatus(record.status)
cell.getElement().style.backgroundColor = parseFunc3(col.onCell)(record)
}
if(col.onCell && !record.status) {
console.log('onCell', col.onCell)
cell.getElement().style.backgroundColor = parseFunc(col.onCell)(record)?.style?.backgroundColor
}
if (`${col.render}`.includes("{{") && `${col.render}`.includes("}}")) {
let record = cell.getRow().getData();
cell.getElement().innerHTML = Handlebars.compile(col.render)(
{...record, __record__: record}
);
} else {
cell.getElement().innerHTML = parseFunc(col.render)(record[col["dataIndex"]] || record[col["field"]] || record[col["key"]], record)
}
});
},
});
let sorter = (a, b) => {
let [a_field, b_field] = [a["field"], b["field"]];
if (a_field.includes("id") && b_field.includes("id"))
return a_field.localeCompare(b_field);
if (a_field.includes("id")) return -1;
if (b_field.includes("id")) return 1;
if (a_field.includes("dt") && b_field.includes("dt"))
return b_field.localeCompare(a_field);
if (a_field.includes("dt")) return -1;
if (b_field.includes("dt")) return 1;
if (a_field.includes("imei") && b_field.includes("imei"))
return b_field.localeCompare(a_field);
if (a_field.includes("imei")) return -1;
if (b_field.includes("imei")) return 1;
return a_field.localeCompare(b_field);
};
const prepCol = (col) =>
cond([
[() => is(String, col.render), () => render(col)],
[equals("link"), () => link(col)],
[T, always(col)],
])(col.mode);
const prepColumns = (cols) =>
is(Array, cols) ? cols.map((col) => prepCol(col)) : [];
const renderGroupHeader = ({ groupHeader, value, count, group, extra }) => {
const group_data = group._group;
if (group_data.level == 0) {
const {company_id, param, train_info: train, date: _date} = group_data.groupList[0].rows[0].data
const date = `${_date}`.replace(/\s(.*)/gm, "")
const icident_group = extra.find((el) =>
el.date === date &&
el.company_id == company_id &&
el.train == train &&
el.param == param
) || {}
return Handlebars.compile(groupHeader)({
_value: value,
_count: count,
extra: extra,
icident_group: icident_group
});
}
return `${value} (${count} записей)`;
};
const groupByFunction = (groupBy) => (data) =>
Handlebars.compile(groupBy)(data);
const makedFiltering = (id, filterParams, selectedFilter, clearFilters) => {
const table = document.querySelector(`#${id}`)
const isFiltering = document.querySelector(`${id}-filters`)
if(id && filterParams) {
filterParams.unshift([])
const filtersContainer = document.createElement('div')
filtersContainer.setAttribute("id", `${id}-filters`)
filtersContainer.classList.add("col-span-12")
filtersContainer.innerHTML = `
<div id="${id}-filtersContainer" class="filters flex flex-row items-center justify-start gap-3 mb-0 pt-2.5 bg-slate-100 rounded-t-md flex-row 2xl:flex-row">
<div id="${id}-selectFilters" class="flex items-center justify-start gap-3 p-1 ml-2 text-base text-slate-700 font-bold">
Фильтр по статусу
<select name="selectFilter" id="${id}-selectFilter" class="flex text-gray-700 pl-3 pr-9 py-0.5 rounded cursor-pointer border-none" style="--tw-ring-color:focus: none" >
${filterParams.map((el) => {
return (`<option value='${el}'>
${el}
</option>`)
})}
</select>
</div>
<button id="${id}-filtersClear"class="w-fit h-fit flex items-center text-white text-sm font-bold bg-slate-600 hover:bg-slate-700 py-1 px-3 gap-1 rounded">Очистить фильтр</button>
</div>`
if(!isFiltering) {
table.before(filtersContainer)
}
else {
isFiltering.remove()
table.before(filtersContainer)
}
const selectFilters = document.querySelectorAll(`#${id}-selectFilter`)
const selectOptions = document.querySelectorAll(`#${id}-selectFilter option`)
const buttonClearFilters = document.querySelector(`#${id}-filtersClear`)
selectFilters.forEach((el) => {
el.onchange = (e) => {
if(e.target.value) {
selectOptions[0].removeAttribute('selected')
console.log('e', e)
console.log('selectFilters[0]', selectFilters[0])
console.log('selectOptions[0][0]', selectOptions[0])
selectedFilter(e.target.value)
} else {
clearFilters()
e.target.setAttribute('selected', 'selected')
}
}
})
buttonClearFilters.onclick = () => {
clearFilters()
selectOptions[0].setAttribute('selected', 'selected')
console.log('selectFilters[0]', selectFilters[0])
}
}
}
const validation = (value, pagination) => {
const pipe = (...fns) => (...x) => fns.reduce((error, validator) => error || validator(...x), undefined)
const required = value => {
return value ? undefined : "*"
}
const minPage = length => value => {
value = value.replace(/\s+/g, '')
return value >= length ? undefined : `Номер страницы должен быть больше или равен ${length}`
}
const maxPage = length => value => {
value = value.replace(/\s+/g, '')
return length >= value ? undefined : `Номер страницы должен быть меньше или равен ${length}`
}
const onlyDigits = value => {
const pattern = /^([\d])*$/g
return pattern.test(value) ? undefined : `Номер страницы должен содержать только цифры`
}
const composedValidators = pipe(onlyDigits, minPage(1), maxPage(pagination.total_pages))
return composedValidators(value)
}
export {prepColumns, sorter, renderGroupHeader, groupByFunction, makedFiltering, tablefy, validation};

View File

@@ -0,0 +1,194 @@
<script>
import {TabulatorFull as Tabulator} from 'tabulator-tables';
import {
prepColumns,
sorter,
renderGroupHeader,
groupByFunction,
makedFiltering,
tablefy
} from "./helper";
import localization from "./localization";
import { cond, equals, is, mergeAll, sort, T, mergeDeepRight } from "ramda";
import { pipe } from "@helpers/functions";
import Pagination from "./Pagination.vue";
export default {
name: 'App',
components: {
Pagination
},
props: {
dataSource: {
type: Array,
default: () => []
},
columns: {
type: Array,
default: () => []
},
initialSort: {
type: String,
default: null
},
groupBy: {
type: String,
default: null
},
groupByTemplate: {
type: String,
default: null
},
groupValues: {
type: String,
default: null
},
height: {
type: String,
default: "550px"
},
groupExtraValues: {
type: String,
default: null
},
groupHeader: {
type: String,
default: null
},
pagination: {
type: Object,
default: null
},
selectOption: {
type: String,
default: null
},
filterData: {
type: String,
default: null
},
withTablefy: {
type: Boolean,
default: true
}
},
data() {
return {
refId: Math.random().toString(36).slice(4),
}
},
computed: {
data () {
if (!is(Array, this.dataSource)) return []
if (this.withTablefy) return tablefy(this.dataSource)
return this.dataSource
}
},
mounted() {
this.makeTabele()
},
updated() {
this.makeTabele()
},
methods: {
makeTabele () {
this.table = new Tabulator(this.$refs[this.refId], this.getConfig())
},
getConfig () {
let configurator = pipe(this.__defaultCfg());
return configurator(
(x) => mergeDeepRight(x, this.__getColsCfg()),
(x) => mergeDeepRight(x, this.__getSortCfg()),
(x) => mergeDeepRight(x, this.__getGroupCfg()),
(x) => mergeDeepRight(x, this.__getGroupHeaderCfg()),
// (x) => mergeAll(x, this.__getPaginationCgs())
);
},
__defaultCfg () {
return {
locale: "ru",
langs: localization(),
height: this.height || "250px",
responsiveLayout: "collapse",
placeholder: "Нет записей",
movableColumns: true,
autoResize: false,
data: this.data,
groupStartOpen: false,
groupHeader: function (value, count, data, group) {
return value + "<span>(" + count + " записей)</span>";
},
};
},
__getColsCfg() {
let layout, columns = []
if (this.autoColumns && is(Object, this.data[0])) {
let first = this.data[0];
let keys = Object.keys(first).map((key) => ({
headerFilter: "input",
headerFilterPlaceholder: key,
title: key,
field: key,
download: false,
print: false,
vertAlign: "middle",
sorter: "string",
responsive: 0,
minWidth: 80,
maxWidth: 300,
}));
layout = "fitDataFill"
columns = sort(sorter, keys)
} else {
layout = "fitDataStretch"
columns = prepColumns(this.columns)
}
if (this.withTablefy) {
columns = tablefy(columns)
}
return {columns, layout}
},
__getSortCfg() {
if (this.initialSort) return { initialSort: this.initialSort };
return {};
},
__getGroupCfg() {
if (this.groupByTemplate)
return { groupBy: groupByFunction(this.groupByTemplate) };
if (this.groupBy) return { groupBy: this.groupBy };
return {};
},
__getGroupHeaderCfg() {
if (this.groupHeader)
return {
groupHeader: (value, count, data, group) =>
renderGroupHeader({
extra: this.groupExtraValues,
groupHeader: this.groupHeader,
value,
count,
data,
group,
}),
};
return {};
},
}
}
</script>
<template>
<div class="mb-4">
<div class="overflow-x-auto w-full">
<div
:ref="refId"
class="w-full"
/>
</div>
</div>
<div v-if="pagination">
<Pagination :params="pagination"/>
</div>
</template>

View File

@@ -0,0 +1,29 @@
const localization = () => ({
ru: {
groups: {
item: "запись",
items: "записей",
},
"pagination":{
"page_size":"Размер таблицы", //label for the page size select element
"page_title":"Показать страницу",//tooltip text for the numeric page button, appears in front of the page number (eg. "Show Page" will result in a tool tip of "Show Page 1" on the page 1 button)
"first":"К первой", //text for the first page button
"first_title":"Показать страницу", //tooltip text for the first page button
"last":"К последней",
"last_title":"Последняя страница",
"prev":"<<",
"prev_title":"<<",
"next":">>",
"next_title":">>",
"all":"Все",
"counter":{
"showing": "Показать",
"of": "из",
"rows": "строки",
"pages": "страницы",
}
},
},
});
export default localization;

View File

@@ -0,0 +1,244 @@
import { cond, T } from "ramda";
const text = `
0 Конфигурация не установлена (по умолчанию)
1 Стандартная конфигурация МРТ (МПТ/АВФ)
2 Стандартная конфигурация ЖДСМ
3 Стандартная конфигурация УК/ВПО
4 ЩОМ/РМ (Акселерометр)
5 СМ (Сибирский ПГУПС)
6 СМ (зарезервировано)
7 Эмуляция платы БКП
8 Конфигурация Блок-М
40 Эмуляторы
41 Эмуляторы (CAN1 + CAN2 (Блок-М) + ModBus
50 Стандартная конфигурация ModBus Master (опрос внешних устройств)
51 Стандартная конфигурация ModBus Master (опрос плат АС КРСПС)
52 Конфигурация трекера (электрички и т.п.)
53 Конфигурация трекер + счетчик импульсов + акселерометр
54 РадарП
55 СДРГО
100 ПМА-1М РПМ
101 ЩОМ-1400 РПМ
102 АМ-140 СКРТ РПМ
103 АС-01 РПМ
104 ТЭС ПСС-1К РПМ
105 ПСС-2П РПМ
200 РПБ-01 ВНИКТИ
201 МПК Спецлоко
202 УК 25/25 ВНИКТИ
203 СЗ-800 ВНИКТИ
300 ЩОМ-1200С ТЖДМ
301 ЩОМ-2000 ТЖДМc
302 ССГС-1 ТЖДМ
303 ВПО-С ТЖДМ
304 МПВ ТЖДМ
305 УПК ТЖДМ
306 ПРЛ-М ТЖДМ
307 РУ-700 ТЖДМ
308 МПВ Секция 2 ТЖДМ
1911 Конфигурация для отладки
4096 Настраиваемая конфигурация
`
//TODO SERGEY
const parse = () => {
text
return {}
}
const get_f1_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
};
return r[el] || el;
};
const sadko_2_preset = (el) => {
const r = {
0: "Конфигурация, не установлена (по умолчанию)",
1: "Стандартная, конфигурация МРТ (МПТ/АВФ)",
2: "Стандартная конфигурация ЖДСМ",
3: "Стандартная конфигурация УК/ВПО",
4: "ЩОМ/РМ (Акселерометр)",
5: "СМ (Сибирский ПГУПС)",
6: "СМ (зарезервировано)",
7: "Эмуляция платы БКП",
8: "Конфигурация Блок-М",
40: "Эмуляторы",
41: "Эмуляторы (CAN1 + CAN2 (Блок-М) + ModBus",
50: "Стандартная конфигурация ModBus Master (опрос внешних устройств)",
51: "Стандартная конфигурация ModBus Master (опрос плат АС КРСПС)",
52: "Конфигурация трекера (электрички и т.п.)",
53: "Конфигурация трекер + счетчик импульсов + акселерометр",
54: "РадарП",
55: "СДРГО",
100 : "ПМА-1М РПМ",
101 : "ЩОМ-1400 РПМ",
102 : "АМ-140 СКРТ РПМ",
103 : "АС-01 РПМ",
104 : "ТЭС ПСС-1К РПМ",
105 : "ПСС-2П РПМ",
200 : "РПБ-01 ВНИКТИ",
201 : "МПК Спецлоко",
202 : "УК 25/25 ВНИКТИ",
203 : "СЗ-800 ВНИКТИ",
300 : "ЩОМ-1200С ТЖДМ",
301 : "ЩОМ-2000 ТЖДМ",
302 : "ССГС-1 ТЖДМ",
303 : "ВПО-С ТЖДМ",
304 : "МПВ ТЖДМ",
305 : "УПК ТЖДМ",
306 : "ПРЛ-М ТЖДМ",
307 : "РУ-700 ТЖДМ",
308 : "МПВ Секция 2 ТЖДМ",
1911: "Конфигурация для отладки",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_4_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
1: "Стандартная конфигурация МРТ (МПТ/АВФ)",
2: "Стандартная конфигурация ЖДСМ",
3: "Стандартная конфигурация УК/ВПО",
4: "ЩОМ/РМ (Акселерометр)",
5: "СМ (Сибирский ПГУПС)",
6: "СМ (зарезервировано)",
7: "Эмуляция платы БКП",
8: "Конфигурация Блок-М",
40: "Эмуляторы (CAN1 + ОНК-160 + ModBus)",
41: "Эмуляторы (CAN1 + CAN2 (Блок-М) + ModBus)",
50: "Стандартная конфигурация ModBus Master (опрос внешних устройств)",
51: "Стандартная конфигурация ModBus Master (опрос плат АС КРСПС)",
52: "Конфигурация трекера (электрички и т.п.)",
53: "Конфигурация трекер + счетчик импульсов + акселерометр",
54: "РадарП",
55: "СДРГО",
100: "ПМА-1М РПМ",
101: "ЩОМ-1400 РПМ",
102: "АМ-140 СКРТ РПМ",
103: "АС-01 РПМ",
104: "ТЭС ПСС-1К РПМ",
105: "ПСС-2П РПМ",
200: "РПБ-01 ВНИКТИ",
201: "МПК Спецлоко",
202: "УК 25/25 ВНИКТИ",
203: "СЗ-800 ВНИКТИ",
300: "ЩОМ-1200С ТЖДМ",
301: "ЩОМ-2000 ТЖДМ",
302: "ССГС-1 ТЖДМ",
303: "ВПО-С ТЖДМ",
304: "МПВ ТЖДМ",
305: "УПК ТЖДМ",
306: "ПРЛ-М ТЖДМ",
307: "РУ-700 ТЖДМ",
308: "МПВ Секция 2 ТЖДМ",
1911: "Конфигурация для отладки",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_skrt_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
1: "Обработка импульсной СКРТ",
2: "Конфигурация ModBus Slave Base",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const uk_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
1: "УК AC с подключением по SPI",
2: "УК AC с подключением по LORA",
3: "УК DC с подключением по SPI",
4: "УК DC с подключением по LORA",
5: "ВПО AC/DC с подключением по SPI",
6: "ВПО AC/DC с подключением по LORA",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_64_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_dig_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
1: "Конфигурация с эмуляторами",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_69_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_80_preset = (el) => {
const r = {
0: "Плата с экраном для СМ",
1: "Плата с экраном для ПСС",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_81_preset = (el) => {
const r = {
0: "Плата с ультразвуковыми датчиками (СМ и т.п.)",
4096: "Плата с ультразвуковыми датчиками (СМ и т.п.) настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_82_preset = (el) => {
const r = {
0: "Плата с дискретными входами + LORA (СМ Tail)",
4096: "Плата с дискретными входами + LORA (СМ Tail) настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_83_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
1: "Конфигурация для МПТ",
2: "Конфигурация для ТЭС ПСС-1К",
3: "Конфигурация для СМ (СМ-2Б)",
4: "Конфигурация для Динамик",
5: "Конфигурация для УТМ-5",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const presetConfig = (el, record) => {
const { board_id: board_id } = record;
board_id == 64 && console.log(record)
return cond([
[(board) => board == 1, () => get_f1_preset(el)],
[(board) => board == 2, () => sadko_2_preset(el)],
[(board) => board == 4, () => sadko_4_preset(el)],
[(board) => board == 16, () => sadko_skrt_preset(el)],
[(board) => board == 19, () => uk_preset(el)],
[(board) => board == 64, () => sadko_64_preset(el)],
[(board) => board == 68, () => sadko_dig_preset(el)],
[(board) => board == 69, () => sadko_69_preset(el)],
[(board) => board == 80, () => sadko_80_preset(el)],
[(board) => board == 81, () => sadko_81_preset(el)],
[(board) => board == 82, () => sadko_82_preset(el)],
[(board) => board == 83, () => sadko_83_preset(el)],
[T, () => el],
])(board_id);
};
export default presetConfig

View File

@@ -0,0 +1,406 @@
// import "antd/dist/antd.css";
import axios from "axios";
import { parseSepareteDate } from "./time_funcs";
import {
range,
groupBy,
find,
propEq,
findIndex,
update,
is,
cond,
T,
isNil
} from "ramda";
let getPackSettings = (userId, page, packStructId, func) => {
if (userId && page && packStructId) {
axios({
method: "GET",
url: `/api/settings/${userId}/${page}/${packStructId}`,
headers: {
Authorization: `vniizht ${btoa(JSON.stringify({ date: new Date() }))}`,
},
})
.then(({ data }) => {
func(data)
})
.catch((e) => console.warn(e));
}
};
const prepDiscretes = (from, to, arr) =>
range(from, to + 1).map((el) => ({
title: el,
dataIndex: `disc${el}`,
key: `disc${el}`,
width: 60,
render: (v) =>
v ? (
"вкл"
) : (
"выкл"
),
}));
const is_number = (val) => {
try {
return +val;
} catch (_e) {
return false;
}
};
const is_dt = (val) => {
let is_invalid = new Date(val) !== "Invalid Date";
return val && val != "" && !is_invalid;
};
const compare = (val, from, untill) => {
if (is_number(val)) {
return val >= +from && val <= +untill;
}
if (is_dt(val)) {
let val = new Date(val).getTime;
let from = new Date(from).getTime;
let untill = new Date(untill).getTime;
return val >= from && val <= untill;
}
return (
`${val}`.toLowerCase() == `${from}`.toLowerCase() ||
`${val}`.toLowerCase() == `${untill}`.toLowerCase()
);
};
const withCustomSettings = (_columns, requestSettings) => {
let columns = _columns
if (requestSettings?.colors?.isOn) {
}
if (requestSettings?.colors?.isOn) {
let colors = requestSettings.colors.list.filter((el) => el.isOn);
colors = groupBy((c) => c.param)(colors);
coloredParams = Object.keys(colors);
columns = columns.map((el) => {
if (coloredParams.includes(el.dataIndex)) {
return {
...el,
onCell: (data, index) => {
let colrSettings = colors[el.dataIndex];
let value = data[el.dataIndex];
let color = find(({ from, untill }) =>
compare(value, from, untill)
)(colrSettings);
return {
style: { background: color?.color?.hex || "white" },
};
},
};
}
return el;
});
}
let sort_order =
requestSettings?.settings?.sort_order == "desc" ? "descend" : "ascend";
let prim_param = requestSettings?.settings?.date_param;
let prim_base_date =
find(propEq("dataIndex", prim_param), columns) ||
find(propEq("dataIndex", "pack_dt_new"), columns);
let prim_index = findIndex(propEq("dataIndex", prim_param), columns);
// сделано, потому что в пакете может быть подменена дата
if (prim_index < 0) {
prim_base_date = find(propEq("dataIndex", "pack_dt_new"), columns);
prim_index = findIndex(propEq("dataIndex", "pack_dt_new"), columns);
}
if (prim_index >= 0) {
let sec_param = prim_param == "pack_dt" ? "dt" : "pack_dt";
let sec_base_date = find(propEq("dataIndex", sec_param), columns);
let sec_index = findIndex(propEq("dataIndex", sec_param), columns);
if (sec_index < 0) {
sec_base_date = find(propEq("dataIndex", "pack_dt_new"), columns);
sec_index = findIndex(propEq("dataIndex", "pack_dt_new"), columns);
}
if (sec_index > 0) {
columns = update(
prim_index,
{
...prim_base_date,
defaultSortOrder: sort_order,
sorter: (a, b) => new Date(a[prim_param]) - new Date(b[prim_param]),
},
columns
);
let sec_object = {
...sec_base_date,
sorter: (a, b) => new Date(a[prim_param]) - new Date(b[prim_param]),
};
delete sec_object.defaultSortOrder;
columns = update(sec_index, sec_object, columns);
}
}
return columns;
};
const fetch_sorter_field = (sorter, req_params) => {
const date_param = req_params?.settings?.date_param
return cond([
[() => sorter?.field, () => sorter.field],
[is(Array), field_from_arr(sorter, date_param)],
[isNil, () => date_param],
])(sorter)
}
const field_from_arr = (sorter, date_param) => () => {
const field = sorter.find((el) => `${el?.field}`.includes("dt"))?.field
return field || date_param
}
const get_f1_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
};
return r[el] || el;
};
const sadko_2_preset = (el) => {
const r = {
0: "Конфигурация, не установлена (по умолчанию)",
1: "Стандартная, конфигурация МРТ (МПТ/АВФ)",
2: "Стандартная конфигурация ЖДСМ",
3: "Стандартная конфигурация УК/ВПО",
4: "ЩОМ/РМ (Акселерометр)",
5: "СМ (Сибирский ПГУПС)",
6: "СМ (зарезервировано)",
7: "Эмуляция платы БКП",
8: "Конфигурация Блок-М",
40: "Эмуляторы",
41: "Эмуляторы (CAN1 + CAN2 (Блок-М) + ModBus",
50: "Стандартная конфигурация ModBus Master (опрос внешних устройств)",
51: "Стандартная конфигурация ModBus Master (опрос плат АС КРСПС)",
52: "Конфигурация трекера (электрички и т.п.)",
53: "Конфигурация трекер + счетчик импульсов + акселерометр",
54: "РадарП",
55: "СДРГО",
100 : "ПМА-1М РПМ",
101 : "ЩОМ-1400 РПМ",
102 : "АМ-140 СКРТ РПМ",
103 : "АС-01 РПМ",
104 : "ТЭС ПСС-1К РПМ",
105 : "ПСС-2П РПМ",
200 : "РПБ-01 ВНИКТИ",
201 : "МПК Спецлоко",
202 : "УК 25/25 ВНИКТИ",
203 : "СЗ-800 ВНИКТИ",
300 : "ЩОМ-1200С ТЖДМ",
301 : "ЩОМ-2000 ТЖДМ",
302 : "ССГС-1 ТЖДМ",
303 : "ВПО-С ТЖДМ",
304 : "МПВ ТЖДМ",
305 : "УПК ТЖДМ",
306 : "ПРЛ-М ТЖДМ",
307 : "РУ-700 ТЖДМ",
308 : "МПВ Секция 2 ТЖДМ",
1911: "Конфигурация для отладки",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_4_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
1: "Стандартная конфигурация МРТ (МПТ/АВФ)",
2: "Стандартная конфигурация ЖДСМ",
3: "Стандартная конфигурация УК/ВПО",
4: "ЩОМ/РМ (Акселерометр)",
5: "СМ (Сибирский ПГУПС)",
6: "СМ (зарезервировано)",
7: "Эмуляция платы БКП",
8: "Конфигурация Блок-М",
40: "Эмуляторы (CAN1 + ОНК-160 + ModBus)",
41: "Эмуляторы (CAN1 + CAN2 (Блок-М) + ModBus)",
50: "Стандартная конфигурация ModBus Master (опрос внешних устройств)",
51: "Стандартная конфигурация ModBus Master (опрос плат АС КРСПС)",
52: "Конфигурация трекера (электрички и т.п.)",
53: "Конфигурация трекер + счетчик импульсов + акселерометр",
54: "РадарП",
55: "СДРГО",
100: "ПМА-1М РПМ",
101: "ЩОМ-1400 РПМ",
102: "АМ-140 СКРТ РПМ",
103: "АС-01 РПМ",
104: "ТЭС ПСС-1К РПМ",
105: "ПСС-2П РПМ",
200: "РПБ-01 ВНИКТИ",
201: "МПК Спецлоко",
202: "УК 25/25 ВНИКТИ",
203: "СЗ-800 ВНИКТИ",
300: "ЩОМ-1200С ТЖДМ",
301: "ЩОМ-2000 ТЖДМ",
302: "ССГС-1 ТЖДМ",
303: "ВПО-С ТЖДМ",
304: "МПВ ТЖДМ",
305: "УПК ТЖДМ",
306: "ПРЛ-М ТЖДМ",
307: "РУ-700 ТЖДМ",
308: "МПВ Секция 2 ТЖДМ",
1911: "Конфигурация для отладки",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_skrt_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
1: "Обработка импульсной СКРТ",
2: "Конфигурация ModBus Slave Base",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const uk_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
1: "УК AC с подключением по SPI",
2: "УК AC с подключением по LORA",
3: "УК DC с подключением по SPI",
4: "УК DC с подключением по LORA",
5: "ВПО AC/DC с подключением по SPI",
6: "ВПО AC/DC с подключением по LORA",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_64_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_dig_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
1: "Конфигурация с эмуляторами",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_69_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_80_preset = (el) => {
const r = {
0: "Плата с экраном для СМ",
1: "Плата с экраном для ПСС",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_81_preset = (el) => {
const r = {
0: "Плата с ультразвуковыми датчиками (СМ и т.п.)",
4096: "Плата с ультразвуковыми датчиками (СМ и т.п.) настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_82_preset = (el) => {
const r = {
0: "Плата с дискретными входами + LORA (СМ Tail)",
4096: "Плата с дискретными входами + LORA (СМ Tail) настраиваемая конфигурация",
};
return r[el] || el;
};
const sadko_83_preset = (el) => {
const r = {
0: "Конфигурация не установлена (по умолчанию)",
1: "Конфигурация для МПТ",
2: "Конфигурация для ТЭС ПСС-1К",
3: "Конфигурация для СМ (СМ-2Б)",
4: "Конфигурация для Динамик",
5: "Конфигурация для УТМ-5",
4096: "Настраиваемая конфигурация",
};
return r[el] || el;
};
const presetConfig = (el, record) => {
const { board_id: board_id } = record;
board_id == 64 && console.log(record)
return cond([
[(board) => board == 1, () => get_f1_preset(el)],
[(board) => board == 2, () => sadko_2_preset(el)],
[(board) => board == 4, () => sadko_4_preset(el)],
[(board) => board == 16, () => sadko_skrt_preset(el)],
[(board) => board == 19, () => uk_preset(el)],
[(board) => board == 64, () => sadko_64_preset(el)],
[(board) => board == 68, () => sadko_dig_preset(el)],
[(board) => board == 69, () => sadko_69_preset(el)],
[(board) => board == 80, () => sadko_80_preset(el)],
[(board) => board == 81, () => sadko_81_preset(el)],
[(board) => board == 82, () => sadko_82_preset(el)],
[(board) => board == 83, () => sadko_83_preset(el)],
[T, () => el],
])(board_id);
};
const fVarsNames = [
"getPackSettings",
"prepDiscretes",
"is_number",
"is_dt",
"compare",
"withCustomSettings",
"parseSepareteDate",
"presetConfig",
];
const fVarsFuncs = [
getPackSettings,
prepDiscretes,
is_number,
is_dt,
compare,
withCustomSettings,
parseSepareteDate,
presetConfig,
];
let parseFunc = (string) => {
return (item, el) => {
const f = new Function(
"item",
"el",
...fVarsNames,
`return (${string})(item, el)`
);
return f(item, el, ...fVarsFuncs);
};
};
let parseFunc2 = (string) => {
const f = new Function(...fVarsNames, `return (() => ${string})()`);
return f(...fVarsFuncs);
};
const parseFunc3 = (string) => {
const f = new Function(`return (${string})`)
return f()
}
export {parseFunc, parseFunc2, parseFunc3}

View File

@@ -0,0 +1,168 @@
import { inc, type, cond, equals, T, pipe } from "ramda";
export const isDate = (date) =>
type(date) === "Date" || new Date(date).getTime() > 0;
export const formateDateToString = (date) =>
isDate(date) && date.toLocaleString();
const monthNames = [
"Январь",
"Февраль",
"Март",
"Апрель",
"Май",
"Июнь",
"Июль",
"Август",
"Сентябрь",
"Октябрь",
"Ноябрь",
"Декабрь",
];
export const reformateDateString = (date, format = "full") =>
!!date ? reformate(date, format) : null;
const reformate = (date, format) =>
cond([
[
equals("time-only"),
() => {
const { hr, min, sec } = getTime(date);
return `${hr}:${min}:${sec}`;
},
],
[
equals("date-only"),
() => {
const { day, mth, year } = getDate(date);
return `${day}/${mth}/${year}`;
},
],
[
equals("date-report"),
() => {
const { day, mth, year } = getDate(date);
return `${year}-${mth}-${day}`;
},
],
[
equals("mth-date-report"),
() => {
date = new Date(date);
return `${getMthName(date)} ${date.getDate()}`;
},
],
[
T,
() => {
const { hr, min, sec } = getTime(date);
const { day, mth, year } = getDate(date);
return `${day}/${mth}/${year} ${hr}:${min}:${sec}`;
},
],
])(format);
export const addHours = (oldDate, h) => {
const date = new Date(oldDate);
date.setHours(date.getHours() + h);
return date;
};
export const lastDay = addHours(new Date(), -24);
const getTime = (date) => {
date = new Date(date);
return {
hr: formatTime(date.getHours()),
minutes: formatTime(date.getMinutes()),
sec: formatTime(date.getSeconds()),
};
};
const getDate = (date) => {
date = new Date(date);
return {
mth: formatDate(date.getMonth()),
day: date.getDate(),
year: date.getFullYear(),
};
};
export const getMthName = (date) => monthNames[date.getMonth()];
export const formatDate = (t) => (inc(t) >= 10 ? inc(t) : `0${inc(t)}`);
export const formatTime = (t) => `0${t}`.slice(-2);
export const getDateByType = (date) => {
if (type(date) === "String") return new Date(date.replace(/\s/, "T"));
return new Date(date);
};
export const parseSepareteDate = (
date = new Date(),
symbol = ".",
mode = "DateDMY"
) => {
const selfDate = getDateByType(date);
const dateBits = {
year: selfDate.getFullYear(),
month: selfDate.getMonth() + 1,
day: selfDate.getDate(),
hours: selfDate.getHours(),
minutes: selfDate.getMinutes(),
seconds: selfDate.getSeconds(),
};
Object.keys(dateBits).forEach((key) => {
if (dateBits[key] < 10) dateBits[key] = `0${dateBits[key]}`;
});
switch (mode) {
case "DateDMY":
return dateBits.day + symbol + dateBits.month + symbol + dateBits.year;
case "DateDM":
return dateBits.day + symbol + dateBits.month;
case "DateYMD":
return dateBits.year + symbol + dateBits.month + symbol + dateBits.day;
case "DateDMYHM":
return `${
dateBits.day + symbol + dateBits.month + symbol + dateBits.year
} ${dateBits.hours}:${dateBits.minutes}`;
case "DateDMYHMS":
return `${
dateBits.day + symbol + dateBits.month + symbol + dateBits.year
} ${dateBits.hours}:${dateBits.minutes}:${dateBits.seconds}`;
case "DateYMDHM":
return `${
dateBits.year + symbol + dateBits.month + symbol + dateBits.day
} ${dateBits.hours}:${dateBits.minutes}`;
case "DateYMDHMS":
return `${
dateBits.year + symbol + dateBits.month + symbol + dateBits.day
} ${dateBits.hours}:${dateBits.minutes}:${dateBits.seconds}`;
case "DateHM":
return `${dateBits.hours}:${dateBits.minutes}`;
case "DateHMDM":
return `${dateBits.hours}:${dateBits.minutes} ${dateBits.day}${symbol}${dateBits.month}`;
case "DateDataBase":
return `${
dateBits.year + symbol + dateBits.month + symbol + dateBits.day
}T${dateBits.hours}:${dateBits.minutes}:${dateBits.seconds}`;
case "DateDataBaseSpace":
return `${
dateBits.year + symbol + dateBits.month + symbol + dateBits.day
} ${dateBits.hours}:${dateBits.minutes}:${dateBits.seconds}`;
default:
return dateBits.day + symbol + dateBits.month + symbol + dateBits.year;
}
};
export const toStartDay = (date = new Date()) => {
const selfDate = getDateByType(date);
return selfDate.setHours(0, 0, 0);
};
export const withTime = (date, time) =>
pipe(
(x) => parseSepareteDate(x, "-", "DateYMD"),
(x) => `${x}T${time}`
)(date);

View File

@@ -0,0 +1,50 @@
<template>
<ButtonModal
text="Выбрать машину"
btnClass="col-span-4"
containerClass="pt-10 px-4 min-w-[80vw]"
>
<Tabulator v-bind="tabulatorOtps" />
</ButtonModal>
</template>
<script>
import ButtonModal from '@molecules/ButtonModal/index.vue'
import Tabulator from "@molecules/Tabulator/index.vue"
export default {
name: 'Name',
components: {ButtonModal, Tabulator},
data() {
},
computed: {
tabulatorOtps() {
return {
dataSource: this.packsData,
columns: [
{
title: "Нормер пакета",
field: "pack_number",
width: 150
},
{
title: "Количество записей",
field: "count",
width: 150
},
{
title: "Последняя дата пакета",
field: "pack_dt",
width: 150,
render: "{{ dt_format (dt_shift pack_dt 3 'hour') }}"
}
],
height: "550px"
}
}
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,223 @@
import { parse } from "handlebars/dist/cjs/handlebars/compiler/base";
import { cond, T, isEmpty } from "ramda";
const setPackColumns = (name) => {
switch(name) {
case 'КолонкиПакетов':
return [
{
title: "Пакет на проде",
field: "switch ",
width: 150,
render: `
<form x-data='{ isProd: {{ isProd }} }' phx-change='update_pack_struct_online:{{ id }}'>
<div class='flex justify-center'>
<div class='form-check form-switch'>
<label for='toogle_group_{{ id }}' class='flex items-center cursor-pointer'>
<input value='true' x-model='isProd' id='toogle_group_{{ id }}' name='toogle_pack_struct_{{ id }}' class='form-check-input appearance-none w-9 rounded-full float-left h-3 align-top bg-white bg-no-repeat bg-contain bg-gray-300 checked:bg-blue-500 focus:outline-none cursor-pointer shadow-sm' type='checkbox' role='switch' id='flexSwitchCheckDefault'>
</label>
</div>
</div>
</form>
`
},
{
title: "Название",
field: "name",
width: 180
},
{
title: "Действия",
field: "actions",
width: 100,
render: `
<div class='flex gap-2 py-1'>
<button style='width: 25px; height: 25px;' class='italic border border-solid transition-all duration-300 rounded-full cursor-pointer hover:text-gray-500 linear-loader card-btn-action pt-[2px] text-primary' phx-click='select_struct:{{ id }}' >
<i class='ri-eye-fill'></i>
</button>
<button style='width: 25px; height: 25px;' class='italic border border-solid transition-all duration-300 rounded-full cursor-pointer hover:text-gray-500 linear-loader card-btn-action'>
<i class='ri-pencil-fill'></i>
</button>
<button style='width: 25px; height: 25px;' class='relative flex items-center justify-center italic border border-solid transition-all duration-300 rounded-full cursor-pointer hover:text-gray-500 linear-loader card-btn-action-red' x-data='{ open: false }' @click='open = !open'>
<div x-show='open' class='popup-text not-italic'>
<span>Вы точно хотите удалить структуру?</span>
<span class='flex gap-2'>
<span class='justify-center flex flex-grow bg-blue-500 hover:bg-blue-700 text-white rounded'>Да</span>
<span class='justify-center flex flex-grow bg-gray-500 hover:bg-gray-700 text-white rounded'>Отмена</span>
</span>
</div>
<i class='ri-delete-bin-fill'></i>
</button>
</div>
`
}
]
case 'Сырые':
return [
{
title: "Время пакета",
field: "pack_dt",
width: 180
},
{
title: "Архивная дата",
field: "arhiv_date",
width: 90
},
{
title: "Номер пакета",
field: "pack_number",
width: 70
},
{
title: "Длинна пакета",
field: "pack_len",
width: 70
},
{
title: "Репл",
field: "id_repl",
width: 70
},
{
title: "IMEI",
field: "imei",
width: 120
},
{
title: "ДАННЫЕ",
field: "data",
width: 600
},
{
title: "Дата начала передачи",
field: "connect_dt",
width: 180
},
{
title: "Дата завершения передачи",
field: "disconnect_dt",
width: 180
}
]
case 'Нераспознанные':
return [
{
title: "Время пакета",
field: "pack_dt",
width: 180
},
{
title: "Архивная дата",
field: "arhiv_date",
width: 90
},
{
title: "Номер пакета",
field: "pack_number",
width: 70
},
{
title: "Длинна пакета",
field: "pack_len",
width: 70
},
{
title: "Репл",
field: "id_repl",
width: 70
},
{
title: "IMEI",
field: "imei",
width: 120
},
{
title: "ДАННЫЕ",
field: "data",
width: 600
},
{
title: "Дата начала передачи",
field: "connect_dt",
width: 180
},
{
title: "Дата завершения передачи",
field: "disconnect_dt",
width: 180
}
]
default: return []
}
}
const prepareSetPackColumns = (name, data) => {
console.log('prepareSetPackColumns data', data);
console.log('prepareSetPackColumns name', name);
switch(name) {
case 'askr_logs':
if (data.length > 0) {
return data[0].map((el) => ({title: Object.keys(el)[0], field: Object.keys(el)[0], width: 150}))
}
if (data.length === 0) {
return []
}
case 'raw':
return setPackColumns('Сырые')
case 'unrecognized':
return setPackColumns('Нераспознанные')
default: return []
}
}
const normalizeCols = (cols) => {
console.log('normalizeCols - cols', cols)
const withoutCoordsPackDtCols = cols.filter((el) => el.field !== 'pack_dt' && el.field !== 'lat_lon')
const coordsField = {
title: "Координаты",
field: "lat_lon",
key: "coords",
render: `(_, rec) => {
if (!rec?.geo) return ""
const lat = rec?.geo?.coordinates[1]
const lon = rec?.geo?.coordinates[0]
return \`<div class="underline underline-offset-2 decoration-dashed cursor-pointer" phx-click="load_map" phx-value-lon="\${lat}" phx-value-lat="\${lon}">
\${lat ? +lat.toFixed(5) : lat} \${lon ? +lon.toFixed(5) : lon}
</div>\`
}`
}
const packDtField = {
title: "Дата формирования пакета",
field: "pack_dt",
key: "pack_dt",
render: `{{ dt_format (dt_shift pack_dt 3 'hour') }}`,
width: 130
}
return [packDtField, coordsField, ...withoutCoordsPackDtCols]
}
const parseCols = (packStructure, report) => {
if (!isEmpty(packStructure)) return normalizeCols(packStructure)
if (!packStructure && report && !isEmpty(report)) return prepareSetPackColumns('askr_logs', report.askr_logs)
if (!packStructure && !report) return []
}
const fetchColumn = (packStructure, report, id = null) => {
console.log('packStructure', packStructure)
console.log('id', id)
const getCurrentPackColls = (packStructure, id) => {
const currentPackStructure = packStructure.find((el) => el.id === id)
console.log('currentPackStructure', currentPackStructure)
const currColumns = currentPackStructure.columns ? currentPackStructure.columns : []
return parseCols(currColumns, null)
}
return cond([
[(packStructure) => !packStructure === true, () => parseCols(null, report)],
[(packStructure) => (packStructure?.length >= 1) && (id && typeof id === 'number'), () => getCurrentPackColls(packStructure, id)],
[(packStructure) => packStructure?.length >= 1, () => parseCols(packStructure[0].columns, null)],
[T, () => parseCols(null, report)],
])(packStructure)
}
export {prepareSetPackColumns, setPackColumns, normalizeCols, parseCols, fetchColumn}

View File

@@ -0,0 +1,58 @@
<!-- eslint-disable -->
<script>
import {mapGetters, mapMutations, mapActions, useStore} from "vuex"
export default {
name: 'PackViewButton',
components: {
},
props: {
active: {
type: Boolean,
default: false
},
mode: {
type: String,
default: ""
},
name: {
type: String,
default: ""
},
inner: {
type: String,
default: ""
},
labelClass: {
type: String,
default: ""
},
idx: {
type: Number,
default: ""
}
},
computed: {
...mapGetters('machines', ['selectedPackMode']),
},
methods: {
...mapActions('machines', ['updateModePack']),
}
}
</script>
<!-- eslint-disable -->
<template>
{{ console.log('selectedPackMode', selectedPackMode)
}}
<button @click="updateModePack(mode)" v-bind:key="idx" type="button" class="w-full">
<input name="switchTableMode" type="radio" :id="name" :value="name" :checked="active" class="hidden peer" required="">
<label :for="name" :class="labelClass" class="transition-all w-full border-slate-600 w-full block cursor-pointer py-1 text-ssm sm:text-sm hover:bg-slate-700 hover:text-white text-slate-600 border-slate-600
peer-checked:bg-slate-600 peer-checked:text-white">
{{ inner }}
</label>
</button>
</template>

View File

@@ -0,0 +1,123 @@
<!-- eslint-disable -->
<script>
import {mapGetters, mapMutations, mapActions, useStore} from "vuex"
import PackViewButton from './Button.vue'
import Spinner from "@molecules/Spinner/index.vue"
import Tabulator from "@molecules/Tabulator/index.vue"
import { isEmpty } from "ramda";
export default {
name: 'PackView',
components: {
PackViewButton,
Spinner,
Tabulator
},
props: {
isLoader: {
type: Boolean,
default: false
},
id: {
type: String,
default: ""
},
},
data () {
return {
hasData: false
}
},
setup (props, {slots}) {
const hasDescription = slots.description != undefined
const hasError = slots.error != undefined
console.log('slots', slots)
return {
hasDescription,
hasError
}
},
updated () {
this.$nextTick(function () {
if (!isEmpty(this.packData)) {
this.hasData = true
}
})
},
computed: {
...mapGetters('machines', ['packData', 'selectPackStructs', 'selectedPackStruct']),
buttonsData: () =>
([
{mode: 'askr_logs', isActive: true, name: 'switch_askr_logs', inner: 'обработанные', labelClass: 'rounded-l-lg border'},
{mode: 'raw', isActive: false, name: 'switch_raw', inner: 'сырые', labelClass: 'border-y'},
{mode: 'unrecognized', isActive: false, name: 'switch_unrecognized', inner: 'нераспознанные', labelClass: 'rounded-r-lg border'},
])
},
methods: {
...mapActions('machines', ['updateSelectStruct']),
updateSelectedValue: function (e) {
console.log('e', e.target.value)
this.updateSelectStruct(Number(e.target.value))
},
}
}
</script>
<!-- eslint-disable -->
<template>
<div class="w-full relative">
{{
console.log('packData', packData)
}}
<div v-if="!hasData" class="w-full h-[50px] rounded-xl bg-slate-200 hidden" :id="`${id}_loader`">
<Spinner/>
</div>
<div v-if="hasData" :id="id" class="w-full rounded-md bg-light border-rounded p-4">
<div class="grid grid-cols-12 mb-6">
<div class="flex flex-col gap-1 col-span-12 sm:col-span-4 md:col-span-5 lg:col-span-5 2xl:col-span-7 sm:mb-0">
<h2 :id="`${id}_pack_name`" class=" text-lg">Пакет: <span v-if="selectPackStructs" >{{selectPackStructs[0]?.packNumber}}</span></h2>
<div v-if="hasDescription" class="text-xs text-slate-500">
<slot name="description" />
</div>
</div>
<!-- <form phx-change="c_packs_mode:on_click" :id="`${id}_table_active`" class="col-span-12 sm:col-span-8 md:col-span-7 lg:col-span-7 2xl:col-span-5 flex justify-center hidden"> -->
<div class="col-span-12 sm:col-span-8 md:col-span-7 lg:col-span-7 2xl:col-span-5 flex justify-center">
<PackViewButton v-for="(button, idx) in buttonsData"
:idx="idx"
:mode="button.mode"
:name="button.name"
:active="button.isActive"
:inner="button.inner"
:labelClass="button.labelClass"
/>
</div>
<!-- </form> -->
</div>
<div class="relative" :id="id">
<div id="selectStructContainer" class="flex gap-2 items-center mb-2">
<span>Выберите структуру:</span>
<select v-if="selectPackStructs" id="selectStruct" name="struct" v-bind:value="selectedPackStruct.id" v-on:change="updateSelectedValue" class=" w-fit bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block py-1 pl-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" style="--tw-ring-color:focus: none" >
<option :key={idx} v-for="(el, idx) in selectPackStructs" :value="el.id">
{{ el.name }}
</option>
</select>
</div>
<div :id="`${id}_report`" class="tab-pane active bg-light relative shadow text-center min-h-[300px] relative rounded-md w-full relative">
<Tabulator v-bind="packData" />
<div v-if="isLoader" class="w-full h-[50px] rounded-xl bg-slate-200 hidden" :id="`${id}_loader`">
<Spinner/>
</div>
</div>
</div>
</div>
<div v-if="hasError" :id="`${id}_error`" class="w-full rounded-md bg-light border-rounded p-4 flex flex-col gap-5 items-center justify-center hidden">
<slot name="error" />
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,10 @@
<script setup>
</script>
<template>
<div class="w-full pl-2 flex items-center h-[40px] border-b border-slate-200">
<div>
Hi!
</div>
</div>
</template>

View File

@@ -0,0 +1,47 @@
<script>
export default {
name: 'BreadCrumbs',
components: {},
props: {
breadcrumbs: {
type: Array,
default: null
},
},
data() {
},
computed: {
},
methods: {
rkey() {
return Math.random().toString(36).slice(4)
}
}
}
</script>
<template>
<!-- BEGIN: Breadcrumb -->
<nav
v-if="breadcrumbs"
aria-label="breadcrumb"
class="-intro-x h-[45px] mr-auto"
>
<ol class="breadcrumb breadcrumb-light">
<li
v-for="{name, link, active} in breadcrumbs"
:key="rkey(name)"
:class="{'breadcrumb-item active': active, 'breadcrumb-item': !active}"
aria-current="page"
>
<a
:href="link"
>{{ name }}</a>
</li>
</ol>
</nav>
<!-- END: Breadcrumb -->
<div class="flex grow w-full border-hide-600" />
</template>

View File

@@ -0,0 +1,43 @@
<script>
export default {
name: 'LeftPanel',
components: {},
data() {
return {
mobile_menu_opened: false
}
},
computed: {
},
methods: {
}
}
</script>
<template>
<div class="flex items-center border-white/[0.2] border-r">
<div
class="flex p-4 cursor-pointer flex md:hidden"
@click="mobile_menu_opened = !mobile_menu_opened"
>
<i
icon="bar-chart-2"
class="w-6 h-6 text-white transform rotate-90"
/>
</div>
<router-link
class="items-center relative -intro-x flex cursor-pointer p-4 hidden md:flex"
to="/?page=machines&mode=cards"
>
<div
class="text-white mx-4"
style="font-weight: 600;"
>
LiveMonitor
</div>
</router-link>
<div class="w-[15px]" />
</div>
</template>

View File

@@ -0,0 +1,75 @@
<script>
import MenuList from './MenuList.vue'
import MobileMenu from './MobileMenu.vue'
import LeftPanel from './LeftPanel.vue'
import Breadcrumbs from './Breadcrumbs.vue'
import RightPanel from './RightPanel.vue'
import {mapGetters} from 'vuex'
export default {
name: 'MenuContainer',
components: {
MobileMenu,
MenuList,
LeftPanel,
Breadcrumbs,
RightPanel
},
data() {
},
computed: {
...mapGetters('layout', ["show_menu", 'is_enabled_menu']),
...mapGetters('auth', ['menuList']),
},
methods: {
}
}
</script>
<template>
<div class="main w-full">
<!-- BEGIN: TOP BAR -->
<div
id="menu"
class="top-bar-boxed w-full h-[59px] mt-[4px] z-[51] relative justify-center border-b border-white/[0.2] -mx-3 px-3 sm:pl-8 md:pt-0 mb-[4px]"
>
<MobileMenu
v-if="false"
current_menu_item=""
current_user=""
:menu_list="menuList"
/>
<div class="w-full">
<div class="flex justify-between items-center">
<LeftPanel />
<Breadcrumbs
v-if="false"
:breadcrumbs="[]"
/>
<RightPanel
:current_user="''"
:page_title="''"
:notifications="[{from: 'Бот из LM', datetime: '10:00', message: 'Проведен редизайн приложения', readed: false}]"
/>
</div>
<div class="sm:hidden flex items-center justify-between" />
</div>
</div>
<!-- END: TOP BAR -->
<div class="wrapper">
<div class="wrapper-box">
<div v-if="show_menu && is_enabled_menu">
<MenuList
:menu_list="menuList"
/>
</div>
<div class="content">
<div class="grid grid-cols-12 gap-6 h-full">
<slot />
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,119 @@
<!-- eslint-disable vue/prop-name-casing -->
<script>
import { cond, equals, T, always } from 'ramda'
import { createIcons, icons } from "lucide";
import MenuItemGroup from './MenuItemGroup.vue'
export default {
name: 'MenuItem',
components: {MenuItemGroup},
props: {
type: {
type: String,
default: null
},
menu_item: {
type: Object,
default: () => ({
link: "/"
})
},
base_class: {
type: String,
default: ""
},
routeName: {
type: String,
default: ""
}
},
data() {
},
computed: {
_base_class() {
return `${this.base_class}`
},
activeClass () {
return 'side-menu side-menu--active'
},
menuRouterName() {
return this.menu_item.router || ""
},
currentRouterName() {
return this.$router.currentRoute._value.name || ""
},
linkClass() {
const menuRouterName = this.menu_item.router || ""
if (menuRouterName === this.routeName)
return `${this._base_class} ${this._base_class}--active`
return this._base_class
},
_type() {
return cond([
[equals("simple_link"), (x) => x],
[equals("line"), (x) => x],
[equals("group"), (x) => x],
[T, () => "other"],
])(this.type)
}
},
mounted() {
this.icons()
},
updated() {
console.log(123)
this.icons()
},
methods: {
icons() {
createIcons({ icons, "stroke-width": 1.5, nameAttr: "icon" })
}
}
}
</script>
<template>
<li v-if="_type == 'simple_link'">
<router-link
phx-hook="MenuLink"
:class="linkClass"
:to="menu_item.link"
>
<div :class="`${_base_class}__icon`">
<i :icon="menu_item?.icon" />
</div>
<div :class="`${_base_class}__title`">
{{ menu_item.show }}
</div>
</router-link>
</li>
<li v-if="_type == 'line'">
<hr class="m-4">
</li>
<MenuItemGroup
v-if="_type == 'group'"
:type="_type"
:base_class="base_class"
:menu_item="menu_item"
/>
<div v-if="_type == 'other'">
""
</div>
</template>
<!-- <router-link
v-if="type === 'link'"
class="flex p-2 rounded-md hover:bg-gray-100 flex items-center"
:class="{'bg-gray-200': pageName === name}"
:to="link"
>
<i :class="`ri-${icon} text-slate-600 mr-2 `" />
<span
v-if="isOpenMenu"
class="text-slate-700"
>{{ title }}</span>
</router-link> -->

View File

@@ -0,0 +1,93 @@
<!-- eslint-disable vue/prop-name-casing -->
<script>
import { cond, equals, T } from 'ramda'
import { createIcons, icons } from "lucide";
export default {
name: 'MenuItemCopy',
components: {},
props: {
type: {
type: String,
default: null
},
menu_item: {
type: Object,
default: () => ({
link: "/"
})
},
base_class: {
type: String,
default: ""
}
},
data() {
},
computed: {
_base_class() {
return `${this.base_class}`
},
activeClass () {
return 'side-menu side-menu--active'
},
menuRouterName() {
return this.menu_item.router || ""
},
currentRouterName() {
return this.$router.currentRoute._value.name || ""
},
linkClass() {
const menuRouterName = this.menu_item.router || ""
const currentRouterName = this.$router.currentRoute._value.name || Math.random()
if (menuRouterName === currentRouterName)
return `${this._base_class} ${this._base_class}--active`
return this._base_class
},
_type() {
return cond([
[equals("simple_link"), (x) => x],
[equals("line"), (x) => x],
[equals("group"), (x) => x],
[T, () => "other"],
])(this.type)
}
},
mounted() {
this.icons()
},
updated() {
this.icons()
},
methods: {
icons() {
createIcons({ icons, "stroke-width": 1.5, nameAttr: "icon" })
}
}
}
</script>
<template>
<li v-if="_type == 'simple_link'">
<router-link
phx-hook="MenuLink"
:class="linkClass"
:to="menu_item.link"
>
<div :class="`${_base_class}__icon`">
<i :icon="menu_item?.icon" />
</div>
<div :class="`${_base_class}__title`">
{{ menu_item.show }}
</div>
</router-link>
</li>
<li v-if="_type == 'line'">
<hr class="m-4">
</li>
<div v-if="_type == 'other'">
""
</div>
</template>

View File

@@ -0,0 +1,80 @@
<!-- eslint-disable vue/prop-name-casing -->
<script>
import MenuItem from './MenuItemCopy.vue'
export default {
name: 'MenuItemGroup',
components: {
MenuItem
},
props: {
type: {
type: String,
default: null
},
base_class: {
type: String,
default: null
},
menu_item: {
type: Object,
default: () => ({})
},
},
data() {
return {
open: false
}
},
computed: {
containerClass() {
const menuRouterName = this.menu_item.router || ""
const currentRouterName = this.$router.currentRoute._value.name || Math.random()
if (menuRouterName === currentRouterName) return `${this.base_class} ${this.base_class}--active`
return `${this.base_class}`
},
itemClass() {
if (this.open) return `${this.base_class}__sub-icon transform rotate-180`
return `${this.base_class}__sub-icon`
},
listClass() {
if (this.open) return `${this.base_class}__sub-open`
return ""
},
menu_list_with_index() {
return this.menu_item.children.map((el, index) => ({...el, index}))
}
},
methods: {
toggle() {
this.open = !this.open
}
}
}
</script>
<template>
<li>
<span
:class="containerClass"
@click="toggle"
>
<div :class="`${base_class}__icon`"> <i :icon="menu_item.icon" /> </div>
<div :class="`${base_class}__title`">
{{ menu_item.show }}
<div :class="itemClass"> <i icon="chevron-down" /> </div>
</div>
</span>
<ul :class="listClass">
<MenuItem
v-for="item in menu_item.children"
:key="JSON.stringify(item)"
:base_class="base_class"
:type="item.type"
:menu_item="item"
/>
</ul>
</li>
</template>

View File

@@ -0,0 +1,63 @@
<!-- eslint-disable vue/prop-name-casing -->
<script>
import {mapGetters} from 'vuex'
import MenuItem from './MenuItem.vue'
export default {
name: 'MenuList',
components: {MenuItem},
props: {
current_menu_item: {
type: String,
default: ""
},
menu_list: {
type: Array,
default: () => []
}
},
data() {
return {
routeName: ""
}
},
computed: {
...mapGetters('', ['mobile_menu_opened', 'show_menu']),
menu_list_with_index() {
return this.menu_list.map((el, index) => ({...el, index}))
}
},
watch:{
$route (to){
this.routeName = to.name;
}
},
methods: {
rkey() {
return Math.random().toString(36).slice(4)
}
}
}
</script>
<template>
<nav
x-show="show_menu"
class="side-nav"
>
<ul>
<MenuItem
v-for="{index, ...menu_item} in menu_list_with_index"
:id="`main_menu_item_component_${index}`"
:key="rkey(index)"
base_class="side-menu"
:type="menu_item.type"
:menu_item="menu_item"
:current_menu_item="current_menu_item"
:routeName="routeName"
/>
</ul>
</nav>
</template>

View File

@@ -0,0 +1,63 @@
<script>
import MenuItem from './MenuItem.vue'
export default {
name: 'MobileMenu',
components: {
MenuItem
},
props: {
// eslint-disable-next-line vue/prop-name-casing
current_menu_item: {
type: String,
default: ""
},
// eslint-disable-next-line vue/prop-name-casing
menu_list: {
type: Array,
default: () => []
}
},
data() {
},
computed: {
menu_list_with_index() {
return this.menu_list.map((el, index) => ({...el, index}))
}
},
methods: {
rkey() {
return Math.random().toString(36).slice(4)
}
}
}
</script>
<template>
<div x-bind:class="mobile_menu_opened ? 'mobile-menu--active mobile-menu md:hidden' : 'mobile-menu md:hidden'">
<div class="scrollable">
<span
class="mobile-menu-toggler"
@click="mobile_menu_opened = !mobile_menu_opened"
>
<i
icon="x-circle"
class="w-8 h-8 text-white transform -rotate-90"
/>
</span>
<ul class="scrollable__content py-2">
<MenuItem
v-for="{index, ...menu_item} in menu_list_with_index"
:id="`main_menu_item_component_${index}`"
:key="rkey(index)"
base_class="menu"
:type="menu_item.type"
:menu_item="menu_item"
:current_menu_item="current_menu_item"
/>
</ul>
</div>
</div>
</template>

View File

@@ -0,0 +1,174 @@
<!-- eslint-disable vue/prop-name-casing -->
<script>
import {mapGetters, mapMutations} from 'vuex'
import { createIcons, icons } from "lucide";
export default {
name: 'RightPanel',
components: {},
props: {
// eslint-disable-next-line vue/prop-name-casing
page_title: {
type: String,
default: ""
}
},
data() {
return {
}
},
computed: {
...mapGetters('auth', ['current_user'])
},
mounted() {
this.icons()
},
updated() {
this.icons()
},
methods: {
...mapMutations('layout', ["toggle_menu"]),
icons() {
createIcons({ icons, "stroke-width": 1.5, nameAttr: "icon" })
},
rkey() {
return Math.random().toString(36).slice(4)
},
closeDropDown() {
setTimeout(() => {
document.querySelector("body").click()
}, 1);
}
},
}
</script>
<template>
<div class="flex items-center justify-end grow gapy-2 px-4 border-hide-600">
<div class="">
{{ page_title }}
</div>
<div class="flex">
<div class="relative">
<div class="intro-x dropdown">
<div
class="dropdown-toggle notification notification--bullet cursor-pointer"
role="button"
aria-expanded="false"
data-tw-toggle="dropdown"
>
<i
icon="bell"
class="cursor-pointer border-white/[0.2] border-r border-l p-4 text-white"
style="width: 55px; height: 55px;"
/>
<div
style="font-size: 7px; font-weight: 900; top: 10px; left: 30px"
class="h-[13px] w-[13px] bg-white rounded-full mr-8 transition absolute flex itens-center justify-center"
>
<span
style="line-height: 13px; font-weight: 900; font-size: 7px"
class="text-black"
>0</span>
</div>
</div>
<div class="notification-content pt-2 dropdown-menu">
<div class="notification-content__box dropdown-content">
<div class="notification-content__title">
Оповещения
</div>
<div
v-for="{from, datetime, message, readed} in notifications"
:key="rkey(message)"
class="cursor-pointer relative flex items-center "
>
<div :class="{'bg-gray-400 w-3 h-3 absolute right-0 bottom-0 rounded-full border-2 border-white dark:border-darkmode-600|, else: ~s|bg-success w-3 h-3 absolute right-0 bottom-0 rounded-full border-2 border-white dark:border-darkmode-600': readed}" />
<div class="ml-2 overflow-hidden">
<div class="flex items-center">
<a
href="javascript:;"
class="font-medium truncate mr-5"
>{{ from }}</a>
<div class="text-xs text-slate-400 ml-auto whitespace-nowrap">
{{ datetime }}
</div>
</div>
<div class="w-full truncate text-slate-500 mt-0.5">
{{ message }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="cursor-pointer border-white/[0.2] border-r p-4">
<i
id="menu_book_icon"
icon="book"
class="text-white w-[24px] h-[24px]"
/>
</div>
<div class="cursor-pointer border-white/[0.2] border-r p-4">
<i
id="menu_question_icon"
icon="help-circle"
class="text-white w-[24px] h-[24px]"
/>
</div>
<div
class="cursor-pointer hidden md:flex border-white/[0.2] border-r p-4"
@click="toggle_menu"
>
<i
id="menu_library"
icon="library"
class="text-white w-[24px] h-[24px]"
/>
</div>
</div>
<!-- BEGIN: Account Menu -->
<div class="intro-x dropdown w-[50px] h-[50px] px-3 flex justify-center items-center">
<div
class="dropdown-toggle rounded-full overflow-hidden shadow-lg image-fit zoom-in scale-110"
role="button"
aria-expanded="false"
data-tw-toggle="dropdown"
>
<img
alt="image"
src="/images/profile-4.jpg"
>
</div>
<div class="dropdown-menu w-56">
<ul class="dropdown-content before:block before:absolute before:bg-black before:inset-0 before:rounded-md before:z-[-1]">
<li class="p-2">
<div class="font-bold">
{{ current_user?.first_name || "" }} {{ current_user?.last_name || "" }}
</div>
<div
class="text-xs text-black/60 dark:text-slate-500"
style="margin-top: -0.25rem;"
>
{{ current_user?.position || "позиция не указана" }}
</div>
</li>
<li>
<hr class="dropdown-divider border-white/[0.08]">
</li>
<li @click="closeDropDown">
<router-link
to="/users/log_out"
class="dropdown-item hover:bg-white/5 hover:text-black"
>
Выйти
</router-link>
</li>
</ul>
</div>
</div>
<!-- END: Account Menu -->
</div>
</template>

View File

@@ -0,0 +1,20 @@
<template>
<div>
123
</div>
</template>
<script>
export default {
name: 'App',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,139 @@
<template>
<div
class="h-[100vh] z-40 bg-white flex flex-col px-4 pt-8 border-r border-slate-200 transition-all"
:class="{'w-[300px]': isOpenMenu, 'w-[70px]': !isOpenMenu}"
>
<div class="pl-1 flex items-center mb-8 justify-between">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--logos"
width="31.88"
height="32"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 256 257"
>
<defs><linearGradient
id="IconifyId1813088fe1fbc01fb466"
x1="-.828%"
x2="57.636%"
y1="7.652%"
y2="78.411%"
><stop
offset="0%"
stop-color="#41D1FF"
/><stop
offset="100%"
stop-color="#BD34FE"
/></linearGradient><linearGradient
id="IconifyId1813088fe1fbc01fb467"
x1="43.376%"
x2="50.316%"
y1="2.242%"
y2="89.03%"
><stop
offset="0%"
stop-color="#FFEA83"
/><stop
offset="8.333%"
stop-color="#FFDD35"
/><stop
offset="100%"
stop-color="#FFA800"
/></linearGradient></defs><path
fill="url(#IconifyId1813088fe1fbc01fb466)"
d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"
/><path
fill="url(#IconifyId1813088fe1fbc01fb467)"
d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"
/>
</svg>
<div
class="p-1 px-2 rounded-md transition-all cursor-pointer hover:bg-gray-200"
:class="{'rotate-90': !isOpenMenu, '-rotate-90': isOpenMenu}"
@click="toggleMenu"
>
<i class="ri-upload-line" />
</div>
</div>
<div class="flex flex-col">
<div
v-for="{title, icon, link, type, name} in menu"
:key="`${title}${name}`"
>
<router-link
v-if="type === 'link'"
class="flex p-2 rounded-md hover:bg-gray-100 flex items-center"
:class="{'bg-gray-200': pageName === name}"
:to="link"
>
<i :class="`ri-${icon} text-slate-600 mr-2 `" />
<span
v-if="isOpenMenu"
class="text-slate-700"
>{{ title }}</span>
</router-link>
<div
v-if="type === 'line'"
class="border-b my-4 border-slate-200"
/>
</div>
</div>
</div>
</template>
<script>
import {mapGetters, mapMutations} from "vuex";
export default {
name: 'App',
components: {},
data() {
const menu = [
{
title: "Главная",
icon: "home-4-line",
link: "/",
type: "link",
name: "main"
},
{
title: "Главная1",
icon: "home-4-line",
link: "/page1",
type: "link",
name: "page1"
},
{
title: "Главная2",
icon: "home-4-line",
link: "/page2",
type: "link",
name: "page2"
},
{
title: "Выход",
icon: "logout-circle-line",
link: "/auth",
type: "link",
name: ""
},
]
return {
menu
}
},
computed: {
...mapGetters('layout', ['isOpenMenu']),
pageName() {
return this.$route.name
}
},
methods: {
...mapMutations('layout', ['toggleMenu']),
}
}
</script>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,159 @@
<template>
<div class="hidden 2xl:flex w-full h-[100vh] overflow-hidden bg-white">
<div class="h-full overflow-hidden flex flex-col justify-between translate-y-1">
<div class="text-3xl text-sky-800 pl-10 pt-10">
<router-link to="/">
Вернуться на главную
</router-link>
</div>
<img
width="100%"
src="/images/not_found.jpg"
alt="404"
>
</div>
</div>
<div class="2xl:hidden flex w-full items-center justify-center h-[100vh] ">
<div
id="notfound"
class="notfound h-fit"
>
<div class="notfound-404">
<div />
<h1>404</h1>
</div>
<h2 class="mb-4">
Страница не найдена
</h2>
<router-link to="/">
На главную
</router-link>
</div>
</div>
</template>
<script>
export default {
name: 'App',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>
<style scoped>
#notfound {
position: relative;
}
#notfound .notfound {
position: absolute;
left: 50%;
top: 50%;
-webkit-transform: translate(-50%,-50%);
-ms-transform: translate(-50%,-50%);
transform: translate(-50%,-50%)
}
.notfound {
max-width: 460px;
width: 100%;
text-align: center;
line-height: 1.4
}
.notfound .notfound-404 {
position: relative;
width: 180px;
height: 180px;
margin: 0 auto 50px
}
.notfound .notfound-404>div:first-child {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: #ffa200;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
border: 5px dashed #000;
border-radius: 5px
}
.notfound .notfound-404>div:first-child:before {
content: '';
position: absolute;
left: -5px;
right: -5px;
bottom: -5px;
top: -5px;
-webkit-box-shadow: 0 0 0 5px rgba(0,0,0,.1) inset;
box-shadow: 0 0 0 5px rgba(0,0,0,.1) inset;
border-radius: 5px
}
.notfound .notfound-404 h1 {
font-family: cabin,sans-serif;
color: #000;
font-weight: 700;
margin: 0;
font-size: 90px;
position: absolute;
top: 50%;
-webkit-transform: translate(-50%,-50%);
-ms-transform: translate(-50%,-50%);
transform: translate(-50%,-50%);
left: 50%;
text-align: center;
height: 40px;
line-height: 40px
}
.notfound h2 {
font-family: cabin,sans-serif;
font-size: 33px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 7px
}
.notfound p {
font-family: cabin,sans-serif;
font-size: 16px;
color: #000;
font-weight: 400
}
.notfound a {
font-family: cabin,sans-serif;
display: inline-block;
padding: 10px 25px;
background-color: #8f8f8f;
border: none;
border-radius: 40px;
color: #fff;
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
text-decoration: none;
-webkit-transition: .2s all;
transition: .2s all
}
.notfound a:hover {
background-color: #2c2c2c
}
</style>

View File

@@ -0,0 +1,194 @@
<script>
import {mapGetters, mapActions, mapMutations} from "vuex";
export default {
name: 'LogIn',
components: {
},
data() {
return {
login: "",
password: ""
}
},
computed: {
...mapGetters('auth', ['errorMsg', "my_comp"])
},
mounted() {
localStorage.removeItem('token')
},
unmounted() {
this.login = ""
this.password = ""
},
methods: {
...mapActions('auth', ['signin']),
...mapMutations('auth', ["set_my_comp" ])
},
}
</script>
<template>
<main>
<div
class="login"
style="max-height: 100vh"
>
<div class="container sm:px-10">
<div class="block xl:grid grid-cols-2 gap-4">
<!-- BEGIN: Login Info -->
<div
style="z-index: 1;"
class="hidden relative xl:flex flex-col min-h-screen"
>
<div class="my-auto">
<div class="-intro-x text-white font-medium text-4xl leading-tight">
<img
alt="логотип"
class="mb-8"
src="/images/illustration.svg"
>
<div>
<a
href="/"
class="-intro-x flex items-center gap-2 pt-5 text-white hover:text-white"
>
<img
alt="логотип"
style="width: 40px; height: 40px"
src="/images/favicon-white.ico"
>
Live Monitor
</a>
</div>
</div>
<div class="-intro-x mt-5 text-lg text-white text-opacity-70 dark:text-slate-400">
Мониторинг СПС максимально приближенный
<br>
к режиму реального времени
</div>
</div>
</div>
<!-- END: Login Info -->
<!-- BEGIN: Login Form -->
<div class="h-screen xl:h-auto flex py-5 xl:py-0 my-10 xl:my-0">
<div class="my-auto mx-auto xl:ml-20 bg-white dark:bg-darkmode-600 xl:bg-transparent px-5 sm:px-8 py-8 xl:p-0 rounded-md shadow-md xl:shadow-none w-full sm:w-3/4 lg:w-2/4 xl:w-auto">
<h2 class="intro-x font-bold text-2xl xl:text-3xl dark:text-gray-100 text-center xl:text-left">
Вход
</h2>
<div class="intro-x mt-2 text-slate-400 xl:hidden text-center">
Мониторинг СПС максимально приближенный
к режиму реального времени
</div>
<div
id="login_form"
phx-update="ignore"
>
<div class="space-y-4 bg-white mt-10 dark:bg-darkmode-600">
<div phx-feedback-for="user[login]">
<label
for="user_login"
class="block text-sm leading-6 text-zinc-800 dark:text-white"
>
Логин
</label>
<input
id="user_login"
v-model="login"
type="text"
name="login"
class="block w-full rounded-lg border-zinc-300 py-[1px] px-[11px] text-zinc-900 focus:outline-none focus:ring-4 text-sm sm:leading-6 phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5 border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5 "
required=""
>
</div>
<div
class="relative"
phx-feedback-for="user[password]"
>
<label
for="user_password"
class="block text-sm leading-6 text-zinc-800 dark:text-white"
>
Пароль
</label>
<input
id="user_password"
v-model="password"
type="password"
name="password"
class="block w-full rounded-lg border-zinc-300 py-[1px] px-[11px] text-zinc-900 focus:outline-none focus:ring-4 text-sm sm:leading-6 phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5 border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5 "
required=""
>
<i
id="show_password"
class="cursor-pointer transition-all text-slate-500 hover:text-slate-800 absolute ri-eye-line bottom-[4px] right-[7px]"
/>
<i
id="hide_password"
class="hidden cursor-pointer transition-all text-slate-500 hover:text-slate-800 absolute ri-eye-off-line bottom-[4px] right-[7px]"
/>
<div
v-if="errorMsg"
class="text-red-600 mt-1 ml-1"
>
{{ errorMsg }}
</div>
</div>
<div class="intro-x flex text-slate-600 dark:text-slate-500 text-xs sm:text-sm mt-4">
<div class="flex items-center mr-auto">
<div phx-feedback-for="user[remember_me]">
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600 dark:text-white">
<input
type="hidden"
name="user[remember_me]"
value="false"
>
<input
id="user_remember_me"
:value="my_comp"
type="checkbox"
name="user[remember_me]"
class="rounded border-zinc-300 text-zinc-900 focus:ring-zinc-900"
@input="set_my_comp"
>
Мой компьютер
</label>
</div>
</div>
<span />
</div>
<div class="intro-x mt-5 xl:mt-8 text-center xl:text-left flex">
<button
type="button"
class="phx-submit-loading:opacity-75 rounded-lg bg-slate-700 hover:bg-slate-800 py-1 px-3 text-sm font-semibold leading-6 text-white active:text-white/80 mr-3 px-4"
phx-disable-with="Вход..."
style="width: 150px"
@click="signin({login, password})"
>
Войти
</button>
<button
type="button"
style="width: 150px"
onclick="toggleDark()"
class="btn btn-outline-secondary py-1 w-full xl:w-36 mt-3 xl:mt-0 align-top"
>
Темный режим
</button>
</div>
</div>
</div>
<div class="text-center my-6">
<span class="inline-block text-sm text-blue-500 align-baseline hover:text-blue-800"> Восстановить пароль можно через обращение в НИАЦ АО ВНИИЖТ</span>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</template>

View File

@@ -0,0 +1,22 @@
<script>
import Login from './Login.vue'
export default {
name: 'Auth',
components: {
Login
},
data() {
return {}
},
computed: {
},
methods: {
}
}
</script>
<template>
<Login />
</template>

View File

@@ -0,0 +1,20 @@
<template>
<div>
123
</div>
</template>
<script>
export default {
name: 'App',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,96 @@
<template>
<div class="flex justify-between w-full border border-slate-300 p-1 flex-col md:flex-row hover:bg-slate-300 hover:border-slate-600 transition-all">
<div class="flex gap-1 items-center">
<div>
<div class="flex justify-center">
<div class="form-check form-switch">
<label class="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
value=""
class="sr-only peer"
checked
>
<div class="w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-2 peer-focus:ring-green-700 dark:peer-focus:ring-green-800 dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-green-600" />
</label>
</div>
</div>
</div>
<div>Название: askr_locations_speed_limit_calc</div>
<div>Описание: Поиск случаев превышения автомобилями скорости передвижения по автодорогам</div>
<div
id="calc_period_59"
current-period="3600000"
phx-hook="TimeConvert"
>
Период: кажды(й/е) час
</div>
</div>
<div class="flex gap-2">
<div>
<TaskHistory>
<template #button>
<i class="cursor-pointer ri-eye-line" />
</template>
</TaskHistory>
</div>
<div>
<TaskModal>
<template #button>
<i class="cursor-pointer ri-pencil-line" />
</template>
</TaskModal>
</div>
<div x-data="{ open: false }">
<i
x-show="!open"
class="cursor-pointer ri-delete-bin-line text-red-500"
@click="open = true"
/>
<div
class="flex my_margin-2 items-center"
x-show="open"
style="display: none;"
@click="open = false"
>
<span
phx-click="delete_period"
phx-value-id="59"
class="cursor-pointer p-0.5 border rounded text-red-600 border-red-600 mr-2"
>Удалить</span>
<span
class="cursor-pointer rounded p-0.5 border border-slate-600 text-slate-600"
@click="open = false"
>Отмена</span>
</div>
</div>
</div>
</div>
</template>
<script>
import TaskModal from './TaskModal.vue'
import TaskHistory from './TaskHistory.vue'
import ButtonModal from '@molecules/ButtonModal/index.vue'
export default {
name: 'Task',
components: {
TaskModal,
TaskHistory
},
props: {
index: {
default: 0,
type: Number
}
},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,47 @@
<template>
<ButtonModal
forcedBtnClass=" "
containerClass="pt-10 px-4 w-[80vw] 2xl:w-[70vw]"
:onToggle="onToggle"
>
<template #button>
<span>
<slot name="button" />
</span>
</template>
1234
</ButtonModal>
</template>
<script>
import ButtonModal from '@molecules/ButtonModal/index.vue'
import {data} from './historyData'
export default {
name: 'TaskHistory',
components: {
ButtonModal,
},
props: {
taskId: {
default: "",
type: String
}
},
data() {
return {
task: "",
task_description: "",
task_name: ""
}
},
computed: {
},
methods: {
onToggle: ({isOpen}) => {
console.log(isOpen)
}
}
}
</script>

View File

@@ -0,0 +1,256 @@
<template>
<ButtonModal
forcedBtnClass=" "
containerClass="pt-10 px-4 w-[80vw] 2xl:w-[70vw]"
:onToggle="onToggle"
>
<template #button>
<span>
<slot name="button" />
</span>
</template>
<form
id="period_modal_form"
phx-submit="form_validation"
class="flex flex-col gap-3 p-4 w-full min-h-[70vh]"
>
<div class="relative hidden">
<label
for="id"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300 text-left "
>
Период (мс)
</label>
<input
id="id"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full py-1 pl-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 "
placeholder="Период (мс)"
name="id"
type="text"
value=""
autocomplete="off"
>
</div>
<h2 class="font-bold text-slate-600 text-xl">
Описание
</h2>
<div class="flex justify-between items-center w-full gap-3">
<div class="min-w-[150px]">
<label
for="dt_start"
class="block font-w-700 text-sm text-gray-900 dark:text-gray-300"
>Дата начала</label>
<div
class="block mb-2 text-sm text-gray-500 dark:text-gray-300"
>
календарь
</div>
<Datepicker
:defaultDate="dtStart"
:enableTime="true"
:onchange="setDtStart"
/>
</div>
<div class="w-fit flex gap-2 items-end">
<div class="px-4">
<div class="font-semibold">
Период
</div>
<div class="flex w-[800px] gap-2">
<div class="relative ">
<label
for="month"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300 text-left "
>
месяц
</label>
<input
id="period_month"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full py-1 pl-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 "
placeholder=""
name="month"
type="number"
value=""
autocomplete="off"
>
</div><div class="relative ">
<label
for="day"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300 text-left "
>
день
</label>
<input
id="period_day"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full py-1 pl-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 "
placeholder=""
name="day"
type="number"
value=""
autocomplete="off"
>
</div><div class="relative ">
<label
for="hour"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300 text-left "
>
час
</label>
<input
id="period_hour"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full py-1 pl-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 "
placeholder=""
name="hour"
type="number"
value=""
autocomplete="off"
>
</div><div class="relative ">
<label
for="minute"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300 text-left "
>
минута
</label>
<input
id="period_minute"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full py-1 pl-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 "
placeholder=""
name="minute"
type="number"
value=""
autocomplete="off"
>
</div><div class="relative ">
<label
for="second"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300 text-left "
>
секунда
</label>
<input
id="period_second"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full py-1 pl-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 "
placeholder=""
name="second"
type="number"
value=""
autocomplete="off"
>
</div>
</div>
</div>
</div>
<div class="flex justify-start items-start flex-col mb-2 w-full col-span-4 w-fit pl-4">
<label
for="dt_start"
class="block mb-2 text-sm font-medium text-gray-900 text-left dark:text-gray-300 font-semibold"
>Задача включена</label>
<div class="block mb-2 text-sm text-gray-500 dark:text-gray-300">
переключить
</div>
<div class="form-check form-switch">
<label class="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
value=""
class="sr-only peer"
:checked="false"
>
<div class="w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-2 peer-focus:ring-green-700 dark:peer-focus:ring-green-800 dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full after:content-[''] after:absolute after:top-0.5 after:start-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 transition-all peer-checked:bg-green-600" />
</label>
</div>
</div>
</div>
<div class="relative ">
<label
for="name"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300 text-left "
>
Название задачи
</label>
<input
id="name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full py-1 pl-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 "
placeholder="Название задачи"
name="name"
type="text"
value=""
autocomplete="off"
>
</div>
<fwb-textarea
v-model="task_description"
:rows="4"
label="Описание задачи"
placeholder="Описание задачи"
class="w-full"
/>
<fwb-textarea
v-model="task"
:rows="14"
label="Тело задачи"
placeholder="тело задачи"
class="w-full"
/>
<button
v-if="taskId != 'new'"
id="btn_update_period"
type="submit"
class="cursor-pointer text-white bg-primary hover:brightness-90 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-ssm w-full px-5 py-1 text-center transition-all w-[150px]"
event="update_period"
from-id="period_modal_form"
phx-hook="SendFormData"
>
Обновить задачу
</button>
<button
v-if="taskId == 'new'"
id="btn_create_period"
type="submit"
class="cursor-pointer text-white bg-primary hover:brightness-90 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-ssm w-full px-5 py-1 text-center transition-all w-[150px]"
event="create_period"
from-id="period_modal_form"
phx-hook="SendFormData"
>
Добавить задачу
</button>
</form>
</ButtonModal>
</template>
<script>
import ButtonModal from '@molecules/ButtonModal/index.vue'
import Datepicker from "@molecules/Datepicker/index.vue"
import { FwbTextarea } from 'flowbite-vue'
export default {
name: 'TaskModal',
components: {
Datepicker,
ButtonModal,
FwbTextarea
},
props: {
taskId: {
default: "",
type: String
}
},
data() {
return {
task: "",
task_description: "",
task_name: ""
}
},
computed: {
},
methods: {
onToggle: ({isOpen}) => {
console.log(isOpen)
}
}
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,161 @@
<template>
<div class="flex flex-col gap-3 bg-light h-full w-full col-span-12 rounded-xl p-2">
<TaskModal taskId="new">
<template #button>
<button class="cursor-pointer bg-primary hover:bg-transparent text-white font-semibold hover:text-primary py-1 text-ssm px-4 border border-primary rounded-lg text-center transition_up w-[200px]">
Добавить задачу
</button>
</template>
</TaskModal>
<div
id="phx-F6ih6f3ULA2TYReS-4-0"
data-phx-component="4"
class="flex flex-col gap-2"
>
<div>
<button
type="button"
class="hidden"
phx-click="[[&quot;toggle&quot;,{&quot;display&quot;:null,&quot;ins&quot;:[[],[],[]],&quot;outs&quot;:[[],[],[]],&quot;time&quot;:200,&quot;to&quot;:&quot;#period_table_modal&quot;}]]"
/>
<div
id="period_table_modal"
class="fixed z-[10000] inset-0 hidden"
phx-remove="[[&quot;transition&quot;,{&quot;time&quot;:200,&quot;to&quot;:null,&quot;transition&quot;:[[&quot;fade-out&quot;],[],[]]}]]"
>
<!-- Modal container -->
<div class="h-screen flex items-center justify-center p-4">
<!-- Overlay -->
<div
class="absolute z-0 inset-0 bg-gray-500 opacity-75"
aria-hidden="true"
/>
<!-- Modal box -->
<div
class="relative max-h-full overflow-y-auto bg-white rounded-lg shadow-xl "
role="dialog"
aria-modal="true"
tabindex="0"
autofocus=""
phx-window-keydown="[[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#period_table_modal&quot;,&quot;transition&quot;:[[&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;],[&quot;opacity-0&quot;]]}],[&quot;dispatch&quot;,{&quot;event&quot;:&quot;click&quot;,&quot;to&quot;:&quot;#period_table_modal-return&quot;}]]"
phx-click-away="[[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#period_table_modal&quot;,&quot;transition&quot;:[[&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;],[&quot;opacity-0&quot;]]}],[&quot;dispatch&quot;,{&quot;event&quot;:&quot;click&quot;,&quot;to&quot;:&quot;#period_table_modal-return&quot;}]]"
phx-key="escape"
>
<button
type="button"
class="absolute top-2 right-2 text-gray-400 flex space-x-1 items-center"
aria_label="close modal"
phx-click="[[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#period_table_modal&quot;,&quot;transition&quot;:[[&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;],[&quot;opacity-0&quot;]]}],[&quot;dispatch&quot;,{&quot;event&quot;:&quot;click&quot;,&quot;to&quot;:&quot;#period_table_modal-return&quot;}]]"
>
<span class="text-sm">(esc)</span>
<i class="ri ri-close-line text-2xl" />
</button>
<div class="min-w-[350px] min-h-[400px] py">
<div class=" p-4 font-bold" />
<div
id="logs_loader"
style="display: none; width: 95vw; height: 95vh;"
class="w-full h-full"
>
<div
style=""
class="h-full absolute inset-0 flex flex-row items-center grow transition items-center justify-center appear_from_opacity-3 z-30 rounded p-2"
>
<div class="flex flex-row items-center grow transition items-center justify-center rounded shadow bg-white h-full">
<div
type-node="loader"
class="flex gap-3 items-center justify-center "
>
<span class="text-slate-600 text-md font-medium">Загрузка</span>
<div role="status">
<svg
aria-hidden="true"
class="mr-2 w-6 h-6 text-gray-300 animate-spin dark:text-gray-400 fill-slate-700"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
<div class="flex flex-col gap-2 px-4 pb-4">
<div class="flex gap-2 items-end">
<div class="relative ">
<label
for=""
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300 text-left "
>
Название
</label>
<input
id="period_view_name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full py-1 pl-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 "
placeholder="Название"
name=""
type="text"
value=""
disabled=""
autocomplete="off"
>
</div>
<div
id="display_modal_period"
phx-hook="TimeConvert"
/>
</div>
<div
id="period_report"
style="width: 95vw"
phx-hook="MakeTabulator"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<Task
v-for="_i in tasks"
:key="`${_i}_key`"
:index="_i"
/>
</div>
</div>
</template>
<script>
import { defineAsyncComponent } from 'vue'
import Task from './Task.vue'
import TaskModal from './TaskModal.vue'
export default {
name: 'Cron',
components: {
Task,
TaskModal,
},
data() {
},
computed: {
tasks () {
return [1,2,3,4]
}
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div>
123
</div>
</template>
<script>
export default {
name: 'App',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div>
123
</div>
</template>
<script>
export default {
name: 'App',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div>
123
</div>
</template>
<script>
export default {
name: 'App',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div>
123
</div>
</template>
<script>
export default {
name: 'App',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div>
123
</div>
</template>
<script>
export default {
name: 'App',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div>
123
</div>
</template>
<script>
export default {
name: 'App',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div>
123
</div>
</template>
<script>
export default {
name: 'App',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,162 @@
<template>
<div class="grid grid-cols-12 gap-6 col-span-12 h-full">
<form
phx-submit="fetch_pack"
class="rounded-md bg-light px-4 pt-4 pb-6 gap-2 shadow lg:col-span-4 xl:col-span-3 2xl:col-span-2 col-span-12"
autocomplete="off"
>
<h1 class="font-medium leading-tight text-xl mb-4 text-slate-600">
Последние пакеты
</h1>
<div class="grid gap-3 mb-3 ">
<div class="relative ">
<label
for="imei"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300 text-left "
>
imei
</label>
<input
id="selected_imei"
required=""
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full py-1 pl-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 "
placeholder="imei"
name="imei"
type="text"
autocomplete="off"
:value="imei"
@input="(e) => setImei(e.target.value)"
>
</div>
</div>
<div class="mb-3">
<label
for="dt_start"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>Дата начала</label>
<Datepicker
:defaultDate="dtStart"
:enableTime="true"
:onchange="setDtStart"
/>
</div>
<div class="mb-3">
<label
for="dt_finish"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>Дата окончания</label>
<Datepicker
:defaultDate="dtFinish"
:enableTime="true"
:onchange="setDtFinish"
/>
</div>
<div class="grid gap-3 grid-cols-4">
<button
type="button"
class="col-span-4 lg:col-span-4 cursor-pointer text-white bg-primary hover:brightness-90 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-ssm w-full sm:w-auto px-5 py-1 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 transition_up"
@click="uploadData"
>
Выгрузить
</button>
<MachinesModal />
<ButtonModal btnClass="cursor-pointer col-span-4 bg-transparent hover:bg-primary text-primary font-semibold hover:text-white py-1 text-ssm px-4 border border-primary hover:border-transparent rounded-lg text-center transition_up">
<template #button>
<div id="question">
<i class="ri-question-mark" />
</div>
</template>
<div class="overflow-auto min-w-[70vw] h-[70vh] p-4">
<div style="display: flex; flex-direction: column; gap: 1rem;">
<span style="font-size: 1.1em; font-weight: bold;">Страница предназначена для получения сведений о последних полученных пакетах от машин<br></span><span style="border-left: 5px solid rgba(0, 0, 0, 0.45); padding: 10px 5px;"><span style="font-weight: bold;">Работа со страницей</span><br><span>Поля выбора находятся на странице. Чтобы загразить данные необходимо -<ol style="font-size: 0.9em; margin-bottom: 0px;"><li>1. ввести пакеты через запятую</li><li>2. ввести imei или выбрать машину/устройство</li><li>3. нажать на кнопку 'загрузить' и дождаться загрузки</li><li style="font-size: 0.9em; color: gray;">(опционально) изменить даты</li></ol></span></span>
</div>
</div>
</ButtonModal>
</div>
</form>
<div class="lg:col-span-8 xl:col-span-9 2xl:col-span-10 col-span-12 bg-light shadow">
<div
v-if="pageState == 'await'"
id="report_packs"
class="tab-pane active relative text-center min-h-[300px] relative rounded-md "
>
<span> Выберите машину и нажмите выгрузить</span>
</div>
<div
v-if="pageState == 'loading'"
id="report_packs"
class="tab-pane active relative text-center min-h-[300px] relative rounded-md w-full h-full flex items-center justify-center"
>
<Spinner />
</div>
<div
v-if="pageState == 'isLoaded'"
id="report_packs"
class="tab-pane active relative text-center min-h-[300px] relative rounded-md "
>
<Tabulator v-bind="tabulatorOtps" />
</div>
</div>
</div>
</template>
<script>
import ButtonModal from '@molecules/ButtonModal/index.vue'
import Datepicker from "@molecules/Datepicker/index.vue"
import {mapGetters, mapMutations, mapActions} from 'vuex'
import Tabulator from "@molecules/Tabulator/index.vue"
import Spinner from "@molecules/Spinner/index.vue"
import MachinesModal from "@organisms/MachinesModal/index.vue"
export default {
name: 'LastPacks',
components: {
Datepicker,
ButtonModal,
Tabulator,
Spinner,
MachinesModal
},
data() {
return {
packNum: null,
}
},
computed: {
...mapGetters('last_packs', ['dtStart', 'dtFinish', 'imei', 'pageState', 'packsData']),
tabulatorOtps() {
return {
dataSource: this.packsData,
columns: [
{
title: "Нормер пакета",
field: "pack_number",
width: 150
},
{
title: "Количество записей",
field: "count",
width: 150
},
{
title: "Последняя дата пакета",
field: "pack_dt",
width: 150,
render: "{{ dt_format (dt_shift pack_dt 3 'hour') }}"
}
],
height: "550px"
}
}
},
methods: {
...mapMutations('last_packs', [
"setDtStart",
"setDtFinish",
"setImei"
]),
...mapActions('last_packs', ['uploadData'])
}
}
</script>

View File

@@ -0,0 +1,173 @@
<template>
<div class="grid grid-cols-12 gap-6 col-span-12 h-full">
<form
phx-submit="fetch_pack"
class="rounded-md bg-light px-4 pt-4 pb-6 gap-2 shadow lg:col-span-4 xl:col-span-3 2xl:col-span-2 col-span-12"
autocomplete="off"
>
<h1 class="font-medium leading-tight text-xl mb-4 text-slate-600">
Последние пакеты
</h1>
<div class="grid gap-3 mb-3 ">
<div class="relative ">
<label
for="imei"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300 text-left "
>
Номер пакета
</label>
<input
id="selected_imei"
required=""
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full py-1 pl-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 "
placeholder="Номер пакета"
name="imei"
type="text"
autocomplete="off"
:value="imei"
@input="(e) => setImei(e.target.value)"
>
</div>
</div>
<div class="grid gap-3 grid-cols-4">
<button
type="button"
class="col-span-4 lg:col-span-4 cursor-pointer text-white bg-primary hover:brightness-90 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-ssm w-full sm:w-auto px-5 py-1 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 transition_up"
@click="uploadData"
>
Выгрузить
</button>
<ButtonModal btnClass="cursor-pointer col-span-4 bg-transparent hover:bg-primary text-primary font-semibold hover:text-white py-1 text-ssm px-4 border border-primary hover:border-transparent rounded-lg text-center transition_up">
<template #button>
<div id="question">
<i class="ri-question-mark" />
</div>
</template>
<div class="overflow-auto min-w-[70vw] h-[70vh] pt-10 px-4">
<div style="display: flex; flex-direction: column; gap: 1rem;">
<span style="font-size: 1.1em; font-weight: bold;">Страница предназначена для получения сведений о последних полученных пакетах, в результате выдающая список машин, которые имели данный пакет за 3х месячный период (иначе запрос становится слишком долгим)<br></span><span style="border-left: 5px solid rgba(0, 0, 0, 0.45); padding: 10px 5px;"><span style="font-weight: bold;">Работа со страницей</span><br><span>Поля выбора находятся на странице. Чтобы загразить данные необходимо -<ol style="font-size: 0.9em; margin-bottom: 0px;"><li>1. ввести номер пакета</li><li>2. нажать на кнопку 'загрузить' и дождаться загрузки</li></ol></span></span>
</div>
</div>
</ButtonModal>
</div>
</form>
<div class="lg:col-span-8 xl:col-span-9 2xl:col-span-10 col-span-12 bg-light shadow">
<div
v-if="pageState == 'await'"
id="report_packs"
class="tab-pane active relative text-center min-h-[300px] relative rounded-md "
>
<span> Выберите машину и нажмите выгрузить</span>
</div>
<div
v-if="pageState == 'loading'"
id="report_packs"
class="tab-pane active relative text-center min-h-[300px] relative rounded-md w-full h-full flex items-center justify-center"
>
<Spinner />
</div>
<div
v-if="pageState == 'isLoaded'"
id="report_packs"
class="tab-pane active relative text-center min-h-[300px] relative rounded-md "
>
<Tabulator v-bind="tabulatorOtps" />
</div>
</div>
</div>
</template>
<script>
import ButtonModal from '@molecules/ButtonModal/index.vue'
import {mapGetters, mapMutations, mapActions} from 'vuex'
import Tabulator from "@molecules/Tabulator/index.vue"
import Spinner from "@molecules/Spinner/index.vue"
export default {
name: 'LastPacksNum',
components: {
ButtonModal,
Tabulator,
Spinner,
},
data() {
return {
packNum: null,
}
},
computed: {
...mapGetters('last_packs_num', ['packNum', 'pageState', 'packsData']),
tabulatorOtps() {
console.log(this.packsData)
return {
dataSource: this.packsData,
columns: [
{
title: "Нормер пакета",
field: "pack_number",
width: 150
},
{
title: "Последняя дата пакета",
field: "pack_dt",
width: 150,
render: "{{ dt_format (dt_shift pack_dt 3 'hour') }}"
},
{
title: "Тип/Модификация",
field: "machine_type",
width: 150
},
{
title: "Номер",
field: "zav_nomer",
width: 150
},
{
title: "id машины",
field: "machine_id",
width: 150
},
{
title: "id устройства",
field: "device_id",
width: 150
},
{
title: "IMEI",
field: "imei",
width: 150
},
{
title: "Дорога",
field: "railway_name",
width: 150
},
{
title: "Предприятие",
field: "org_name",
width: 150
},
{
title: "Номер 8 зн.",
field: "nomer_zn8",
width: 150
},
{
title: "Номер устройства",
field: "device_number",
width: 150
}
]
}
}
},
methods: {
...mapMutations('last_packs_num', [
"setPackNum"
]),
...mapActions('last_packs_num', ['uploadData'])
}
}
</script>

View File

@@ -0,0 +1,49 @@
<script>
import {mapGetters, mapActions, mapMutations, useStore} from "vuex"
import Button from "@/components/1_atoms/Button.vue"
import Tabulator from "@molecules/Tabulator/index.vue"
export default {
name: 'AddUsers',
components: {
Button,
Tabulator
},
setup() {
const store = useStore()
store.dispatch('add_users/uploadData')
},
data() {
return {
}
},
computed: {
...mapGetters('add_users', ['adminData']),
},
unmounted() {
const store = useStore()
store.dispatch('add_users/resetStore')
},
methods: {
}
}
</script>
<!-- eslint-disable -->
<template>
{{ console.log('adminData', adminData)
}}
<div class="rounded w-full pt-4 pb-1 bg-light cursor-pointer transition-all">
<div class="mx-2 mb-3">
<a :href="`/html/admin/manage/new?prev_page=/\?page_display_mode=users`">
<Button title="Создать" classBtn="cursor-pointer text-white bg-primary hover:brightness-90 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-ssm w-full px-5 py-1 text-center transition-all " />
</a>
</div>
<div id="admin_list" class="tab-pane m-4 active bg-light relative col-span-12 text-center min-h-[300px] rounded-md tabulator">
<Tabulator v-bind="adminData" />
</div>
</div>
</template>

View File

@@ -0,0 +1,105 @@
<script>
import {mapGetters, mapMutations, mapActions, useStore} from "vuex"
import Spinner from "@molecules/Spinner/index.vue"
import {PushAfterTimeout} from '@store/hooks/PushAfterTimeout'
import Accordion from '@atoms/Accordion.vue'
export default {
name: 'FinderPacks',
components: {
Spinner,
Accordion
},
setup() {
const store = useStore()
store.dispatch('finder_packs/uploadData')
},
data() {
return {
}
},
computed: {
...mapGetters('finder_packs', ['packsGroups', 'searchValue', 'isOpen']),
},
unmounted() {
const store = useStore();
store.dispatch('finder_packs/resetStore')
},
methods: {
...mapActions('finder_packs', ['updateSearch']),
updateSearchValue: function(e) {
const searchInput = new PushAfterTimeout('searchInput', 1500, 'right-[25px] top-[53%]')
if (searchInput.input) {
setTimeout(() => {
this.updateSearch(e.target.value)
}, 1500)
}
},
}
}
</script>
<!-- eslint-disable -->
<template>
<div class="rounded w-full transition-all min-h-[500px] relative">
<div v-if="!packsGroups" id="dataLoader" class="h-full absolute inset-0 flex flex-row items-center grow transition items-center justify-center bg-light appear_from_opacity-3 z-30 rounded p-2">
<Spinner style="padding: 0"/>
</div>
<div v-if="packsGroups" phx-change="update_pack_filter" class="p-5 flex flex-col gap-4 bg-light relative items-center">
<div id="searchInput" class="w-full flex flex-col gap-2" >
<div class="font-bold">Поиск (название и номер): </div>
<input v-bind:value="searchValue" v-on:change="updateSearchValue" :status="status" class="pl-2 py-1 rounded-r-md w-full max-h-[29px] text-sm border-none focus-visible:outline-blue-300" placeholder="Название и номер через пробел"/>
</div>
<div class="border-2 rounded-lg w-full bottom-0"></div>
</div>
<div v-if="packsGroups?.length === 0" id="no_data" class="w-full min-h-[80vh] bg-light dark:bg-slate-900 dark:text-slate-300 relative">
<div class="flex items-center justify-center text-lg font-bold pt-5">Пакеты не найдены</div>
</div>
<div class="bg-light">
<div v-if="packsGroups?.length > 0" class="grid grid-cols-12 gap-3 p-4">
<div :key="idx" v-for="(group, idx) in packsGroups" class="col-span-12 sm:col-span-6 lg:col-span-4">
<Accordion :isOpen="isOpen" :id="`pack_group_${group.pack_group_id}`">
<template #header>
<div v-if="group.order_index" id="packsInfoTitle" class="font-bold text-sm">
<span class="text-slate-500 font-bold">
{{ group.order_index }}.
</span>
<span class="font-bold">
{{ group.title }}
</span></div>
</template>
<template #body>
<div class="flex flex-col gap-2">
<table >
<thead>
<tr>
<th class="items-center justify-center pb-2">Название</th>
</tr>
</thead>
<tbody>
<tr v-for="struct in group.pack_structs" >
<td class="items-center justify-center py-2">
<div class="flex gap-2 w-full">
<div class="w-full text-left decoration-dashed cursor-pointer hover:text-slate-800 transition-all">
{{ struct.name }}
</div>
<a class="bg-slate-500 hover:bg-slate-700 text-white text-xs font-bold py-1 px-3 rounded transition-all h-fit" target="_blank" :href="`/html/packs/${group.link}?pack=${struct.name}`">
таблица
</a>
<a class="bg-slate-500 hover:bg-slate-700 text-white text-xs font-bold py-1 px-3 rounded transition-all h-fit" target="_blank" :href="`/admin_panel/packs/${group.pack_group_id}/${struct.id}`">карточка</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
</Accordion>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,129 @@
<!-- eslint-disable -->
<script>
import {ref, toRaw, computed} from 'vue'
import {mapGetters, mapMutations, mapActions, useStore} from "vuex";
import {useRoute, useRouter} from 'vue-router'
import Button from "@/components/1_atoms/Button.vue";
import DoubleSwitch from "@/components/1_atoms/DoubleSwitch.vue";
import Chart from "@/components/2_molecules/Chart/index.vue";
import ECharts from '@store/hooks/Echarts';
export default {
name: 'Charts',
components: {
Button,
Chart,
DoubleSwitch
},
setup() {
const route = useRoute()
const path = computed(() => route.path)
const query = computed(() => route.query)
const typeCharts = path.value === '/' && query?.value?.mode && query?.value?.type ? query?.value?.type : ''
const isChartTypesUrl = typeCharts === 'main' ? false : true
return {
isChartTypesUrl
}
},
data() {
return {
isChartTypes: this.isChartTypesUrl,
}
},
provide() {
return {
isChecked: computed(() => this.isChartTypes),
}
},
watch: {
activeChartData: {
handler(newValue, oldValue) {
console.log('newValue - activeChartData', newValue)
if (newValue.id && newValue.type && newValue.data) {
const chart = new ECharts(newValue.id)
const isSelectedLegend = this.legendFiltersParams.length > 0 && this.legendFiltersParams[0].id === newValue.id
console.log('this.legendFiltersParams', this.legendFiltersParams)
newValue.data.legendFilterData = isSelectedLegend ? this.legendFiltersParams : []
this.setTypeFunc(newValue.type, chart, newValue.data)
}
},
// deep: true
},
activeMenuChartData: {
handler(newValue, oldValue) {
// console.log('newValue - activeMenuChartData', newValue)
if (newValue.groupByInfo && newValue.groupByInfo.chartId) {
const chart = new ECharts(newValue.groupByInfo.chartId)
chart.setMenuChart(newValue)
if (newValue.groupByInfo.isDownloadChart) {
chart.downloadChart(newValue, this.updateCharts)
}
}
},
},
},
computed: {
...mapGetters('machines', ['selectedSelects', 'selectsData', 'toggleFilter', 'chartsData', 'activeChartData', 'prevGroupByData', 'activeMenuChartData', 'openMenuChart', 'legendFiltersParams']),
},
mounted () {
},
methods: {
...mapMutations('machines', ['setSelectedSelect', 'setToggleFilter']),
...mapActions('machines', ['updateSelects', 'updateCharts']),
updateSelectedSelects: function(value) {
if (value.length > 0) {
this.updateSelects({key: 'set', value: value})
}
},
removedOption: function(value) {
this.updateSelects({key: 'removed', value: value})
},
onToggleCharts: function(value) {
console.log('onToggleCharts', value)
const path = this.$route.path
const query = this.$route.query
const chartsType = value ? 'types' : 'main'
this.$router.push({ path: path, query: {...query, type: chartsType} })
this.isChartTypes = value
},
setTypeFunc: function(type, chart, data) {
switch(type) {
case 'pie': return chart.makePieCharts({...data, type: type})
case 'mount': return chart.makeBarMountCharts({...data, type: type, height: this.height})
case 'mainMl': return chart.makeBarCharts({...data, type: type})
case 'default': return chart.makeBarTypesCharts({...data, type: type})
case 'types': return chart.makeBarTypesCharts({...data, type: type})
case 'without_first_layout': return chart.makeBarTypesCharts({...data, type: type})
case 'ml': return chart.makeBarTypesCharts({...data, type: type})
}
}
}
}
</script>
<!-- eslint-disable -->
<template>
<div class="px-2 sm:px-5 pt-1 bg-light">
<div class="relative">
<div class="doubleSwitch flex justify-end mt-3 mb-2">
<DoubleSwitch name="chartsView" @switched="onToggleCharts" firstColor="#8b5cf6" secondColor="#2563eb" firstTitle="Общий вид" secondTitle="По типам" />
</div>
<div v-if="!isChartTypes" id="main" class="relative flex flex-wrap justify-center w-full h-full mb-3" >
<div v-for="(chart, idx) in chartsData.main">
<Chart :idx="idx" :id="chart.id" :type="chart.type" :height="chart.height" :maxHeight="chart.maxHeight" :data="chart.data"/>
</div>
</div>
<div v-if="isChartTypes" id="types" class="relative flex flex-wrap justify-center w-full h-full mb-3" >
<div v-for="(chart, idx) in chartsData.types">
<Chart :idx="idx" :id="chart.id" :type="chart.type" :height="chart.height" :maxHeight="chart.maxHeight" :data="chart.data"/>
</div>
</div>
</div>
</div>
</template>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -0,0 +1,170 @@
<!-- eslint-disable -->
<script>
import {mapGetters, mapMutations, mapActions, useStore} from "vuex"
import Modal from '@molecules/Modal/index.vue'
import Accordion from '@atoms/Accordion.vue'
import PackView from '@organisms/PackView/index.vue'
import Spinner from "@molecules/Spinner/index.vue"
import {getMachineInfo} from './helpers'
export default {
name: 'DataModal',
components: {
Modal,
Accordion,
PackView,
Spinner
},
props: {
open: {
type: Boolean,
default: false
},
close: {
type: Function,
default: () => {}
},
id: {
type: String,
default: ""
},
text: {
type: String,
default: ""
},
headerClass: {
type: String,
default: ""
},
containerClass: {
type: String,
default: ""
}
},
data () {
return {
machineMainInfo: null,
machineOverInfo: null
}
},
updated () {
this.$nextTick(function () {
if (this.data && this.id && this.open) {
// const newMap = new CoordsMap(this.id)
// newMap.loadMap(this.data.coords, this.data.machine)
console.log('this.data', this.data)
this.machineMainInfo = getMachineInfo(this.machineInfo.machine_info, true)
this.machineOverInfo = getMachineInfo(this.machineInfo.machine_info, false)
}
})
},
computed: {
...mapGetters('machines', ['machineInfo', 'reportPacks']),
},
methods: {
...mapActions('machines', ['loadPack']),
}
}
</script>
<!-- eslint-disable -->
<template>
<Modal
v-if="open"
:close="close"
:isOpen="true"
>
<div class="border-b p-4 font-bold">
Последние данные
</div>
{{ console.log('reportPacks', reportPacks)
}}
<div :id="id" class="w-[95vw] h-[85vh] m-auto p-5">
<div v-if="!machineInfo" id="dataLoader" class=" h-full flex justify-center items-center">
<Spinner style="padding: 0"/>
</div>
<div v-if="machineInfo" class="w-full flex flex-col gap-5">
<div class="w-full">
<Accordion id="machineInfo">
<template #header>
<div v-if="machineInfo?.machine_name" id="machineInfoTitle" class="font-bold text-xl">{{ machineInfo.machine_name }}</div>
</template>
<template #body>
<div v-if="machineInfo.machine_info?.length > 0" id="machineInfoBody" class="flex w-full justify-start 2xl:justify-between gap-3 flex-wrap">
<div class="flex p-4">
<div class="flex mr-8 flex-col bg-slate-200 p-3 rounded">
<h2 clas="font-bold flex items-center"><i class="ri-flashlight-line mr-1" /> Основная информация</h2>
<div class="mt-2 p-2 rounded-md flex flex-col bg-slate-300">
<div v-if="machineMainInfo?.length > 0" class="flex flex-col gap-2">
<div :key="idx" v-for="(info, idx) in machineMainInfo" class="flex items-center items-center bg-slate-200 px-3 py-2 rounded-md">
<div class="text-slate-700 font-bold mr-2">{{ info.title }}</div>
<div class="text-slate-500">{{ info.val }}</div>
</div>
</div>
</div>
</div>
<div style="min-width: 300px;" class="flex flex-col bg-slate-200 p-3 rounded">
<h2 clas="font-bold flex items-center"><i class="ri-git-pull-request-line mr-1"></i> Дополнительно</h2>
<div class="mt-2 p-2 rounded-md flex flex-col bg-slate-300">
<div v-if="machineOverInfo?.length > 0" class="flex flex-col gap-2">
<div :key="idx" v-for="(info, idx) in machineOverInfo" class="flex items-center items-center bg-slate-200 px-3 py-2 rounded-md">
<div class="text-slate-700 font-bold mr-2">{{ info.title }}</div>
<div class="text-slate-500">{{ info.val }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</Accordion>
</div>
<Accordion :isOpen="true" id="machine_packs">
<template #header>
<div class="font-bold text-xl">Пакеты</div>
</template>
<template #body>
<div class="flex flex-col gap-4">
<div id="packReportButtons">
<div class="flex flex-col gap-5">
<div class="grid grid-cols-12 gap-3">
<div v-if="reportPacks.length === 0" class="flex items-center justify-center text-slate-700 text-2xl font-bold col-span-12">
<h2>Нет данных по машине</h2>
</div>
<div :key="idx" v-if="reportPacks.length > 0" v-for="(pack, idx) in reportPacks" class="col-span-6 sm:col-span-4 lg:col-span-3 xl:col-span-2 flex items-start flex gap-1 justify-center rounded-md bg-slate-300">
<div class="flex-col flex-grow flex py-1">
<div class="flex text-slate-700 font-medium px-3 text-slate-700">
<div class="mr-1">Номер</div>
<div>{{ pack.pack_number }}</div>
</div>
<div class="text-slate-600 flex px-3 text-slate-600">
<div class="mr-1">Количество</div>
<div>{{ pack.count }}</div>
</div>
</div>
<div @click="loadPack(pack.pack_number)" class="h-full flex items-center justify-center text-white rounded-md px-3 text-center cursor-pointer transitiona-all bg-slate-400 hover:bg-slate-600">
<i class="ri-eye-line"></i>
</div>
</div>
</div>
</div>
</div>
<div class="w-full h-[1px] bg-slate-300"></div>
<PackView :isLoader="false" id="machineReport">
<template #description>
<span class="text-slate-400 text-sm">последние 30</span>
</template>
<template #error>
Слишком много пакетов для запроса
</template>
</PackView>
</div>
</template>
</Accordion>
</div>
</div>
</Modal>
</template>

View File

@@ -0,0 +1,28 @@
const getMachineInfo = (info, isMain) => {
const mainInfo = ["Текущая симкарта", "Последняя коммуникация", "Предприятие приписки", "Текущее устройство"]
if (isMain) {
return info.reduce((acc, el, idx) => {
if (mainInfo.includes(Object.keys(el)[0])) {
const item = {title: Object.keys(el)[0], val: Object.values(el)[0]}
acc.push(item)
} else {
acc
}
return acc
}, [])
}
if (!isMain) {
return info.reduce((acc, el, idx) => {
if (!mainInfo.includes(Object.keys(el)[0])) {
const item = {title: Object.keys(el)[0], val: Object.values(el)[0]}
acc.push(item)
} else {
acc
}
return acc
}, [])
}
}
export { getMachineInfo }

View File

@@ -0,0 +1,71 @@
<!-- eslint-disable -->
<script>
import {ref, toRaw, computed} from 'vue'
import {mapGetters, mapMutations, mapActions, useStore} from "vuex";
import Button from "@/components/1_atoms/Button.vue";
import VueMultiselect from 'vue-multiselect'
export default {
name: 'Filters',
components: {
Button,
VueMultiselect
},
setup() {
// const store = useStore()
// store.dispatch('main/updateSelects')
},
data() {
return {
isOpenOptions: false,
}
},
computed: {
...mapGetters('machines', ['selectedSelects', 'selectsData', 'toggleFilter', 'selectedData']),
},
mounted () {
},
methods: {
...mapMutations('machines', ['setSelectedSelect', 'setToggleFilter']),
...mapActions('machines', ['updateSelects']),
updateSelectedSelects: function(value) {
if (value.length > 0) {
this.updateSelects({key: 'set', value: value})
}
},
removedOption: function(value) {
this.updateSelects({key: 'removed', value: value})
},
}
}
</script>
<!-- eslint-disable -->
<template>
<div id="ignore:2Qxwqz" class="pb-1">
{{ console.log('selectedData' ,selectedData)
}}
<div v-if="toggleFilter" wrapper_class="grid grid-cols-12 gap-2" selects_class="col-span-12 sm:col-span-6 lg:col-span-3" class="grid grid-cols-12 gap-2 mb-3 border-t border-slate-300 pt-3" fields={@filters_fields} id="SelectFilters" phx-hook="SelectFilters">
<div v-for="selectData in selectsData" class="col-span-12 sm:col-span-6 lg:col-span-3">
<VueMultiselect
:multiple="true"
v-model="selectedSelects[selectData.key]"
label="name"
track-by="name"
:options="selectData.data"
:placeholder="selectData.title"
@update:model-value="updateSelectedSelects"
@remove="removedOption"
:showLabels="false"
>
</VueMultiselect>
</div>
</div>
</div>
</template>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -0,0 +1,62 @@
<script>
import {mapGetters, mapMutations, useStore} from "vuex"
import Tabulator from "@molecules/Tabulator/index.vue"
import { EventStore } from "@store/hooks/EventStore"
export default {
name: 'HistoryViews',
components: {Tabulator},
setup () {
const initLocalStorage = new EventStore()
const historyData = initLocalStorage.localStoreGetData({key: 'historyViews', onlyGet: true})
const isHistoryData = historyData && historyData.length > 0
// const updatedData = historyData.filter((el, idx, arr) => arr.findIndex((item) => item.machine_id === el.machine_id) === idx).slice(0, 50) // uniq values
const updatedData = isHistoryData ? historyData.slice(0, 50) : []
const store = useStore()
store.commit('machines/setHistoryData', updatedData)
},
computed: {
...mapGetters('machines', ['historyData']),
tabulatorOtps() {
return {
dataSource: this.historyData,
columns: [
{
title: "ID машины",
field: "machine_id",
width: 150
},
{
title: "Тип машины",
field: "machine_type",
width: 150
},
{
title: "Карточка машины",
field: "link",
log: true,
width: 180,
render: `<div class="flex items-center gap-2">
<a target="_blank" class="underline decoration-dashed" href="{{ link }}">Открыть карточку машины</i></a>
</div>`
},
{
title: "Время просмотра",
field: "created_at",
width: 150
}
],
height: "550px"
}
}
},
methods: {
...mapMutations('machines', ['setHistoryData']),
}
}
</script>
<template>
<Tabulator v-bind="tabulatorOtps" />
</template>

View File

@@ -0,0 +1,121 @@
<!-- eslint-disable -->
<script>
import {mapGetters, mapMutations, mapActions, useStore} from "vuex"
import Button from "@/components/1_atoms/Button.vue"
import { EventStore } from "@store/hooks/EventStore"
import moment from 'moment'
export default {
name: 'Machine',
components: {
Button,
},
props: {
machine: {
type: Object,
default: () => ({})
},
openMap: {
type: Function,
default: () => {}
},
openData: {
type: Function,
default: () => {}
}
},
data() {
return {
initLocalStorage: null,
}
},
computed: {
...mapGetters('machines', ['machinesData', 'historyData', 'historyMachines']),
},
mounted () {
},
methods: {
...mapMutations('machines', ['setInitHistory', 'setHistoryData', 'setHistoryMachines']),
...mapActions('machines', ['openMapModal', 'openDataModal']),
buttonClass: function(value) {
return `border border-slate-400 flex items-center justify-center cursor-pointer rounded transition-all text-xs text-center py-[5px] px-2 ${value}`
},
cardFunc: function(value) {
this.initLocalStorage = new EventStore()
if (this.machine.machine_id) {
const valueStorage = {machine_id: this.machine.machine_id, machine_type: this.machine.machine_type, link: `/html/askr_devices/analyze_device/${this.machine.device_number}`, created_at: moment().format('HH:mm:ss DD-MM-YY')}
return this.initLocalStorage.localStoreGetData({key: 'historyViews', newData: valueStorage}, this.updateLocalData)
}
},
updateLocalData: function(value) {
const historyData = value.data ? value.data : []
const mergedHistory = value.newData !== 'mount' ? [value.newData, ...historyData] : historyData
// const updatedHistory = mergedHistory.filter((el, idx, arr) => arr.findIndex((item) => item.machine_id === el.machine_id) === idx).slice(0, 50) // uniq values
const updatedHistory = mergedHistory.slice(0, 50)
const initHistory = (Array.isArray(value.newData) && value.newData.length > 0 || value.newData === 'mount') ? true : false
this.setInitHistory(initHistory)
this.setHistoryData(updatedHistory)
return this.initLocalStorage.localStoreSetData({key: 'historyViews', value: updatedHistory})
},
mapFunc: function(machine) {
// console.log('machine', machine)
this.openMapModal(machine)
return this.openMap()
},
dataFunc: function(machine) {
// console.log('machine', machine)
this.openDataModal(machine)
return this.openData()
},
}
}
</script>
<!-- eslint-disable -->
<template>
<div id="card" class="pt-3 border w-[23%] min-w-[310px] border-slate-300 rounded transition-all bg-slate-300}">
<div class="h-full flex flex-col gap-5 relative text-left justify-between">
<div class="flex flex-col items-end absolute text-white top-[-0.75rem] right-0 h-full min-w-[173px] p-2" style="backdrop-filter: blur(10px);">
<div class="mb-2 text-right drop-shadow" search-mode="machine_id">ID: {{ machine?.machine_id }}</div>
<div class="flex flex-row">
<span>{{ machine.railway_name }}</span><i class="ri-route-line ml-2" title="Дорога:"/>
</div>
<div class="flex flex-row">
<span>{{ machine.org_name }}</span><i class="ri-building-2-line ml-2" title="Компания:"/>
</div>
<div class="flex flex-row">
<span>{{ machine.nomer_zn8 }}</span><i class="ri-number-8 ml-2" title="8зн номер:"/>
</div>
<div class="flex flex-row" search-mode="device_number">
<span>{{ machine.device_number }}</span><i class="ri-rss-line ml-2" title="Устройство:"/>
</div>
</div>
<div class="flex flex-col w-full mt-4 mb-2 pl-4 mt-1 z-[1]">
<div class="text-slate-800 font-bold m-0" search-mode="machine_type">{{ machine.machine_type }}</div>
<div :machine_id="machine.machine_id" history_click="/html/askr_devices/analyze_comm/#{@machine?.imei}" class="text-slate-500 text-xs underline-offset-4" search-mode="imei">{{ machine.imei }}</div>
</div>
<div class="flex lg:flex-row flex-col gap-1 my-2 px-2 z-[1]">
<div class="flex">
<a :href="`/html/askr_devices/analyze_device/${machine.device_number}`" id="to_card_machine" no-init="false" @click="cardFunc(machine)" target="_blank" :class="buttonClass('bg-primary text-white')">Карточка</a>
</div>
<div class="flex gap-1">
<div @click="mapFunc(machine)" :class="buttonClass('bg-white/25 hover:bg-slate-400 hover:text-white')"><i class="ri-map-pin-line"/></div>
<div @click="dataFunc(machine)" :class="buttonClass('bg-white/25 hover:bg-slate-400 hover:text-white')">30</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
#card {
background: linear-gradient(96.95deg,rgba(236,236,243,.95) 1.45%,hsla(0,0%,100%,.703) 20.43%,hsla(0,0%,100%,0) 98.58%),linear-gradient(277.71deg,rgba(0,8,38,.3),rgba(0,5,20,.076) 45.46%,hsla(0,0%,100%,0) 97.87%),linear-gradient(0deg,rgba(123,134,152,.7),rgba(123,134,152,.7));
}
</style>

View File

@@ -0,0 +1,241 @@
<!-- eslint-disable -->
<script>
import {computed} from 'vue'
import {mapGetters, mapMutations, mapActions, useStore} from "vuex"
import {useRoute, useRouter} from 'vue-router'
import Button from "@/components/1_atoms/Button.vue"
import VueMultiselect from 'vue-multiselect'
import Filters from "./Filters.vue"
import Machine from "./Machine.vue"
import Charts from "./Charts.vue"
import HistoryViews from "./HistoryViews.vue"
import Table from "./Table.vue"
import MapModal from '@molecules/MapModal/index.vue'
import DataModal from "./DataModal/DataModal.vue"
import Spinner from "@molecules/Spinner/index.vue"
import {PushAfterTimeout} from '@store/hooks/PushAfterTimeout'
import {EventStore} from '@store/hooks/EventStore'
export default {
name: 'Machines',
components: {
Button,
Filters,
VueMultiselect,
Machine,
Table,
Charts,
HistoryViews,
MapModal,
DataModal,
Spinner
},
props: {
status: {
type: String,
default: 'static',
},
},
data () {
return {
openMap: false,
openData: false,
}
},
setup () {
const initLocalStorage = new EventStore()
const store = useStore()
const route = useRoute()
const path = computed(() => route.path)
const query = computed(() => route.query)
const historyData = initLocalStorage.localStoreGetData({key: 'historyViews', onlyGet: true})
const isHistoryData = historyData && historyData.length > 0
const updatedData = isHistoryData ? historyData.filter((el, idx, arr) => arr.findIndex((item) => item.machine_id === el.machine_id) === idx).slice(0, 20) : []
const urlParams = path.value === '/' && query?.value?.mode ? query.value.mode : ''
const externalParams = {historyData: updatedData, urlParams: urlParams}
store.commit('machines/setHistoryData', updatedData)
store.dispatch('machines/uploadData', externalParams)
},
computed: {
...mapGetters('machines', ['searchModes', 'searchValue', 'selectedSearchMode', 'leftTopButtons', 'rightTopButtons', 'selectedMode', 'toggleFilter', 'activeFilterBtn', 'mapData', 'machinesData']),
...mapGetters('layout', ['isOpenMenu']),
},
mounted () {
},
updated () {
this.$nextTick(function () {
const path = this.$route.path
const query = this.$route.query
if (path === '/' && query?.mode) {
this.setSelectedMode(query?.mode)
}
if (path === '/' && !query?.mode) {
this.setSelectedMode('cards')
}
})
},
unmounted() {
const store = useStore();
store.dispatch('machines/resetStore')
},
watch: {
isSuccessSearch: {
handler(newValue, oldValue) {
console.log('isSuccessSearch', newValue, oldValue)
},
deep: true
},
},
methods: {
...mapMutations('machines', ['setSearchMode', 'setSelectedMode', 'setToggleFilter', 'setActiveFilterBtn']),
...mapActions('machines', ['updateSearch', 'clearFilters']),
updateSearchMode: function(selectedSearchMode) {
this.setSearchMode(selectedSearchMode)
},
updateSearchValue: function(e) {
const searchInput = new PushAfterTimeout('searchInput', 1500)
if (searchInput.input) {
setTimeout(() => {
this.updateSearch(e.target.value)
}, 1500)
}
},
setFilteredAction (filteredAction) {
this.setActiveFilterBtn(filteredAction)
if (filteredAction === 'filter') {
this.setToggleFilter()
}
if (filteredAction ==='clearFilter') {
this.clearFilters()
}
},
updateMode: function(mode) {
if (mode) {
const path = this.$route.path
const params = mode === 'charts' ? {mode: mode, type: 'main'} : {mode: mode}
this.$router.push({ path: path, query: params })
return this.setSelectedMode(mode)
}
},
toggleMap () {
this.openMap = !this.openMap
},
toggleData () {
this.openData = !this.openData
},
closeMap () {
return this.openMap = !this.openMap
},
closeData () {
return this.openData = !this.openData
}
}
}
</script>
<!-- eslint-disable -->
<template>
<div class="px-2 sm:px-5 pt-1 bg-light">
<div class="relative">
<div class="grid grid-cols-12 gap-4 md:gap-3 lg:gap-4 pb-3">
<div class="flex items-center col-span-12 sm:col-span-8 lg:col-span-5 xl:col-span-4 2xl:col-span-3">
<div id="search" class="relative w-full max-w-[150px] h-[31px] pl-1 py-1 bg-white border border-primary rounded-l-md z-30">
<VueMultiselect
v-model="selectedSearchMode"
label="name"
:options="searchModes"
:searchable="false"
placeholder=""
:showLabels="false"
@update:model-value="updateSearchMode"
>
</VueMultiselect>
</div>
<div id="searchInput" class="relative w-full border-r border-t border-b border-primary rounded-r-md">
<input v-bind:value="searchValue" v-on:change="updateSearchValue" :status="status" class="pl-1 py-1 rounded-r-md w-full max-h-[29px] border-none focus-visible:outline-blue-300"/>
</div>
</div>
<!-- {{ console.log('machinesData', machinesData) }} -->
<ul id="machinesMode" class="flex col-span-6 sm:col-span-4 md:col-span-4 lg:col-span-3 xl:col-span-2 justify-center">
<li v-bind:key="idx" v-for="(mode, idx) in leftTopButtons">
<Button :onClick="updateMode" :selected="mode.key" :classIcon="mode.class" :classBtn="[mode.key === 'cards' ? 'rounded-r-none' : 'rounded-l-none', selectedMode === mode.key ? 'text-white bg-primary focus:ring-4 focus:outline-none focus:ring-blue-300' : 'bg-transparent', 'w-full min-w-[88px] h-[30px] border border-primary rounded-md']" />
</li>
</ul>
<div id="historyMode" class="flex col-span-6 sm:col-span-4 md:col-span-4 lg:col-span-3 xl:col-span-2 justify-center">
<Button :onClick="updateMode" selected="history" title="Просмотры" :classBtn="[selectedMode === 'history' ? 'focus:ring-4 focus:outline-none focus:ring-blue-300' : '', 'w-full min-w-[88px] h-[30px] bg-primary text-white border border-primary rounded-md']" />
</div>
<div id="chartsMode" class="flex col-span-6 sm:col-span-4 md:col-span-4 lg:col-span-3 xl:col-span-2 justify-center">
<Button :onClick="updateMode" selected="charts" classIcon="ri-pie-chart-2-line" :classBtn="[selectedMode === 'charts' ? 'focus:ring-4 focus:outline-none focus:ring-blue-300' : '', 'w-full min-w-[88px] h-[30px] bg-primary text-white border border-primary rounded-md']" />
</div>
<ul id="filtersButtons" class="flex col-span-6 sm:col-span-4 md:col-span-4 lg:col-span-3 xl:col-span-2 justify-center">
<li v-bind:key="idx" v-for="(mode, idx) in rightTopButtons">
<Button :onClick="setFilteredAction" :selected="mode.key" :title="mode.name" :classIcon="mode.iconClass" :classBtn="[activeFilterBtn === mode.key ? 'focus:ring-4 focus:outline-none focus:ring-blue-300' : '', mode.key === 'filter' ? 'rounded-r-none' : 'rounded-l-none', mode.classBtn, 'w-full min-w-[88px] h-[30px] border border-primary rounded-md']" />
</li>
</ul>
</div>
<div id="filters" v-if="toggleFilter">
<Filters />
</div>
<div class="border-2 rounded-lg w-full bottom-0"></div>
<div id="listMachines" v-if="selectedMode === 'cards'" class="w-full min-h-[80vh] p-4 bg-light dark:bg-slate-900 dark:text-slate-300 relative">
<ul v-if="selectedMode === 'cards'" class="flex flex-wrap gap-4 w-full justify-center align-center">
<!-- <li v-for="machine in machinesData" class="w-full sm:w-1/2 md:w-1/2 lg:w-1/2 xl:w-1/2 p-2 border-2 border-primary rounded-md"> -->
<li v-for="machine in machinesData" class="">
<Machine :machine="machine" :openMap="toggleMap" :openData="toggleData" />
</li>
</ul>
</div>
<div id="tableMachines" v-if="selectedMode === 'table'" class="tab-pane bg-light relative py-4 text-center min-h-[300px] relative rounded-md w-full">
<Table />
</div>
<div id="historyViews" v-if="selectedMode === 'history'" class="tab-pane bg-light relative shadow text-center min-h-[300px] relative w-full p-4">
<HistoryViews />
</div>
<div id="charts" v-if="selectedMode === 'charts'" class="w-full min-h-[80vh] pb-5 bg-light dark:bg-slate-900 dark:text-slate-300 relative">
<!-- <.loader :if={@display_config.is_loading_charts} /> -->
<div>
<Charts />
</div>
</div>
<!-- <div id="no_data" :if={@display_config.no_data && @display_config.machines_display_mode != "history_views"} class="w-full min-h-[80vh] bg-light dark:bg-slate-900 dark:text-slate-300 relative">
<.loader :if={@list == [] && !@display_config.no_data || @display_config.is_loading_list} />
<div class="flex items-center justify-center text-lg font-bold pt-5">Нет данных</div>
</div> -->
<MapModal
:open="openMap"
:close="closeMap"
:data="mapData"
id="mapModal"
headerClass="border-b p-4 font-bold"
>
<template #header>
<div id="headerTitle">
Карта
</div>
</template>
<div v-if="!mapData" id="map_loader" class="flex justify-center" style="width: 95vw; height: 82vh;">
<Spinner />
</div>
<div v-if="mapData" id="mapModal" style="width: 95vw; height: 82vh;"></div>
<div id="popup" class="ol-popup hidden transition-all">
<a href="#" id="popup-closer" class="ol-popup-closer hover:no-underline top-2 right-2 ">
<i class="ri-close-fill"></i>
</a>
<div id="popup-content"></div>
</div>
</MapModal>
<DataModal id="dataModal" :open="openData" :close="closeData" />
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,94 @@
<script>
import {mapGetters, mapActions, mapMutations, useStore} from "vuex"
import Tabulator from "@molecules/Tabulator/index.vue"
import { EventStore } from "@store/hooks/EventStore"
export default {
name: 'Table',
components: {Tabulator},
data () {
return {
}
},
computed: {
...mapGetters('machines', ['tableData', 'pagination']),
tabulatorOtps() {
return {
dataSource: this.tableData,
pagination: {...this.pagination, updatePagination: this.updatePagination},
columns:
[
{
title: "Тип и № машины",
field: "machine_type",
width: 150,
log: true,
render: `<div class="flex items-center gap-2">
<a target="_blank" class="underline decoration-dashed" href="/html/askr_devices/analyze_machine/{{ machine_id }}">{{machine_type}}</i></a>
</div>`
},
{
title: "Imei",
field: "imei",
width: 150,
log: true,
render: `<div class="flex items-center gap-2">
<a target="_blank" class="underline decoration-dashed" href="/html/askr_devices/analyze_comm/{{ imei }}">{{imei}}</i></a>
</div>`
},
{
title: "Дорога",
field: "railway_name",
width: 90
},
{
title: "Предприятие",
field: "org_name",
width: 150
},
{
title: "КР устройства",
field: "device_number",
width: 120,
log: true,
render: `<div class="flex items-center gap-2">
<a target="_blank" class="underline decoration-dashed" href="/html/askr_devices/analyze_device/{{ device_number }}">{{device_number}}</i></a>
</div>`
},
{
title: "Номер",
field: "nomer_zn8",
width: 90
},
{
title: "Заводской номер",
field: "zav_nomer",
width: 90
},
{
title: "ID машины",
field: "machine_id",
width: 90
},
{
title: "ID устройства",
field: "device_id",
width: 90
}
],
height: "550px"
}
}
},
methods: {
...mapMutations('machines', ['setHistoryData']),
...mapActions('machines', ['updatePagination']),
}
}
</script>
<template>
<Tabulator v-bind="tabulatorOtps" />
</template>

View File

@@ -0,0 +1,89 @@
<!-- eslint-disable -->
<script>
import {computed} from 'vue'
import {mapGetters, mapMutations, mapActions, useStore} from "vuex";
import {useRoute} from 'vue-router'
import Machines from "./Machines/Machines.vue";
import FinderPacks from "./FinderPacks/FinderPacks.vue";
import AddUsers from "./AddUsers/AddUsers.vue";
import Button from "@/components/1_atoms/Button.vue";
export default {
name: 'Main',
components: {
Machines,
FinderPacks,
AddUsers,
Button
},
setup() {
const store = useStore()
const route = useRoute()
const path = computed(() => route.path)
store.dispatch('main/uploadData', path.value)
store.commit('main/setPath', path.value)
},
data() {
return {
enabled: true,
dragging: false,
isTopScrolled: true,
containerOffset: 0
}
},
computed: {
...mapGetters('main', ['pages', 'activePath', 'selectedPage']),
...mapGetters('layout', ['isOpenMenu']),
},
mounted () {
},
unmounted() {
const store = useStore();
store.dispatch('main/resetStore')
},
updated() {
this.$nextTick(function () {
const path = this.$route.path
if (path !== this.activePath) {
const path = this.$route.path
this.$router.push({ path: path, query: {} })
this.setPath(path)
this.uploadData(path)
}
})
},
methods: {
...mapMutations('main', ['setPage', 'setPath']),
...mapActions('main', ['uploadData']),
setSelPage: function(page) {
console.log('page', page)
return this.setPage(page)
}
}
}
</script>
<!-- eslint-disable -->
<template>
<div class="col-span-12">
<div class="hidden" id="ignore:21QxOfq">
{{ console.log('selectedPage', selectedPage) }}
</div>
<div class="p-2 sm:px-5 pt-1 bg-light rounded-t-xl">
<div class="relative">
<div class="flex gap-3 sm:gap-7 w-full lg:w-[50%]">
<router-link :to="{name: page.link}" @click="setPage(page)" v-for="(page, idx) in pages" class="flex w-full">
<Button v-bind:key="idx" type="button" :title="page.name" :classBtn="[selectedPage.key !== page.key ? 'border-transparent': 'border-slate-500', 'flex border-b-4']" />
</router-link>
</div>
<div class="border-2 rounded-lg w-full bottom-0 absolute"/>
</div>
</div>
<router-view />
</div>
</template>

View File

@@ -0,0 +1,20 @@
<template>
<div>
123
</div>
</template>
<script>
export default {
name: 'App',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,65 @@
<!-- eslint-disable -->
<script>
import Chart from "@/components/2_molecules/Chart/index.vue";
import moment from 'moment'
export default {
name: 'PackChart',
components: {
Chart,
},
props: {
curerentTableData: {
type: Array,
default: () => []
},
displayView: {
type: String,
default: ""
},
},
data() {
return {
currentSizes: {
width: document.documentElement.clientWidth - 700,
height: document.documentElement.clientHeight - 350
},
maxWidth: document.documentElement.clientWidth - 300,
maxHeight: document.documentElement.clientHeight - 300,
}
},
computed: {
},
methods: {
chartData: function(params) {
console.log('params', params)
const yAxisAata = params.columns.map(column => {
const value = params.dataSource.map((el) => {
const dt = moment(el[column.pack_dt]).format('DD.MM.YYYY HH:mm')
return {
datetime: dt, param: column.key, state: el[column.key] ? el[column.key] : 0
}
})
return {key: column.key, name: column.title, value}
})
console.log('yAxisAata', yAxisAata)
const xAxisData = params.dataSource.map((el) => {
const dt = moment(el.pack_dt).format('DD.MM.YYYY HH:mm')
return dt
})
return {charts: yAxisAata, names: {yAxis: ' ', xAxis: 'Дата и время'}, xAxisData, title: 'График'}
},
}
}
</script>
<!-- eslint-disable -->
<template>
{{ console.log('currentSizes.heigth', maxHeight) }}
<div v-if="displayView === 'chart'" id="packChartWrapper" class="relative flex flex-wrap justify-center w-full h-full mb-3" >
<Chart id="packChart" type="packLines" height="800" :data="chartData(curerentTableData)" :sizes="currentSizes" :maxWidth="`${maxWidth}px`" :maxHeight="`${maxHeight}px`" />
</div>
</template>

View File

@@ -0,0 +1,558 @@
<script>
import {ref, toRaw, computed} from 'vue'
import {mapGetters, mapMutations, mapActions, useStore} from "vuex";
import {useRoute, useRouter} from 'vue-router'
import Datepicker from "@molecules/Datepicker/index.vue"
import Spinner from "@molecules/Spinner/index.vue"
import Tabulator from "@molecules/Tabulator/index.vue"
import PackChart from './Chart/Chart.vue'
import VueMultiselect from 'vue-multiselect'
import moment from 'moment'
export default {
name: 'Packs',
components: {
Datepicker,
VueMultiselect,
Tabulator,
PackChart,
Spinner
},
provide() {
return {
defaultDate: computed(() => this.setPickerFormat(this.packsSettings.dates.selectedDate)),
}
},
setup() {
const store = useStore()
const route = useRoute()
const path = computed(() => route.path)
const query = computed(() => route.query)
console.log('path', path)
console.log('query', query)
store.commit('packs/setIsLoading', true)
store.dispatch('packs/uploadData', {path: path.value, query: query.value})
},
data() {
return {
columnSelector: false,
displayView: 'table',
}
},
computed: {
...mapGetters('packs', ['selectedPacksGroup', 'selectedPack', 'tableData', 'packsSettings', 'columnsSelect', 'selectedColumnsSelect', 'isLoading']),
curerentTableData () {
const pagination = {...this.packsSettings.pagination, updatePagination: this.updatePagination}
return {
...this.tableData, pagination
}
},
},
unmounted() {
const store = useStore()
store.dispatch('packs/resetStore')
},
methods: {
...mapActions('packs', ['uploadData', 'updatePagination', 'updatePackSettings', 'updateSelects']),
...mapMutations('packs', ['setSelectedPack']),
updateSelectedPack: function(value) {
this.setSelectedPack(value)
},
setPackSettings: function(value, key) {
console.log('value', value)
console.log('key', key)
switch (key) {
case 'date':
this.updatePackSettings({dates: {...this.packsSettings.dates, selectedDate: value}})
break
case 'imei':
this.updatePackSettings({imei: value})
break
case 'selectedPack':
this.updatePackSettings({selectedPack: value})
break
}
},
setDate: function(value) {
console.log('value', value)
this.updatePackSettings({dates: {...this.packsSettings.dates, selectedDate: value}})
},
setImei: function(e) {
console.log('value', e.target.value)
this.updatePackSettings({imei: e.target.value})
},
setPack: function(value) {
console.log('value', value)
this.updatePackSettings({selectedPack: value})
},
fetchPack: function(params) {
console.log('params', params)
if (params.imei && params.dates.selectedDate && params.selectedPack) {
const path = this.$route.path
const query = this.$route.query
const dateUrl = moment(params.dates.selectedDate).format('YYYY-MM-DD')
console.log('params.selectedPack', params.selectedPack)
const queryUrl = {...query, date: dateUrl, imei: params.imei, pack: params.selectedPack.name}
this.$router.push({ path: path, query: queryUrl})
const currentQuery = {...queryUrl, date: params.dates.selectedDate}
this.uploadData({path: path, query: currentQuery})
}
},
setPickerFormat: (date) => {
console.log('moment(date)', moment(date).format('DD.MM.YYYY'))
if (!date) return null
return moment(date).format('DD.MM.YYYY')
},
setOpenColumnSelect: function() {
return this.columnSelector = !this.columnSelector
},
toggleDisplayView: function() {
this.displayView = this.displayView === 'chart' ? 'table' : 'chart'
},
addedOption: function(value) {
console.log('value', value)
this.updateSelects({key: 'added', select: value})
},
removedOption: function(value) {
this.updateSelects({key: 'removed', select: value})
},
}
}
</script>
<!-- eslint-disable -->
<template>
<div class="grid grid-cols-12 gap-6 col-span-12 h-full">
{{
// console.log('tableData', tableData)
console.log('packsSettings', packsSettings)
}}
<div class="rounded-md bg-light px-4 pt-4 pb-6 gap-2 shadow lg:col-span-4 xl:col-span-3 2xl:col-span-2 col-span-12">
<h1 class="font-medium leading-tight text-xl mb-4 text-slate-600">
Первый пакет
</h1>
<div class="grid gap-3 mb-3 ">
<div class="grid grid-cols-1">
<label for="date" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
Дата
</label>
<Datepicker id="date" :onchange="setDate" />
</div>
<div phx-feedback-for="imei">
<label for="selectedImei" class="block text-sm leading-6 text-zinc-800 dark:text-white">
imei
</label>
<input
id="selectedImei"
type="text"
name="imei"
v-bind:value="packsSettings.imei"
v-on:change="setImei"
class="block w-full rounded-lg border-zinc-300 py-[1px] px-[11px] text-zinc-900 focus:outline-none focus:ring-4 text-sm sm:leading-6 phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5 border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5 "
placeholder="imei"
required=""
>
</div>
<div>
<label for="selectedPack" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400"
>Выбрать пакет</label>
<div id="selected_settings_name_wrapper" class="w-full">
<div id="packsOptions" class="col-span-12 sm:col-span-6 lg:col-span-3 rounded-lg">
<VueMultiselect
name="selectedPack"
v-model="packsSettings.selectedPack"
label="name"
track-by="name"
:options="packsSettings.selectedPackGroup"
:placeholder="packsSettings.selectedPack?.name"
@update:model-value="setPack"
:showLabels="false"
>
</VueMultiselect>
</div>
</div>
</div>
</div>
<div class="grid gap-3 grid-cols-4">
<input
type="submit"
@click="fetchPack(packsSettings)"
class="col-span-4 lg:col-span-4 cursor-pointer text-white bg-primary hover:brightness-90 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-ssm w-full sm:w-auto px-5 py-1 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 transition_up"
value="Выгрузить"
>
<div
phx-click="load_pack_settings"
class="cursor-pointer col-span-4 xl:col-span-4 bg-transparent hover:bg-primary text-primary font-semibold hover:text-white py-1 text-ssm px-4 border border-primary hover:border-transparent rounded-lg text-center transition_up"
>
Настройки
</div>
<div
class="cursor-pointer col-span-4 lg:col-span-4 bg-transparent hover:bg-primary text-primary font-semibold hover:text-white py-1 text-ssm px-4 border border-primary hover:border-transparent rounded-lg text-center transition_up"
phx-click="[[&quot;show&quot;,{&quot;display&quot;:null,&quot;time&quot;:200,&quot;to&quot;:&quot;#select_machines_modal&quot;,&quot;transition&quot;:[[],[],[]]}],[&quot;show&quot;,{&quot;display&quot;:null,&quot;time&quot;:200,&quot;to&quot;:&quot;#machines_modal_loader&quot;,&quot;transition&quot;:[[],[],[]]}],[&quot;push&quot;,{&quot;event&quot;:&quot;load_machines_table&quot;,&quot;value&quot;:{&quot;mode&quot;:&quot;page&quot;}}]]"
>
Выбрать машину
</div>
<div
id="question"
class="cursor-pointer col-span-4 xl:col-span-4 bg-transparent hover:bg-primary text-primary font-semibold hover:text-white py-1 text-ssm px-4 border border-primary hover:border-transparent rounded-lg text-center transition_up"
phx-hook="InfoModal"
>
<i class="ri-question-mark" />
</div>
</div>
</div>
<div id="packageDisplay" class="relative lg:col-span-8 xl:col-span-9 2xl:col-span-10 col-span-12 bg-light shadow">
<div v-if="!isLoading && curerentTableData.dataSource" id="tableButtons" class="w-full">
<div class="w-full flex justify-end">
<div class="flex gap-3 pt-4 pb-2 pr-4">
<div class="">
<button
type="button"
@click="setOpenColumnSelect"
class="flex items-center transition-all justify-center py-2 px-3 bg-light text-sm font-medium text-slate-700 focus:outline-none rounded-lg border border-dashed border-slate-400 hover:text-blue-800 hover:border-blue-900 hover:shadow focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
>
<i class="ri-settings-3-line" />
</button>
</div>
<div
id="graphic_btn_inactive"
class=""
>
<button
type="button"
@click="toggleDisplayView"
class="flex items-center transition-all justify-center py-2 px-3 bg-light text-sm font-medium text-slate-700 focus:outline-none rounded-lg border border-dashed border-slate-400 hover:text-blue-800 hover:border-blue-900 hover:shadow focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
phx-click="[[&quot;toggle&quot;,{&quot;display&quot;:null,&quot;ins&quot;:[[],[],[]],&quot;outs&quot;:[[],[],[]],&quot;time&quot;:200,&quot;to&quot;:&quot;#graphic_btn_inactive&quot;}],[&quot;toggle&quot;,{&quot;display&quot;:null,&quot;ins&quot;:[[],[],[]],&quot;outs&quot;:[[],[],[]],&quot;time&quot;:200,&quot;to&quot;:&quot;#graphic_btn_active&quot;}],[&quot;toggle&quot;,{&quot;display&quot;:null,&quot;ins&quot;:[[],[],[]],&quot;outs&quot;:[[],[],[]],&quot;time&quot;:200,&quot;to&quot;:&quot;#pack_table_warapper&quot;}],[&quot;toggle&quot;,{&quot;display&quot;:null,&quot;ins&quot;:[[],[],[]],&quot;outs&quot;:[[],[],[]],&quot;time&quot;:200,&quot;to&quot;:&quot;#pack_graphic_wrapper&quot;}],[&quot;push&quot;,{&quot;event&quot;:&quot;toggle_display_data&quot;}]]"
>
<i class="ri-bar-chart-grouped-line" />
</button>
</div>
<div
id="graphic_btn_active"
class="hidden"
>
<button
type="button"
class="flex items-center transition-all justify-center py-2 px-3 bg-blue-200 text-sm font-medium text-blue-700 focus:outline-none rounded-lg border border-dashed border-blue-500 hover:text-blue-800 hover:border-blue-900 hover:shadow focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
phx-click="[[&quot;toggle&quot;,{&quot;display&quot;:null,&quot;ins&quot;:[[],[],[]],&quot;outs&quot;:[[],[],[]],&quot;time&quot;:200,&quot;to&quot;:&quot;#graphic_btn_inactive&quot;}],[&quot;toggle&quot;,{&quot;display&quot;:null,&quot;ins&quot;:[[],[],[]],&quot;outs&quot;:[[],[],[]],&quot;time&quot;:200,&quot;to&quot;:&quot;#graphic_btn_active&quot;}],[&quot;toggle&quot;,{&quot;display&quot;:null,&quot;ins&quot;:[[],[],[]],&quot;outs&quot;:[[],[],[]],&quot;time&quot;:200,&quot;to&quot;:&quot;#pack_table_warapper&quot;}],[&quot;toggle&quot;,{&quot;display&quot;:null,&quot;ins&quot;:[[],[],[]],&quot;outs&quot;:[[],[],[]],&quot;time&quot;:200,&quot;to&quot;:&quot;#pack_graphic_wrapper&quot;}],[&quot;push&quot;,{&quot;event&quot;:&quot;toggle_display_data&quot;}]]"
>
<i class="ri-bar-chart-grouped-line" />
</button>
</div>
<div
id="excel_btn"
phx-click="download_csv"
style=""
class="flex items-center justify-center transition-all justify-center py-2 px-3 bg-light text-sm font-medium text-slate-700 focus:outline-none rounded-lg border border-dashed border-slate-400 hover:text-blue-800 hover:border-blue-900 hover:shadow focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 cursor-pointer"
>
<i class="ri-file-excel-line" />
</div>
<div
id="excel_loader"
style="display: none"
class="flex items-center justify-center transition-all justify-center py-2 px-3 bg-light text-sm font-medium text-slate-700 focus:outline-none rounded-lg border border-dashed border-slate-400 hover:text-blue-800 hover:border-blue-900 hover:shadow focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 "
>
<svg
aria-hidden="true"
class="w-4 h-4 text-gray-300 animate-spin dark:text-gray-400 fill-slate-700"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
</div>
</div>
</div>
<div v-if="columnSelector" id="colums_select_wrapper" class="w-full">
<VueMultiselect
name="selectedPack"
v-model="selectedColumnsSelect"
label="name"
track-by="name"
:options="columnsSelect"
:placeholder="null"
@select="addedOption"
@remove="removedOption"
:showLabels="false"
:multiple="true"
:group-select="true"
>
</VueMultiselect>
</div>
<div v-if="displayView === 'table'" id="packTableWarapper">
{{ console.log('getters.isLoading', isLoading) }}
<div id="reportPacks" class="tab-pane active text-center min-h-[300px] rounded-md ">
<Tabulator v-if="!isLoading && curerentTableData.dataSource" v-bind="curerentTableData" />
<span v-if="!isLoading && !curerentTableData.dataSource" class="absolute flex w-full justify-center items-center mt-2"> Выберите пакет и нажмите выгрузить</span>
<div v-if="isLoading && !curerentTableData.dataSource" class="absolute flex w-full h-full justify-center items-center bg-slate-50" id="packLoader">
<Spinner />
</div>
</div>
</div>
<PackChart :displayView="displayView" :curerentTableData="curerentTableData" />
</div>
<div>
<button
type="button"
class="hidden"
phx-click="[[&quot;toggle&quot;,{&quot;display&quot;:null,&quot;ins&quot;:[[],[],[]],&quot;outs&quot;:[[],[],[]],&quot;time&quot;:200,&quot;to&quot;:&quot;#period_table_modal&quot;}]]"
/>
<div
id="period_table_modal"
class="fixed z-[10000] inset-0 hidden"
phx-remove="[[&quot;transition&quot;,{&quot;time&quot;:200,&quot;to&quot;:null,&quot;transition&quot;:[[&quot;fade-out&quot;],[],[]]}]]"
>
<!-- Modal container -->
<div class="h-screen flex items-center justify-center p-4">
<!-- Overlay -->
<div
class="absolute z-0 inset-0 bg-gray-500 opacity-75"
aria-hidden="true"
/>
<!-- Modal box -->
<div
class="relative max-h-full overflow-y-auto bg-white rounded-lg shadow-xl "
role="dialog"
aria-modal="true"
tabindex="0"
autofocus=""
phx-window-keydown="[[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#period_table_modal&quot;,&quot;transition&quot;:[[&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;],[&quot;opacity-0&quot;]]}],[&quot;dispatch&quot;,{&quot;event&quot;:&quot;click&quot;,&quot;to&quot;:&quot;#period_table_modal-return&quot;}]]"
phx-key="escape"
>
<button
type="button"
class="absolute top-2 right-2 text-gray-400 flex space-x-1 items-center"
aria_label="close modal"
phx-click="[[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#period_table_modal&quot;,&quot;transition&quot;:[[&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;],[&quot;opacity-0&quot;]]}],[&quot;dispatch&quot;,{&quot;event&quot;:&quot;click&quot;,&quot;to&quot;:&quot;#period_table_modal-return&quot;}]]"
>
<span class="text-sm">(esc)</span>
<i class="ri ri-close-line text-2xl" />
</button>
<div class="min-w-[350px] min-h-[400px] py">
<div class=" p-4 font-bold" />
<div
id="pack-settings-editor"
phx-hook="PackSettingsEditor"
/>
</div>
</div>
</div>
</div>
</div>
<div
id="ignore:31QzedwP"
phx-update="ignore"
>
<div>
<button
type="button"
class="hidden"
phx-click="[[&quot;toggle&quot;,{&quot;display&quot;:null,&quot;ins&quot;:[[],[],[]],&quot;outs&quot;:[[],[],[]],&quot;time&quot;:200,&quot;to&quot;:&quot;#select_machines_modal&quot;}]]"
/>
<div
id="select_machines_modal"
class="fixed z-[10000] inset-0 hidden"
phx-remove="[[&quot;transition&quot;,{&quot;time&quot;:200,&quot;to&quot;:null,&quot;transition&quot;:[[&quot;fade-out&quot;],[],[]]}]]"
>
<!-- Modal container -->
<div class="h-screen flex items-center justify-center p-4">
<!-- Overlay -->
<div
class="absolute z-0 inset-0 bg-gray-500 opacity-75"
aria-hidden="true"
/>
<!-- Modal box -->
<div
class="relative max-h-full overflow-y-auto bg-white rounded-lg shadow-xl "
role="dialog"
aria-modal="true"
tabindex="0"
autofocus=""
phx-window-keydown="[[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#select_machines_modal&quot;,&quot;transition&quot;:[[&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;],[&quot;opacity-0&quot;]]}],[&quot;dispatch&quot;,{&quot;event&quot;:&quot;click&quot;,&quot;to&quot;:&quot;#select_machines_modal-return&quot;}]]"
phx-key="escape"
>
<button
type="button"
class="absolute top-2 right-2 text-gray-400 flex space-x-1 items-center"
aria_label="close modal"
phx-click="[[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#select_machines_modal&quot;,&quot;transition&quot;:[[&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;],[&quot;opacity-0&quot;]]}],[&quot;dispatch&quot;,{&quot;event&quot;:&quot;click&quot;,&quot;to&quot;:&quot;#select_machines_modal-return&quot;}]]"
>
<span class="text-sm">(esc)</span>
<i class="ri ri-close-line text-2xl" />
</button>
<div class="min-w-[350px] min-h-[400px] py">
<div class=" p-4 font-bold" />
<div class="w-[95vw] h-[95vh] flex flex-col gap-8 p-5">
<div id="machines_modal_loader">
<div
style=""
class="h-full absolute inset-0 flex flex-row items-center grow transition items-center justify-center appear_from_opacity-3 z-30 rounded p-2"
>
<div class="flex flex-row items-center grow transition items-center justify-center rounded shadow bg-white h-full">
<div
type-node="loader"
class="flex gap-3 items-center justify-center "
>
<span class="text-slate-600 text-md font-medium">Загрузка</span>
<div role="status">
<svg
aria-hidden="true"
class="mr-2 w-6 h-6 text-gray-300 animate-spin dark:text-gray-400 fill-slate-700"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
<div
id="select_machines"
no-init="true"
phx-hook="MakeTabulator"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<button
type="button"
class="hidden"
phx-click="[[&quot;toggle&quot;,{&quot;display&quot;:null,&quot;ins&quot;:[[],[],[]],&quot;outs&quot;:[[],[],[]],&quot;time&quot;:200,&quot;to&quot;:&quot;#map_modal&quot;}]]"
/>
<div
id="map_modal"
class="fixed z-[10000] inset-0 hidden"
phx-remove="[[&quot;transition&quot;,{&quot;time&quot;:200,&quot;to&quot;:null,&quot;transition&quot;:[[&quot;fade-out&quot;],[],[]]}]]"
>
<!-- Modal container -->
<div class="h-screen flex items-center justify-center p-4">
<!-- Overlay -->
<div
class="absolute z-0 inset-0 bg-gray-500 opacity-75"
aria-hidden="true"
/>
<!-- Modal box -->
<div
class="relative max-h-full overflow-y-auto bg-white rounded-lg shadow-xl "
role="dialog"
aria-modal="true"
tabindex="0"
autofocus=""
phx-window-keydown="[[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#map_modal&quot;,&quot;transition&quot;:[[&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;],[&quot;opacity-0&quot;]]}],[&quot;dispatch&quot;,{&quot;event&quot;:&quot;click&quot;,&quot;to&quot;:&quot;#map_modal-return&quot;}]]"
phx-click-away="[[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#map_modal&quot;,&quot;transition&quot;:[[&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;],[&quot;opacity-0&quot;]]}],[&quot;dispatch&quot;,{&quot;event&quot;:&quot;click&quot;,&quot;to&quot;:&quot;#map_modal-return&quot;}]]"
phx-key="escape"
>
<button
type="button"
class="absolute top-2 right-2 text-gray-400 flex space-x-1 items-center"
aria_label="close modal"
phx-click="[[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#map_modal&quot;,&quot;transition&quot;:[[&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;],[&quot;opacity-0&quot;]]}],[&quot;dispatch&quot;,{&quot;event&quot;:&quot;click&quot;,&quot;to&quot;:&quot;#map_modal-return&quot;}]]"
>
<span class="text-sm">(esc)</span>
<i class="ri ri-close-line text-2xl" />
</button>
<div class="min-w-[350px] min-h-[400px] py">
<div class="border-b p-4 font-bold">
Карта
</div>
<div
id="ignore:7"
phx-update="ignore"
class="w-[95vw] h-[85vh] m-auto relative"
>
<div id="map_loader">
<div
style="padding: 0"
class="h-full absolute inset-0 flex flex-row items-center grow transition items-center justify-center appear_from_opacity-3 z-30 rounded p-2"
>
<div class="flex flex-row items-center grow transition items-center justify-center rounded shadow bg-white h-full">
<div
type-node="loader"
class="flex gap-3 items-center justify-center "
>
<span class="text-slate-600 text-md font-medium">Загрузка</span>
<div role="status">
<svg
aria-hidden="true"
class="mr-2 w-6 h-6 text-gray-300 animate-spin dark:text-gray-400 fill-slate-700"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
<div
id="map_show"
phx-hook="CoordsMap"
style="width: 95vw; height: 82vh;"
/>
<div
id="popup"
class="ol-popup hidden transition-all"
>
<a
id="popup-closer"
href="#"
class="ol-popup-closer hover:no-underline top-2 right-2 "
>
<i class="ri-close-fill" />
</a>
<div id="popup-content" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -0,0 +1,20 @@
<template>
<div>
123
</div>
</template>
<script>
export default {
name: 'App',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div>
123
</div>
</template>
<script>
export default {
name: 'App',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div>
123
</div>
</template>
<script>
export default {
name: 'App',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div>
123
</div>
</template>
<script>
export default {
name: 'App',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,105 @@
<!-- eslint-disable -->
<script>
import {mapActions, useStore} from "vuex"
import Spinner from "@molecules/Spinner/index.vue"
import { CopyToClipboard } from "@store/hooks/CopyToClipboard"
export default {
name: 'Service',
components: {
Spinner,
},
props: {
idx: {
type: Number,
default: 0
},
name: {
type: String,
default: ""
},
service: {
type: Object,
default: () => {}
},
copyed: {
type: String,
default: "copyed"
},
},
data () {
return {
isCopyed: false
}
},
setup (props, {slots}) {
const store = useStore()
store.dispatch('logger/uploadData', {status: 'info', text: 'Сервис Info'})
},
methods: {
...mapActions('services', ['uploadServiceData']),
isInitParams: function(service) {
if (service?.init_params && typeof service?.init_params === 'object') {
if (service?.init_params?.service && service?.init_params?.machine_addr) return true
}
},
copyText: function (e) {
const id = e.target.getAttribute('id')
const text = e.target.getAttribute('copyOnClickText')
this.isCopyed = true
return new CopyToClipboard('serviceList', text)
}
}
}
</script>
<!-- eslint-disable -->
<template>
<div :key="idx" style="min-width: 240px" class="h-full overflow-hidden shadow-lg rounded-lg h-auto w-80 my-2 md:w-80 p-4 bg-indigo-50 transition duration-200 ease-in-out transform hover:translate-y-1 hover:shadow-2xl">
<div class="w-full flex flex-col h-full justify-center">
<div class="flex my-2 justify-between">
<span class="font-bold"> {{ service?.title }} </span>
<span v-if="service?.is_online || name === 'глобальное хранилище'" class="font-bold text-green-700">доступен</span>
<span v-else class="font-bold text-red-800">отключен </span>
</div>
<hr>
<div class="text-gray-700 my-3 flex items-center flex-grow">
<div v-if="service?.serviceIp || service?.serviceCode" class="flex flex-col gap-4">
<div class="flex flex-col">
<span class="italic text-xs font-bold">Детали</span>
<span v-if="service?.serviceIp" class="italic text-xs">{{ service?.serviceIp }}</span>
<span v-if="service?.serviceCode" class="italic text-xs">{{ service?.serviceCode }}</span>
</div>
<div class="flex flex-col">
<span class="italic text-xs font-bold">Последний перезапуск(UTC):</span>
<span class="italic text-xs">{{ service?.dateStart }}</span>
</div>
<div class="flex flex-col">
<span class="italic text-xs font-bold">Описание сервиса:</span>
<span class="italic text-xs">{{ service?.description || "... нет описания" }}</span>
</div>
</div>
<div v-else class="flex flex-col">
<span class="italic text-xs font-bold">Описание сервиса:</span>
<span class="italic text-xs">Сервис глобального хранилища применяется для условно постоянного хранения данных в кэше</span>
</div>
</div>
<hr>
<div class="mt-2">
<span v-if="isInitParams(service)" class="flex justify-between" >
<span @click="copyText" :copyOnClickText="`${service?.init_params?.service}:${service?.init_params?.machine_addr}`" :copyed="`${isCopyed ? copyed : ''}`" class="copyText italic text-xs flex cursor-pointer" :id="`service_${service?.init_params?.service}`">
<i class="ri-file-copy-2-line text-slate-600 mr-1"></i>
{{ service?.serviceLink }}
</span>
<a :href="`/admin_panel/auth_service?show_service_cache=${name}`" id="selectService" @click="uploadServiceData(service)" class="flex italic text-xs cursor-pointer underline hover:text-gray-500 linear-loader">
открыть
</a>
</span>
<div v-else class="italic text-xs">нет доступа к глоб. хранилищу</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,74 @@
import Services from './index.vue'
import { globalServices } from '@store/modules/services/StaticData'
import { shallowMount, mount } from '@vue/test-utils'
import {createStore} from 'vuex'
const store = createStore({
state() {
return {
services: globalServices,
}
},
getters: {
services: (state) => state.services,
},
mutations: {
setServices: (state, updval) => state.services = updval,
},
})
describe("Services", () => {
it("renders Services, with custom button title", () => {
const wrapper = mount(Services, {
global: {
plugins: [store],
},
data () {
return {
titleServicesButton: 'Показать больше сервисов',
}
},
})
expect(wrapper.text()).toContain('Показать больше сервисов')
})
})
describe("Services", () => {
it("renders after click button, open full Services", async () => {
const wrapper = mount(Services, {
global: {
plugins: [store],
},
})
await wrapper.find('button').trigger('click')
expect(wrapper.find('button').text()).toContain('Скрыть')
})
})
describe("Services", () => {
it("renders Services with exists data", async () => {
const wrapper = mount(Services, {
global: {
plugins: [store],
},
})
expect(store.state.services.length).toBeGreaterThan(0)
})
})
describe("Services", () => {
it("renders Services maked empty data", async () => {
const wrapper = mount(Services, {
global: {
plugins: [store],
},
})
store.commit('setServices', [])
expect(store.state.services.length).toBe(0)
})
})

View File

@@ -0,0 +1,67 @@
<!-- eslint-disable -->
<script>
import {mapGetters, useStore} from "vuex"
import Service from './Service.vue'
import Spinner from "@molecules/Spinner/index.vue"
import Tabulator from "@molecules/Tabulator/index.vue"
export default {
name: 'Services',
components: {
Service,
Spinner,
Tabulator,
},
props: {
},
data () {
return {
isOpenServices: false,
titleServicesButton: 'Показать больше'
}
},
setup (props, {slots}) {
const store = useStore()
store.dispatch('services/uploadData')
},
computed: {
...mapGetters('services', ['services']),
},
methods: {
sortingServices: function (services) {
return services?.sort((a, b) => b.is_online - a.is_online)
},
toggleServices: function (isOpenServices) {
this.isOpenServices = !isOpenServices
this.titleServicesButton = this.isOpenServices ? 'Скрыть' : 'Показать больше'
}
}
}
</script>
<!-- eslint-disable -->
<template>
<div class="col-span-12">
<div class="flex flex-col mt-4 shadow-md mb-1 mx-5 bg-white" style="max-width: calc(100vw - 40px); margin-bottom: 30px;">
<div class="flex flex-col flex-wrap justify-between px-4 pb-2 pt-8 w-full">
<div class="text-lg font-bold text-center text-gray-600">Информация о сети (кэш)</div>
<div class="flex flex-col max-w-full">
<div>
Сервисы, записывающие данные в кэш
</div>
<div id="serviceList" :class="`flex flex-wrap h-auto flex-row mt-4 overflow-x-auto w-full gap-4 transition-all pb-4 ${isOpenServices ? 'max-h-full' : 'max-h-[220px]'}`">
<div v-for="(service, idx) in sortingServices(services)">
<Service :idx="idx" :name="service.service || service.name" :service="service" />
</div>
</div>
</div>
</div>
<button @click="toggleServices(isOpenServices)" class="w-full max-h-[90px] py-2.5 px-5 text-sm font-medium text-gray-900 bg-gray-100 border-t border-gray-200 hover:bg-gray-100 hover:text-slate-700 dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
{{titleServicesButton}}
</button>
</div>
</div>
</template>

View File

@@ -0,0 +1,122 @@
import Service from './Service.vue'
import { globalServices } from '@store/modules/services/StaticData'
import { shallowMount, mount } from '@vue/test-utils'
import {createStore} from 'vuex'
const store = createStore({
state() {
return {
services: globalServices,
}
},
getters: {
services: (state) => state.services,
},
mutations: {
setServices: (state, updval) => state.services = updval,
},
})
describe("Service", () => {
it("renders Service, with set props value", async () => {
const wrapper = mount(Service, {
global: {
plugins: [store],
},
props: {
idx: 1,
},
})
await wrapper.setProps({ idx: 3 })
expect(wrapper.html()).toContain(3)
})
})
describe("Service", () => {
it("renders Service with condition success", async () => {
const wrapper = mount(Service, {
global: {
plugins: [store],
},
props: {
name: '',
},
})
await wrapper.setProps({ name: 'глобальное хранилище' })
expect(wrapper.html()).toContain('доступен')
})
})
describe("Service", () => {
it("renders Service with condition not success", async () => {
const wrapper = mount(Service, {
global: {
plugins: [store],
},
props: {
name: '',
},
})
await wrapper.setProps({ name: 'Test service name' })
expect(wrapper.html()).toContain('отключен')
})
})
describe("Service", () => {
it("renders Service if not service details", async () => {
const wrapper = mount(Service, {
global: {
plugins: [store],
},
props: {
service: {},
},
})
expect(wrapper.html()).toContain('Сервис глобального хранилища применяется для условно постоянного хранения данных в кэше')
})
})
describe("Service", () => {
it("renders Service with set service details", async () => {
const wrapper = mount(Service, {
global: {
plugins: [store],
},
props: {
service: {serviceIp: '192.168.1.1', serviceCode: '1234567890', serviceLink: 'http://test.com', is_online: false,
},
},
})
await wrapper.setProps({ service: {serviceIp: '192.168.1.7', serviceCode: '1234567897', is_online: true} }) // 1 раз, далее новый тест
expect(wrapper.html()).toContain('192.168.1.7')
expect(wrapper.html()).toContain('1234567897')
expect(wrapper.html()).toContain('доступен')
expect(wrapper.html()).toContain('... нет описания')
})
})
describe("Service", () => {
it("renders Service with alert copyText success after click", async () => {
const wrapper = mount(Service, {
global: {
plugins: [store],
},
props: {
copyed: '',
service: {init_params: {service: 'Test service', machine_addr: 'Test machine_addr'}},
},
})
await wrapper.get('.copyText').trigger('click')
expect(wrapper.html()).toContain('copyed')
})
})

View File

@@ -0,0 +1,761 @@
export default dom = (() => {
const getNodeList = (arg) => {
if (typeof arg === "string" && arg.trim().slice(0, 1) !== "<") {
return document.querySelectorAll(arg);
} else if (typeof arg === "string" && arg.trim().slice(0, 1) === "<") {
const dom = domParser(arg);
return [dom];
} else if (typeof arg === "object" && arg instanceof NodeList) {
return arg;
} else if (typeof arg === "object" && arg instanceof HTMLElement) {
return [arg];
} else if (typeof arg === "object" && arg instanceof SVGElement) {
return [arg];
} else {
return arg;
}
};
const domParser = (arg) => {
const parser = new DOMParser(),
content = "text/html",
DOM = parser.parseFromString(arg, content);
return DOM.body.childNodes[0];
};
const addEvent = (nodeList, event, callback) => {
nodeList[event] = callback;
};
const uuidv4 = () => {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
let r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
};
const eventPath = (evt) => {
let path = (evt.composedPath && evt.composedPath()) || evt.path,
target = evt.target;
if (path != null) {
// Safari doesn't include Window, but it should.
return path.indexOf(window) < 0 ? path.concat(window) : path;
}
if (target === window) {
return [window];
}
function getParents(node, memo) {
memo = memo || [];
let parentNode = node.parentNode;
if (!parentNode) {
return memo;
} else {
return getParents(parentNode, memo.concat(parentNode));
}
}
return [target].concat(getParents(target), window);
};
const addEvents = (nodeList) => {
// .on()
addEvent(nodeList, "on", (arg1, arg2, arg3) => {
nodeList.forEach((node) => {
node.addEventListener(
arg1,
(e) => {
const uuid = uuidv4();
if (typeof arg2 === "string") {
eventPath(e).every((parentNode) => {
if (parentNode.matches && parentNode.matches(arg2)) {
parentNode[uuid] = arg3;
parentNode[uuid](parentNode);
delete parentNode[uuid];
return false;
} else {
return true;
}
});
} else {
node[uuid] = arg2;
node[uuid](e);
delete node[uuid];
}
},
false
);
});
return nodeList;
});
// .css()
addEvent(nodeList, "css", (arg1, arg2) => {
// Get values
if (arg2 === undefined && typeof arg1 !== "object") {
return getComputedStyle(nodeList[0])[arg1];
}
// Set values
nodeList.forEach((node) => {
if (typeof arg1 === "object") {
for (const [key, val] of Object.entries(arg1)) {
node.style[key] = val;
}
} else {
node.style[arg1] = arg2;
}
});
addEvents(nodeList);
return nodeList;
});
// .slideUp()
addEvent(nodeList, "slideUp", (arg1 = 300, arg2 = () => {}) => {
nodeList.forEach((node) => {
node.style.transitionProperty = "height, margin, padding";
node.style.transitionDuration = arg1 + "ms";
node.style.height = node.offsetHeight + "px";
node.offsetHeight;
node.style.overflow = "hidden";
node.style.height = 0;
node.style.paddingTop = 0;
node.style.paddingBottom = 0;
node.style.marginTop = 0;
node.style.marginBottom = 0;
window.setTimeout(() => {
node.style.display = "none";
node.style.removeProperty("height");
node.style.removeProperty("padding-top");
node.style.removeProperty("padding-bottom");
node.style.removeProperty("margin-top");
node.style.removeProperty("margin-bottom");
node.style.removeProperty("overflow");
node.style.removeProperty("transition-duration");
node.style.removeProperty("transition-property");
const uuid = uuidv4();
node[uuid] = arg2;
node[uuid](node);
delete node[uuid];
}, arg1);
});
addEvents(nodeList);
return nodeList;
});
// .slideDown()
addEvent(nodeList, "slideDown", (arg1 = 300, arg2 = () => {}) => {
nodeList.forEach((node) => {
node.style.removeProperty("display");
let display = window.getComputedStyle(node).display;
if (display === "none") display = "block";
node.style.display = display;
let height = node.offsetHeight;
node.style.overflow = "hidden";
node.style.height = 0;
node.style.paddingTop = 0;
node.style.paddingBottom = 0;
node.style.marginTop = 0;
node.style.marginBottom = 0;
node.offsetHeight;
node.style.transitionProperty = "height, margin, padding";
node.style.transitionDuration = arg1 + "ms";
node.style.height = height + "px";
node.style.removeProperty("padding-top");
node.style.removeProperty("padding-bottom");
node.style.removeProperty("margin-top");
node.style.removeProperty("margin-bottom");
window.setTimeout(() => {
node.style.removeProperty("height");
node.style.removeProperty("overflow");
node.style.removeProperty("transition-duration");
node.style.removeProperty("transition-property");
const uuid = uuidv4();
node[uuid] = arg2;
node[uuid](node);
delete node[uuid];
}, arg1);
});
addEvents(nodeList);
return nodeList;
});
// .fadeOut()
addEvent(nodeList, "fadeOut", (arg1 = 300, arg2 = () => {}) => {
nodeList.forEach((node) => {
node.style.opacity = 1;
node.style.transitionProperty = "opacity";
node.style.transitionDuration = arg1 + "ms";
node.style.opacity = 0;
window.setTimeout(() => {
node.style.display = "none";
node.style.removeProperty("transition-property");
node.style.removeProperty("transition-duration");
node.style.removeProperty("opacity");
const uuid = uuidv4();
node[uuid] = arg2;
node[uuid](node);
delete node[uuid];
}, arg1);
});
addEvents(nodeList);
return nodeList;
});
// .fadeIn()
addEvent(nodeList, "fadeIn", (arg1 = 300, arg2 = () => {}) => {
nodeList.forEach((node) => {
let display = window.getComputedStyle(node).display;
if (display === "none") display = "block";
node.style.display = display;
node.style.opacity = 0;
node.style.transitionProperty = "opacity";
node.style.transitionDuration = arg1 + "ms";
window.setTimeout(() => {
node.style.opacity = 1;
window.setTimeout(() => {
node.style.removeProperty("transition-property");
node.style.removeProperty("transition-duration");
node.style.removeProperty("opacity");
}, arg1);
const uuid = uuidv4();
node[uuid] = arg2;
node[uuid](node);
delete node[uuid];
}, arg1);
});
addEvents(nodeList);
return nodeList;
});
// .hide()
addEvent(nodeList, "hide", () => {
nodeList.forEach((node) => {
node.style.display = "none";
});
addEvents(nodeList);
return nodeList;
});
// .show()
addEvent(nodeList, "show", () => {
nodeList.forEach((node) => {
if (node.style.display === "none") {
node.style.display = "block";
}
});
addEvents(nodeList);
return nodeList;
});
// .clone()
addEvent(nodeList, "clone", () => {
let clonedNodes = [];
nodeList.forEach((node) => {
clonedNodes.push(node.cloneNode(true));
});
addEvents(clonedNodes);
return clonedNodes;
});
// .each()
addEvent(nodeList, "each", (arg1) => {
nodeList.forEach((node, index) => {
const uuid = uuidv4();
node[uuid] = arg1;
node[uuid](index, node);
delete node[uuid];
});
addEvents(nodeList);
return nodeList;
});
// .find()
addEvent(nodeList, "find", (arg1) => {
let results = [];
nodeList.forEach((node) => {
const matchedNodes = node.querySelectorAll(arg1);
if (matchedNodes.length) {
matchedNodes.forEach((matchedNode) => {
results.push(matchedNode);
});
}
});
addEvents(results);
return results;
});
// .hasClass()
addEvent(nodeList, "hasClass", (arg1) => {
let found = false;
nodeList.forEach((node) => {
if (node.classList.contains(arg1)) found = true;
});
return found;
});
// .removeClass()
addEvent(nodeList, "removeClass", (arg1) => {
if (arg1.length) {
arg1.split(" ").forEach((classname) => {
nodeList.forEach((node) => {
node.classList.remove(classname);
});
});
}
addEvents(nodeList);
return nodeList;
});
// .addClass()
addEvent(nodeList, "addClass", (arg1) => {
if (arg1.length) {
arg1.split(" ").forEach((classname) => {
nodeList.forEach((node) => {
node.classList.add(classname);
});
});
}
addEvents(nodeList);
return nodeList;
});
// .is()
addEvent(nodeList, "is", (arg1) => {
if (typeof arg1 === "string") {
return (
nodeList[0].matches ||
nodeList[0].matchesSelector ||
nodeList[0].msMatchesSelector ||
nodeList[0].mozMatchesSelector ||
nodeList[0].webkitMatchesSelector ||
nodeList[0].oMatchesSelector
).call(nodeList[0], arg1);
} else {
return nodeList[0] === arg1;
}
});
// .attr()
addEvent(nodeList, "attr", (arg1, arg2) => {
if (arg2 === undefined && typeof arg1 !== "object") {
if (nodeList[0] !== undefined) {
const attr = nodeList[0].getAttribute(arg1);
return attr === null ? undefined : attr;
} else {
return undefined;
}
}
nodeList.forEach((node) => {
if (typeof arg1 === "object") {
for (const [key, val] of Object.entries(arg1)) {
node.setAttribute(key, val);
}
} else {
node.setAttribute(arg1, arg2);
}
});
addEvents(nodeList);
return nodeList;
});
// .removeAttr()
addEvent(nodeList, "removeAttr", (arg1) => {
nodeList.forEach((node) => {
node.removeAttribute(arg1);
});
addEvents(nodeList);
return nodeList;
});
// .data()
addEvent(nodeList, "data", (arg1, arg2) => {
if (arg2 === undefined) {
const attr = nodeList[0].getAttribute(`data-${arg1}`);
return attr === null ? undefined : attr;
}
nodeList.forEach((node) => {
node.setAttribute(`data-${arg1}`, arg2);
});
addEvents(nodeList);
return nodeList;
});
// .width()
addEvent(nodeList, "width", (arg1) => {
if (arg1 === undefined) {
if (nodeList === window) {
return parseInt(window.innerWidth);
} else {
return typeof nodeList[0] !== "undefined"
? parseInt(getComputedStyle(nodeList[0])["width"])
: null;
}
}
if (nodeList === window) {
window.resizeTo(arg1, window.innerHeight);
} else {
nodeList.forEach((node) => {
node.style["width"] = arg1;
});
}
addEvents(nodeList);
return nodeList;
});
// .height()
addEvent(nodeList, "height", (arg1) => {
if (arg1 === undefined) {
if (nodeList === window) {
return parseInt(window.innerHeight);
} else {
return typeof nodeList[0] !== "undefined"
? parseInt(getComputedStyle(nodeList[0])["height"])
: null;
}
}
if (nodeList === window) {
window.resizeTo(window.innerWidth, arg1);
} else {
nodeList.forEach((node) => {
node.style["height"] = arg1;
});
}
addEvents(nodeList);
return nodeList;
});
// .css()
addEvent(nodeList, "css", (arg1, arg2) => {
if (arg2 === undefined && typeof arg1 !== "object") {
return getComputedStyle(nodeList[0])[arg1];
}
nodeList.forEach((node) => {
if (typeof arg1 === "object") {
for (const [key, val] of Object.entries(arg1)) {
node.style[key] = val;
}
} else {
node.style[arg1] = arg2;
}
});
addEvents(nodeList);
return nodeList;
});
// .replaceWith()
addEvent(nodeList, "replaceWith", (arg1) => {
const replacerNodes = [];
const argumentTypes = getNodeList(arg1);
nodeList.forEach((node, key) => {
argumentTypes.forEach((replacerNode) => {
let dom = replacerNode;
if (key > 0) {
dom = replacerNode.cloneNode(true);
}
node.parentNode.insertBefore(dom, node.nextSibling);
replacerNodes.push(dom);
});
node.remove();
});
addEvents(replacerNodes);
return replacerNodes;
});
// .insertAfter()
addEvent(nodeList, "insertAfter", (arg1) => {
const insertedNodes = [];
const argumentTypes = getNodeList(arg1);
nodeList.forEach((node) => {
argumentTypes.forEach((targetNode, key) => {
let dom = node;
if (key > 0) {
dom = node.cloneNode(true);
}
targetNode.parentNode.insertBefore(dom, targetNode.nextSibling);
insertedNodes.push(dom);
});
});
addEvents(insertedNodes);
return insertedNodes;
});
// .appendTo()
addEvent(nodeList, "appendTo", (arg1) => {
const appendedNodes = [];
const argumentTypes = getNodeList(arg1);
nodeList.forEach((node) => {
argumentTypes.forEach((targetNode, key) => {
let dom = node;
if (key > 0) {
dom = node.cloneNode(true);
}
targetNode.appendChild(dom);
appendedNodes.push(dom);
});
});
addEvents(appendedNodes);
return appendedNodes;
});
// .append()
addEvent(nodeList, "append", (arg1) => {
const argumentTypes = getNodeList(arg1);
nodeList.forEach((node, key) => {
argumentTypes.forEach((appendNode) => {
let dom = appendNode;
if (key > 0) {
dom = appendNode.cloneNode(true);
}
node.appendChild(dom);
});
});
addEvents(nodeList);
return nodeList;
});
// .remove()
addEvent(nodeList, "remove", () => {
nodeList.forEach((node) => {
if (node.parentNode !== null) {
node.parentNode.removeChild(node);
}
});
addEvents(nodeList);
return nodeList;
});
// .first()
addEvent(nodeList, "first", () => {
const firstNode = nodeList[0] !== undefined ? [nodeList[0]] : [];
addEvents(firstNode);
return firstNode;
});
// .last()
addEvent(nodeList, "last", () => {
const lastNodeList =
nodeList[nodeList.length - 1] !== undefined
? [nodeList[nodeList.length - 1]]
: [];
addEvents(lastNodeList);
return lastNodeList;
});
// .val()
addEvent(nodeList, "val", (arg1) => {
if (arg1 === undefined) {
if (nodeList[0] instanceof HTMLSelectElement && nodeList[0].multiple) {
const selectedOptions = [];
for (const selectedOption of nodeList[0].selectedOptions) {
selectedOptions.push(selectedOption.value);
}
return selectedOptions;
} else {
return nodeList[0].value;
}
}
nodeList.forEach((node) => {
if (node instanceof HTMLSelectElement) {
node.value = "";
if (typeof arg1 !== "object") {
arg1 = [arg1];
}
for (const value of arg1) {
const selectedOption = Array.from(node).find(
(option) => option.value == value
);
if (selectedOption !== undefined) {
selectedOption.selected = true;
}
}
} else {
node.value = arg1;
}
});
addEvents(nodeList);
return nodeList;
});
// .html()
addEvent(nodeList, "html", (arg1) => {
if (arg1 === undefined) {
return nodeList[0].innerHTML;
}
nodeList.forEach((node) => {
node.innerHTML = arg1;
});
addEvents(nodeList);
return nodeList;
});
// .text()
addEvent(nodeList, "text", (arg1) => {
if (arg1 === undefined) {
return nodeList[0].textContent;
}
nodeList.forEach((node) => {
node.textContent = arg1;
});
addEvents(nodeList);
return nodeList;
});
// .filter()
addEvent(nodeList, "filter", (arg1) => {
let results = [];
nodeList.forEach((node, index) => {
const uuid = uuidv4();
node[uuid] = arg1;
const filter = node[uuid](index, node);
delete node[uuid];
if (filter) {
results.push(node);
}
});
addEvents(results);
return results;
});
// .closest()
addEvent(nodeList, "closest", (arg1) => {
let results = [];
nodeList.forEach((node) => {
const matchedNode = node.closest(arg1);
if (
matchedNode !== null &&
!results.filter((resNode) => resNode === matchedNode).length
)
results.push(matchedNode);
});
addEvents(results);
return results;
});
// .children()
addEvent(nodeList, "children", (arg1) => {
let results = [];
nodeList.forEach((node) => {
for (const matchedNode of node.children) {
if (arg1 === undefined) {
results.push(matchedNode);
} else {
for (const childNode of node.querySelectorAll(arg1)) {
if (childNode === matchedNode) results.push(childNode);
}
}
}
});
addEvents(results);
return results;
});
// .parent()
addEvent(nodeList, "parent", () => {
let results = [];
nodeList.forEach((node) => {
const matchedNode = node.parentNode;
if (
matchedNode !== null &&
!results.filter((resNode) => resNode === matchedNode).length
)
results.push(matchedNode);
});
addEvents(results);
return results;
});
// .prev()
addEvent(nodeList, "prev", () => {
let results = [];
nodeList.forEach((node) => {
if (node.previousElementSibling !== null) {
results.push(node.previousElementSibling);
}
});
addEvents(results);
return results;
});
// .next()
addEvent(nodeList, "next", () => {
let results = [];
nodeList.forEach((node) => {
if (node.nextElementSibling !== null) {
results.push(node.nextElementSibling);
}
});
addEvents(results);
return results;
});
};
return (window.dom = (arg) => {
const nodeList = getNodeList(arg);
addEvents(nodeList);
return nodeList;
});
})();

View File

@@ -0,0 +1,179 @@
import * as Popper from "@popperjs/core";
import dom from "./dom";
(function (dom) {
"use strict";
function hide() {
dom(".dropdown-menu").each(async function () {
if (
dom(this).attr("id") !== undefined &&
dom('[data-dropdown-replacer="' + dom(this).attr("id") + '"]').length &&
dom(this).data("dropdown-programmatically") === undefined
) {
let randId = dom(this).attr("id");
let dropdownToggle = dom('[data-dropdown-replacer="' + randId + '"]')
.parent()
.find("[data-tw-toggle='dropdown']");
dom(this).removeClass("show");
const event = new Event("hide.tw.dropdown");
dom(dropdownToggle).parent()[0].dispatchEvent(event);
await setTimeout(() => {
dom('[data-dropdown-replacer="' + randId + '"]').replaceWith(this);
dom(this).removeAttr("style");
dom(this).removeAttr("data-popper-placement");
dom(dropdownToggle).attr("aria-expanded", false);
const event = new Event("hidden.tw.dropdown");
dom(dropdownToggle).parent()[0].dispatchEvent(event);
}, 200);
} else if (
dom(this).attr("id") !== undefined &&
!dom('[data-dropdown-replacer="' + dom(this).attr("id") + '"]')
.length &&
dom(this).hasClass("show") &&
dom(this).data("dropdown-programmatically") === undefined
) {
dom(this).remove();
} else if (dom(this).data("dropdown-programmatically") == "initiate") {
dom(this).attr("data-dropdown-programmatically", "showed");
} else if (dom(this).data("dropdown-programmatically") == "showed") {
dom(this).removeAttr("data-dropdown-programmatically");
hide();
}
});
}
function findVisibleDropdownToggle(dropdownToggle) {
return dropdownToggle.filter((key, dropdownToggle) => {
return dropdownToggle.offsetParent !== null;
});
}
async function show(dropdown) {
let dropdownBox = dom(dropdown).find(".dropdown-menu").first();
let dropdownToggle = findVisibleDropdownToggle(
dom(dropdown).find("[data-tw-toggle='dropdown']")
);
let placement = dom(dropdown).data("tw-placement")
? dom(dropdown).data("tw-placement")
: "bottom-end";
let randId = "_" + Math.random().toString(36).substr(2, 9);
hide();
if (dom(dropdownBox).length) {
dom(dropdownToggle).attr("aria-expanded", true);
dom(dropdown).css("position") == "static"
? dom(dropdown).css("position", "relative")
: "";
dom(dropdownBox).css("width", dom(dropdownBox).css("width").localeCompare("300px") ? "300px": dom(dropdownBox).css("width"));
dom('<div data-dropdown-replacer="' + randId + '"></div>').insertAfter(
dropdownBox
);
dom(dropdownBox).attr("id", randId).appendTo("body");
dom(".modal.show").each(function () {
if (dom(this).find('[data-dropdown-replacer="' + randId + '"]')) {
dom(dropdownBox).css("z-index", dom(this).css("z-index"));
}
});
Popper.createPopper(dropdownToggle[0], dropdownBox[0], {
placement: placement,
});
dom(dropdownBox).addClass("show");
const event = new Event("show.tw.dropdown");
dom(dropdown)[0].dispatchEvent(event);
await setTimeout(() => {
const event = new Event("shown.tw.dropdown");
dom(dropdown)[0].dispatchEvent(event);
}, 200);
}
}
function toggleProgrammatically(dropdown) {
let dropdownBox = dom(dropdown).find(".dropdown-menu").first();
if (dom(dropdownBox).length) {
showProgrammatically(dropdown);
} else {
hide();
}
}
function showProgrammatically(dropdown) {
if (dom(dropdown).find(".dropdown-menu").length) {
dom(dropdown)
.find(".dropdown-menu")
.attr("data-dropdown-programmatically", "initiate");
} else {
let randId = dom("[data-dropdown-replacer]").data("dropdown-replacer");
dom("#" + randId).attr("data-dropdown-programmatically", "initiate");
}
show(dropdown);
}
function createInstance(dropdownToggle) {
const dropdown = dom(dropdownToggle).closest(".dropdown");
return {
show() {
showProgrammatically(dropdown);
},
hide() {
hide();
},
toggle() {
toggleProgrammatically(dropdown);
},
};
}
dom("body").on("click", function (event) {
let dropdown = dom(event.target).closest(".dropdown");
let dropdownToggle = dom(dropdown).find("[data-tw-toggle='dropdown']");
let dropdownBox = dom(dropdown).find(".dropdown-menu").first();
let activeDropdownBox = dom(event.target).closest(".dropdown-menu").first();
let dismissButton = dom(event.target).data("tw-dismiss");
if (
(!dom(dropdown).length && !dom(activeDropdownBox).length) ||
(dom(dropdownToggle).length && !dom(dropdownBox).length) ||
dismissButton == "dropdown"
) {
hide();
} else if (!dom(activeDropdownBox).length) {
show(dropdown);
}
});
document.addEventListener("keydown", function (event) {
if (event.code == "Escape") {
hide();
}
});
(function init() {
dom("[data-tw-toggle='dropdown']").each(function () {
this["__dropdown"] = createInstance(this);
});
if (window.tailwind === undefined) window.tailwind = {};
window.tailwind.Dropdown = {
getInstance(el) {
return el.__dropdown;
},
getOrCreateInstance(el) {
return el.__dropdown === undefined ? createInstance(el) : el.__dropdown;
},
};
})();
})(dom);

View File

@@ -0,0 +1,20 @@
<template>
<div>
123
</div>
</template>
<script>
export default {
name: 'Name',
components: {},
data() {
},
computed: {
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,55 @@
import { type } from 'ramda'
/* eslint-disable */
export const runAfter = (func, wait, immediate) => {
let timeout
return function () {
const context = this
const args = arguments
const later = function () {
timeout = null
if (!immediate) func.apply(context, args)
}
const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(context, args)
}
}
export const pipe = (x) => (...fns) => fns.reduce((r, fn) => fn(r), x)
export const capitalizeFirstLetter = (string) => {
if (string) return string.charAt(0).toUpperCase() + string.slice(1);
else {
const error = `error by func capitalizeFirstLetter, ivalid string. string can't equal ${string}`
console.error(error)
return error;
}
}
export const lowerFirstLetter = (string) => {
if (string && string[0] && type(string) === "String") return string[0].toLowerCase() + string.slice(1);
else {
const error = `error by func lowerFirstLetter, ivalid string. string can't equal ${string}`
console.error(error)
return error;
}
}
export const substringIfNeed = (string, length = 40) => {
if (string && `${string}`.length > length) {
return `${string}`.substring(0, length) + "...";
}
return string;
}
export const copyObj = (mainObj) => {
let objCopy = {};
let key;
for (key in mainObj) {
objCopy[key] = mainObj[key];
}
return objCopy;
}

View File

@@ -0,0 +1,15 @@
import { createApp } from 'vue'
import "./styles/_index.css"
import 'remixicon/fonts/remixicon.css'
import App from './App.vue'
import Router from "@router"
import ramdaVue from "ramda-vue";
import { store } from '@/store'
import "./helpers/dropdown.js"
createApp(App)
.use(Router)
.use(ramdaVue)
.use(store)
.mount('#app')

View File

@@ -0,0 +1,152 @@
import {createRouter, createWebHistory} from "vue-router";
import Main from "@pages/Main/index.vue"
import Auth from "@pages/Auth/index.vue"
import Communication from "@pages/Communication/index.vue"
import LastPacksNum from "@pages/LastPacksNum/index.vue"
import Fuel from "@pages/Fuel/index.vue"
import Cron from "@pages/Cron/index.vue"
import ICCID from "@pages/ICCID/index.vue"
import Devices from "@pages/Devices/index.vue"
import FreePacks from "@pages/FreePacks/index.vue"
import Packs from "@pages/Packs/index.vue"
import FileViewer from "@pages/FileViewer/index.vue"
import NotFound from "@pages/404/index.vue"
import Diap from "@admin_pages/Diap/index.vue"
import News from "@admin_pages/News/index.vue"
import Machines from "@pages/Main/Machines/Machines.vue"
import FinderPacks from "@pages/Main/FinderPacks/FinderPacks.vue"
import AddUsers from "@pages/Main/AddUsers/AddUsers.vue"
import Services from "@admin_pages/Services/index.vue"
import ServiceManage from "@admin_pages/ServiceManage/index.vue"
import {get} from "@store/modules/apiHelpers"
import {cond, T} from 'ramda'
const routes = [
{
path: "/",
name: "main",
component: Main,
children: [
{
path: "/",
name: "machines",
component: Machines
},
{
path: "finder_packs",
name: "finder_packs",
component: FinderPacks
},
{
path: "add_users",
name: "add_users",
component: AddUsers
},
]
},
{
path: "/html/last_packs",
name: "last_packs",
component: import("@pages/LastPacks/index.vue")
},
{
path: "/html/controll_com_service",
name: "communication",
component: import("@pages/Communication/index.vue")
},
{
path: "/html/last_packs_by_pack",
name: "last_packs_by_pack",
component: LastPacksNum
},
{
path: "/html/live_fuel",
name: "fuel",
component: Fuel
},
{
path: "/html/cron",
name: "cron",
component: Cron
},
{
path: "/html/fetch_iccid",
name: "iccid",
component: ICCID
},
{
path: "/html/fetch_askr_devices",
name: "devices",
component: Devices
},
{
path: "/html/packs/:pack",
name: "packs",
component: Packs
},
{
path: "/html/free_packs",
name: "free_packs",
component: FreePacks
},
{
path: "/html/file_viewer/:mode",
name: "file_viewer",
component: FileViewer
},
{
path: "/admin_panel/diap",
name: "diap",
component: Diap
},
{
path: "/admin_panel/services",
name: "services",
component: Services
},
{
path: "/users/log_out",
name: "auth",
component: Auth
},
{
path: '/:pathMatch(.*)*',
name: '404',
component: NotFound
},
];
const routerHistory = createWebHistory();
const router = createRouter({
history: routerHistory,
routes
});
const logout = (from, next) => {
localStorage.removeItem("token")
sessionStorage.setItem("pathname", from.fullPath)
next()
}
const auth_check = async () => {
return await get("/api/auth/loginStatus", ({data}) => {
return data
})
}
// router.beforeEach(async (to, from, next) => {
// const status = await auth_check()
// return cond([
// [() => to.name === "auth" && from.name === "auth", () => next()],
// [() => to.name === "auth", () => logout(from, () => next())],
// [() => !status, () => logout(from, () => next({name: "auth"}))],
// [T, () => next()],
// ])()
// })
export {routes};
export default router;

View File

@@ -0,0 +1,54 @@
export class Alert {
constructor({ id, color, icon, message, delay = 4000, isManualClose, width = 'auto' }) {
const wrapper = document.createElement("div");
const alert = document.createElement("div");
wrapper.appendChild(alert)
const closeIcon = document.createElement("i");
closeIcon.className = `ri-close-circle-fill cursor-pointer text-${color}-800 text-xl`;
closeIcon.onclick = () => {
this.destroy()
}
alert.className = `bg-${color}-100 bg-light shadow-md rounded-lg py-5 px-6 mb-3 text-base text-${color}-700 inline-flex items-center fixed transition_up top-0 translate-y-4 left-0 w-fit slideInFromTop border border-${color}-300 gap-4 flex items-center`;
alert.style.zIndex = "10000";
alert.style.width = width
alert.style.transform = "translateY(-5rem) translateX(calc(50vw - 50%))";
alert.setAttribute("role", "alert");
alert.innerHTML = `
<div class="w-fit gap-1 flex items-center">
${icon}
${typeof message === 'string' ? message : JSON.stringify(message)}
</div>
`;
alert.appendChild(closeIcon);
document.getElementById(id).appendChild(wrapper);
setTimeout(() => {
if (delay > 100 && alert) {
alert.style.transform =
"translateY(1.25rem) translateX(calc(50vw - 50%))";
}
}, 300);
setTimeout(() => {
if(!isManualClose) {
this.destroy()
}
}, delay)
this.alert = alert;
this.wrapper = wrapper;
}
destroy() {
if (this.alert) {
this.alert.style.transform =
"translateY(-5rem) translateX(calc(50vw - 50%))";
setTimeout(() => {
this.wrapper.innerHTML = "";
this.alert = null;
}, 300);
}
}
}

View File

@@ -0,0 +1,53 @@
import { Alert } from "@store/hooks/Alert"
export class CopyToClipboard {
constructor (id, text) {
this.id = id
this.el = document.getElementById(id)
this.text = this.el?.getAttribute('copyOnClickText')
this.clipBoardText = text
if (this.el && this.clipBoardText && !this.text) {
this.copy(this.clipBoardText, this.id)
}
if (this.el && this.text && !this.clipBoardText) {
this.copy(this.text, this.id)
}
}
copy (text, id) {
if (!navigator.clipboard) {
fallbackCopyTextToClipboard(text)
return
}
navigator.clipboard.writeText(text).then(function() {
new Alert({id: id, color: "green", icon: `<i class="ri-book-fill"></i>`, message: "Текст скопирован", delay: 2000})
}, function(err) {
new Alert({id: id, color: "yellow", icon: `<i class="ri-book-fill"></i>`, message: "Ваш браузер не поддерживает копирование", delay: 2000})
console.error('Async: Could not copy text: ', err)
})
}
fallbackCopyTextToClipboard (text, id) {
var textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.top = "-2000px";
textArea.style.left = "-2000px";
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
new Alert({id: id, color: "green", icon: `<i class="ri-book-fill"></i>`, message: "Текст скопирован", delay: 2000})
} catch (err) {
new Alert({id: id, color: "yellow", icon: `<i class="ri-book-fill"></i>`, message: "Ваш браузер не поддерживает копирование", delay: 2000})
}
document.body.removeChild(textArea);
}
}

View File

@@ -0,0 +1,110 @@
import {groupByKeys} from "./staticData"
import {getTextWidth, isEqualKeyValue} from "./helpers"
const viewGraphicsPieBar = (chartId, period = '', countMachines, title, windowConfig, isOpenMenu = false, type) => {
const elements = []
const elementsMenu = []
const elPeriod = period ? {id: 'period', name: 'Период загрузки данных', type: 'text', right: windowConfig?.pie?.graphic?.period?.right, top: windowConfig?.pie?.graphic?.period?.top, z: 999, style: {text: period, fill: '#777', fontSize: windowConfig?.pie?.graphic?.period?.fontSize}} : ''
elements.push(elPeriod)
const elCount = {id: 'count', type: 'text', left: windowConfig?.pie?.graphic?.count?.left, top: windowConfig?.pie?.graphic?.count?.top, z: 999, style: {text: countMachines, textAlign: 'center', fontSize: windowConfig?.pie?.graphic?.count?.fontSize}}
!chartId.includes('bar') ? elements.push(elCount) : ''
const elMenu = {id: 'menu', info: {typeEl: 'menu', chartId: chartId, type: type}, type: 'image', right: windowConfig?.bar?.graphic?.menu?.right, top: windowConfig?.bar?.graphic?.menu?.top, z: 999, style: {image: '/images/charts-more-vertical.svg', width: 17, height: 17}}
elements.push(elMenu)
const elRect = {id: 'rect', info: {typeEl: 'rect'}, type: 'rect', right: windowConfig?.bar?.graphic?.rect?.right, top: windowConfig?.bar?.graphic?.rect?.top, z: 999, shape: {width: 90, height: 40, r: [5]}, style: {fill: '#eee', shadowBlur: 8, shadowOffsetX: 3, shadowOffsetY: 3, shadowColor: 'rgba(0,0,0,0.3)'}}
const elDownload = {id: 'download', info: {typeEl: 'download', title: title, countCharts: countMachines, chartId: chartId, type: type}, type: 'image', right: windowConfig?.bar?.graphic?.download?.right, top: windowConfig?.bar?.graphic?.download?.top, z: 999, style: {image: '/images/charts-download.svg', width: 17, height: 17}}
const elDownloadTitle = {id: 'downloadTitle', info: {typeEl: 'downloadTitle', title: title, countCharts: countMachines, chartId: chartId, type: type}, type: 'text', right: windowConfig?.bar?.graphic?.downloadTitle?.right, top: windowConfig?.bar?.graphic?.downloadTitle?.top, z: 999, style: {text: 'Скачать', fontSize: 12}}
elementsMenu.push(elRect)
elementsMenu.push(elDownload)
elementsMenu.push(elDownloadTitle)
const menuContainer = {id: 'menuContainer', type: 'group', bounding: 'raw', right: windowConfig?.bar?.graphic?.container?.right, top: windowConfig?.bar?.graphic?.container?.top, z: 9999, children: elementsMenu, style: {}}
isOpenMenu ? elements.push(menuContainer) : ''
return elements
}
const viewGraphicsTypesBar = (chartId, title, groupByKey, prevGroupByData, filterKey, prevGroupByFilters, type, groupByValue, period, countCharts, windowConfig, isOpenMenu = false) => {
const elements = []
const elementsMenu = []
const elPeriod = {id: 'period', name: 'Период загрузки данных', type: 'text', right: windowConfig?.bar?.graphic?.period?.right, top: windowConfig?.bar?.graphic?.period?.top, z: 999, style: {text: period, fill: '#777', fontSize: windowConfig?.pie?.graphic?.period?.fontSize}}
elements.push(elPeriod)
const elMenu = {id: 'menu', info: {typeEl: 'menu', chartId: chartId, groupByKey: groupByKey, filterKey: filterKey, prevGroupByData: prevGroupByData, prevGroupByFilters: prevGroupByFilters, type: type, groupByValue: groupByValue}, type: 'image', right: windowConfig?.bar?.graphic?.menu?.right, top: windowConfig?.bar?.graphic?.menu?.top, z: 999, style: {image: '/images/charts-more-vertical.svg', width: 17, height: 17}}
elements.push(elMenu)
const elRect = {id: 'rect', info: {typeEl: 'rect'}, type: 'rect', right: windowConfig?.bar?.graphic?.rect?.right, top: windowConfig?.bar?.graphic?.rect?.top, z: 999, shape: {width: 90, height: 40, r: [5]}, style: {fill: '#eee', shadowBlur: 8, shadowOffsetX: 3, shadowOffsetY: 3, shadowColor: 'rgba(0,0,0,0.3)'}}
const elDownload = {id: 'download', info: {typeEl: 'download', title: title, countCharts: countCharts, chartId: chartId, groupByKey: groupByKey, filterKey: filterKey, prevGroupByData: prevGroupByData, prevGroupByFilters: prevGroupByFilters, type: type, groupByValue: groupByValue}, type: 'image', right: windowConfig?.bar?.graphic?.download?.right, top: windowConfig?.bar?.graphic?.download?.top, z: 999, style: {image: '/images/charts-download.svg', width: 17, height: 17}}
const elDownloadTitle = {id: 'downloadTitle', info: {typeEl: 'downloadTitle', title: title, countCharts: countCharts, chartId: chartId, groupByKey: groupByKey, filterKey: filterKey, prevGroupByData: prevGroupByData, prevGroupByFilters: prevGroupByFilters, type: type, groupByValue: groupByValue}, type: 'text', right: windowConfig?.bar?.graphic?.downloadTitle?.right, top: windowConfig?.bar?.graphic?.downloadTitle?.top, z: 999, style: {text: 'Скачать', fontSize: 12}}
elementsMenu.push(elRect)
elementsMenu.push(elDownload)
elementsMenu.push(elDownloadTitle)
const menuContainer = {id: 'menuContainer', type: 'group', bounding: 'raw', right: windowConfig?.bar?.graphic?.container?.right, top: windowConfig?.bar?.graphic?.container?.top, z: 9999, children: elementsMenu, style: {}}
isOpenMenu ? elements.push(menuContainer) : ''
if(groupByKey !== groupByKeys.firstLayout && !isEqualKeyValue(prevGroupByData, groupByKeys.firstLayout, null)) {
const buttonPrevious = {id: `${filterKey}`, info: {typeEl: 'buttonPrevious', prevGroupByData: prevGroupByData, prevGroupByFilters: prevGroupByFilters, type: type, groupByValue: groupByValue}, type: 'image', right: windowConfig?.bar?.graphic?.back?.right, top: windowConfig?.bar?.graphic?.back?.top, z: 999, style: {image: '/images/arrow-go-back-line.svg', width: 17, height: 17}}
elements.push(buttonPrevious)
}
const elMain = {id: '', info: {typeEl: 'link', filterKey: filterKey, prevGroupByData: prevGroupByData, prevGroupByFilters: prevGroupByFilters, type: type, groupByValue: groupByValue}, type: 'text', left: windowConfig?.bar?.graphic?.link?.left, top: windowConfig?.bar?.graphic?.link?.top, z: 999, style: {text: 'Главная', fontSize: 10}}
const notFirstLayout = !Object.keys(prevGroupByFilters).includes(groupByKeys.firstLayout)
for (const groupByFilter in prevGroupByFilters) {
if (groupByFilter === groupByKeys.firstLayout) {
const linkTitle = `/ ${prevGroupByFilters[groupByKeys.firstLayout]}`
const leftPos = getTextWidth('Главная ', 10)
elMain.id = `0:link_${groupByFilter}-${linkTitle}`
elements.push(elMain)
const paramsFirst = {id: `1:link_${groupByFilter}-${linkTitle}`, info: {typeEl: 'link', filterKey: filterKey, prevGroupByData: prevGroupByData, prevGroupByFilters: prevGroupByFilters, type: type, groupByValue: groupByValue}, type: 'text', left: leftPos + windowConfig?.bar?.graphic?.link?.left, top: windowConfig?.bar?.graphic?.link?.top, z: 999, style: {text: linkTitle, fontSize: 10}}
elements.push(paramsFirst)
}
if (groupByFilter === groupByKeys.secondLayout) {
const linkTitle = ` / ${prevGroupByFilters[groupByKeys.secondLayout]}`
const leftPos = getTextWidth(notFirstLayout ? 'Главная ' : `Главная / ${prevGroupByFilters[groupByKeys.firstLayout]}`, 10)
if(notFirstLayout) {
elMain.id = `0:link_${groupByFilter}-${linkTitle}`
elements.push(elMain)
}
const paramsSecond = {id: `2:link_${groupByFilter}-${linkTitle}`, info: {typeEl: 'link', filterKey: filterKey, prevGroupByData: prevGroupByData, prevGroupByFilters: prevGroupByFilters, type: type, groupByValue: groupByValue}, type: 'text', left: leftPos + windowConfig?.bar?.graphic?.link?.left, top: windowConfig?.bar?.graphic?.link?.top, z: 999, style: {text: linkTitle, fontSize: 10}}
elements.push(paramsSecond)
}
if (groupByFilter === groupByKeys.thirdLayout) {
const linkTitle = ` / ${prevGroupByFilters[groupByKeys.thirdLayout]}`
const leftPos = getTextWidth(notFirstLayout ? `Главная / ${prevGroupByFilters[groupByKeys.secondLayout]} ` : `Главная / ${prevGroupByFilters[groupByKeys.firstLayout]} / ${prevGroupByFilters[groupByKeys.secondLayout]}`, 10)
const paramsThird = {id: `3:link_${groupByFilter}-${linkTitle}`, info: {typeEl: 'link', filterKey: filterKey, prevGroupByData: prevGroupByData, prevGroupByFilters: prevGroupByFilters, type: type, groupByValue: groupByValue}, type: 'text', left: leftPos + windowConfig?.bar?.graphic?.link?.left, top: windowConfig?.bar?.graphic?.link?.top, z: 999, style: {text: linkTitle, fontSize: 10}}
elements.push(paramsThird)
}
if (groupByFilter === groupByKeys.fourthLayout) {
const linkTitle = ` / ${prevGroupByFilters[groupByKeys.fourthLayout]}`
const leftPos = getTextWidth(notFirstLayout ? `Главная / ${prevGroupByFilters[groupByKeys.secondLayout]} / ${prevGroupByFilters[groupByKeys.thirdLayout]} ` : `Главная / ${prevGroupByFilters[groupByKeys.firstLayout]} / ${prevGroupByFilters[groupByKeys.secondLayout]} / ${prevGroupByFilters[groupByKeys.thirdLayout]}`, 10)
const paramsFourth = {id: `4:link_${groupByFilter}-${linkTitle}`, info: {typeEl: 'link', filterKey: filterKey, prevGroupByData: prevGroupByData, prevGroupByFilters: prevGroupByFilters, type: type, groupByValue: groupByValue}, type: 'text', left: leftPos + windowConfig?.bar?.graphic?.link?.left, top: windowConfig?.bar?.graphic?.link?.top, z: 999, style: {text: linkTitle, fontSize: 10}}
elements.push(paramsFourth)
}
}
return elements
}
export {viewGraphicsPieBar, viewGraphicsTypesBar}

View File

@@ -0,0 +1,157 @@
import {groupByKeys} from './staticData'
const getTextWidth = (text, font) => {
const canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement('canvas'));
const context = canvas.getContext('2d');
context.font = font;
const metrics = context.measureText(text);
return metrics.width;
}
const isEqualKeyValue = (obj, key, value) => {
if(Object.keys(obj).length > 0) {
if(Object.keys(obj).includes(key) && Object.values(obj).includes(value)) return true
else return false
} else return true
}
const chartsFiltersNames = (data) => [...new Set(data.map((el) => el.charts.map((item) => item.name)).flat())]
const chartsFiltersNamesMl = (data) => [...new Set(data.map((el) => el.map((item) => item.charts.name)).flat().filter(el => el !== undefined))]
const updateBarChartsData = (data, key) => data.map((el) => {
return el.filter((item) => {
if(item.name === key) {
return el
}
})
}).flat()
const dataFilterTitle = (data, title) => data.map((el) => {
return el.reduce((acc, item) => {
if(item.name === title || acc !== undefined && acc?.name === title) {
if(acc === 0) {
return {name: item.name, count: Number(item.value)}
}
if(acc?.count >= 0) {
return {name: item.name, count: acc.count + Number(item.value)}
}
}
}, 0)
}).filter((el) => el !== undefined)
const prepareBarTypesData = (data, xAxisData, params) => {
if(data.charts.length < chartsFiltersNames(xAxisData).length) {
// Если нет фильтруемых данных, то добавляем столбец графика с нулевым значением для каждого элемента el
const zeroData = chartsFiltersNames(xAxisData).filter((name) => !data.charts.some(({name: name2}) => name === name2)).map((name) => {
return {filterKey: params.filterKey, name: name, value: 0}
})
const withoutZeroData = data.charts.map((item) => {
item.type = params.type
item.filterKey = params.filterKey
item.groupByKey = params.groupByKey
item.groupByValue = params.groupByValue ? params.groupByValue : params.typeId // Для графика типа "types" params.typeId
item.groupByData = {[params.groupByKey]: data.yAxisData}
item.prevGroupByData = params.prevGroupByFilters
return item})
return [withoutZeroData, zeroData].flat()
} else {
return data.charts.map((item) => {
item.type = params.type
item.filterKey = params.filterKey
item.groupByKey = params.groupByKey
item.groupByValue = params.groupByValue ? params.groupByValue : params.typeId
item.groupByData = {[params.groupByKey]: data.yAxisData}
item.prevGroupByData = params.prevGroupByFilters
return item
})
}
}
const prepareBarTypesMlData = (data, params) => {
return data.map((item) => {
// console.log('item', item)
item.charts.type = params.type
item.charts.groupByKey = params.groupByKey
item.charts.groupByValue = params.groupByValue ? params.groupByValue : params.typeId
item.charts.groupByData = {[params.groupByKey]: item.yAxisData}
item.charts.prevGroupByData = params.prevGroupByFilters
return item.charts
})
}
const groupByBarTypesData = (data, xAxisData, func) => {
const dataGroupBy = []
const dataCustomTitle = []
for(let i = 0; i < (func(xAxisData).length); i++) {
dataGroupBy.push(updateBarChartsData(data, func(xAxisData)[i]))
dataCustomTitle.push(dataFilterTitle(dataGroupBy, func(xAxisData)[i]))
}
return {dataGroupBy: dataGroupBy, dataCustomTitle: dataCustomTitle}
}
const updatedGroupByData = (idx, prevGroupByFilters, groupByKey, groupByValue) => {
switch(idx) {
case '0': return {[groupByKey]: groupByValue}
case '1':
for (const prevGroupByFilter in prevGroupByFilters) {
if(prevGroupByFilter === groupByKeys.secondLayout) {
delete prevGroupByFilters[prevGroupByFilter]
}
if(prevGroupByFilter === groupByKeys.thirdLayout) {
delete prevGroupByFilters[prevGroupByFilter]
}
if(prevGroupByFilter === groupByKeys.fourthLayout) {
delete prevGroupByFilters[prevGroupByFilter]
}
}
return prevGroupByFilters
case '2':
for (const prevGroupByFilter in prevGroupByFilters) {
if(prevGroupByFilter === groupByKeys.thirdLayout) {
delete prevGroupByFilters[prevGroupByFilter]
}
if(prevGroupByFilter === groupByKeys.fourthLayout) {
delete prevGroupByFilters[prevGroupByFilter]
}
}
return prevGroupByFilters
case '3':
for (const prevGroupByFilter in prevGroupByFilters) {
if(prevGroupByFilter === groupByKeys.fourthLayout) {
delete prevGroupByFilters[prevGroupByFilter]
}
}
return prevGroupByFilters
}
}
const choiceTypeBarTypesCharts = (params) => {
switch(params.type) {
case 'ml':
const yAxisMlData = params.yAxisData.sort().map((el) => {
return `${el.key !== null ? el.key : 'Нет данных'} ${el.val}`
}).sort((a, b) => a - b).reverse()
const xAxisMlData = params.xAxisData.map((item) => item.map((el) => el))
const mlData = xAxisMlData.sort().map((el) => prepareBarTypesMlData(el, params))
return {axisData: yAxisMlData, updatedData: groupByBarTypesData(mlData, xAxisMlData, chartsFiltersNamesMl)}
case 'mount':
const xAxisMountData = params.yAxisData.sort((a, b) => a.key - b.key).map((el) => {
return `${el.key !== null ? el.key : 'Нет данных'} \n ${el.val}`})
const mountData = params.xAxisData.sort().map((el) => prepareBarTypesData(el, params.xAxisData, params))
return {axisData: xAxisMountData, updatedData: groupByBarTypesData(mountData, params.xAxisData, chartsFiltersNames)}
default :
const yAxisDefaultData = params.yAxisData.sort().map((el) => {
return `${el.key !== null ? el.key : 'Нет данных'} ${el.val}`
}).sort((a, b) => a - b).reverse()
const defaultData = params.xAxisData.sort().map((el) => prepareBarTypesData(el, params.xAxisData, params))
return {axisData: yAxisDefaultData, updatedData: groupByBarTypesData(defaultData, params.xAxisData, chartsFiltersNames)}
}
}
export {isEqualKeyValue, getTextWidth, updatedGroupByData, choiceTypeBarTypesCharts}

View File

@@ -0,0 +1,906 @@
import * as echarts from "echarts"
import {
TitleComponent,
TooltipComponent,
LegendComponent
} from 'echarts/components';
import { PieChart } from 'echarts/charts';
import { LabelLayout } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import {colorList, colorListBar, orderCharts, title, openMachine, groupByKeys, typesCharts} from './staticData'
import {viewGraphicsPieBar, viewGraphicsTypesBar} from "./graphicElements";
import {updatedGroupByData, choiceTypeBarTypesCharts} from './helpers'
import {axisBarConfig, windowConfig, sizeOptions} from './windowConfigs'
echarts.use([
TitleComponent,
TooltipComponent,
LegendComponent,
PieChart,
CanvasRenderer,
LabelLayout
])
const lineConfig = (params, id, windowConfig) => {
const times = params.xAxisData
const states = params.yAxisData
const series = [
{
data: states,
type: 'line'
}
]
const axisConfig = axisBarConfig(params.type, times, windowConfig, params.names)
return {
title: params.title,
titleLeft: 'center',
grid: {
left: '5%',
width: '90%',
top: 30,
containLabel: true
},
axisConfig,
series,
windowConfig,
graphic: [{id: 'yAxisTitle', type: 'text', left: 40, top: 20, z: 999, style: {text: '%', fontSize: 12}}]
}
}
const linesConfig = (params, id, windowConfig) => {
// console.log('params', params)
const times = params.xAxisData
const text = params.names.yAxis ? params.names.yAxis : 'Гб'
const namesLegend = params.charts.map((el) => el.name)
const series = params.charts.map((el) => {
return {
name: el.name,
data: el.value.map((item) => item.state).reverse(),
type: 'line'
}
})
const axisConfig = axisBarConfig(params.type, times, windowConfig, params.names)
return {
title: params.title,
titleLeft: 'center',
legend: {
textStyle: {
fontSize: windowConfig?.line?.legend?.fontSize
},
orient: 'horizontal',
// top: 360,
bottom: windowConfig?.line?.legend?.bottom,
// padding: windowConfig?.bar?.legend?.padding,
left: 'center',
itemGap: windowConfig?.line?.legend?.itemGap,
itemWidth: windowConfig?.line?.legend?.itemWidth,
itemHeight: windowConfig?.line?.legend?.itemHeight,
data: namesLegend
},
grid: {
left: '5%',
width: '90%',
top: 30,
containLabel: true
},
axisConfig,
series,
windowConfig,
graphic: [{id: 'yAxisTitle', type: 'text', left: 40, top: 20, z: 999, style: {text: text, fontSize: 12}}]
}
}
const linesPackConfig = (params, id, windowConfig) => {
console.log('params', params)
const times = params.xAxisData
const text = params.names.yAxis ? params.names.yAxis : ' '
const namesLegend = params.charts.map((el) => el.name)
const series = params.charts.map((el) => {
return {
name: el.name,
data: el.value.map((item) => item.state).reverse(),
type: 'line'
}
})
const axisConfig = axisBarConfig(params.type, times, windowConfig, params.names)
return {
title: params.title,
titleLeft: 'center',
legend: {
textStyle: {
fontSize: windowConfig?.line?.legend?.fontSize
},
orient: 'horizontal',
// top: 360,
bottom: windowConfig?.linesPack?.legend?.bottom,
// padding: windowConfig?.bar?.legend?.padding,
left: 'center',
itemGap: windowConfig?.linesPack?.legend?.itemGap,
itemWidth: windowConfig?.linesPack?.legend?.itemWidth,
itemHeight: windowConfig?.linesPack?.legend?.itemHeight,
data: namesLegend
},
grid: {
left: '3%',
width: '95%',
height: '50%',
top: 30,
containLabel: true
},
axisConfig,
series,
windowConfig,
graphic: [{id: 'yAxisTitle', type: 'text', left: 40, top: 20, z: 999, style: {text: text, fontSize: 12}}]
}
}
const pieConfig = (params, id, windowConfig) => {
const data = params.machines.map((el, idx) => {
el.color = colorList(el.name)
el.order = orderCharts(el.name)
return el
}).sort((a, b) => a.order - b.order)
const series = [{
name: '',
type: 'pie',
label: {
position: 'inner',
formatter: (d) => {
// console.log('d', d)
return d.value
},
color: '#FFF',
fontWeight: 'bold'
},
minAngle: 15,
center: ['50%', '50%'],
radius: ['40%', '70%'],
color: data.map((el) => el.color), //26.12.2023
itemStyle: {
borderRadius: 5,
borderColor: '#FFF',
borderWidth: 2
},
data: data,
emphasis: {
itemStyle: {
// color: {
// type: 'radial',
// x: 0.4,
// y: 0.3,
// r: 1,
// // colorStops: [
// // {
// // offset: 0,
// // // color: `rgb(251, 118, 123, 0.5)`,
// // opacity: 0.5
// // },
// // {
// // offset: 1,
// // // color: `rgb(204, 46, 72, 0.5)`
// // opacity: 0.5
// // }
// // ]
// },
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
opacity: 0.7
}
},
}]
return {
title: title(id, params.countMachines),
type: 'pie',
legend: {
formatter: (title) => {
const [elem] = params.machines.filter((el) => el.name == title) //26.12.2023
return title + ' ' + elem.value
},
textStyle: {
fontSize: windowConfig?.pie?.legend?.fontSize
},
icon: 'circle',
orient: 'horizontal',
bottom: 0,
left: 'center',
itemGap: windowConfig?.pie?.legend?.itemGap,
itemWidth: windowConfig?.pie?.legend?.itemWidth,
itemHeight: windowConfig?.pie?.legend?.itemHeight,
},
graphic: [{elements: viewGraphicsPieBar(id, params.period, params.countMachines, title(id, params.countMachines), windowConfig, params.isOpenMenu, params.type)}],
series,
windowConfig
}
}
const barConfig = (params, id, windowConfig) => {
// console.log('params', params)
const countMachinesWithoutMl = params?.machines?.[params?.machines?.length - 1]?.key === 'routes_without_ml' ? params?.machines?.[params?.machines?.length - 1]?.values : 0
const data = params?.machines?.map((el, idx) => {
el.itemStyle = {color: colorListBar(params.type, el.name)}
return el
}).sort((a, b) => a?.value - b?.value).reverse()
const xAxisData = data.map((el) => {
return `${el.name}`
})
const series = data?.map((chart, idx) => {
return {
name: `${chart?.name} ${chart?.value}`,
type: 'bar',
tooltip: {
formatter: value => `${value.marker} ${value.data.name !== null ? value.data.name : 'Нет данных'} ${value.data.value}`
},
label: {
show: true,
position: 'inside',
overflow: 'break',
color: '#FFF',
fontWeight: 'bold',
textborderColor: '#ff0000'
// formatter: '{c}'
},
labelLine: {
show: true,
lineStyle: {
// color: 'source',
color: 'black',
}
},
labelLayout:
(p) => {
if(p.labelRect.width >= p.rect.width) {
return {hideOverlap: true}
}
},
scaleLimit: {
min: 1,
max: 5
},
itemStyle: {
borderColor: chart?.itemStyle?.color,
color: chart?.itemStyle?.color,
// borderRadius: 5,
// borderWidth: 1
},
barMinWidth: 25,
barMinHeight: 20,
barMaxWidth: '90%',
barGap: '10%',
barCategoryGap: '30%',
groupPadding: 2,
data: data.map((el, index) => idx === index ? el : ''),
// data: data, // Для мульти данных, то есть больше чем один тип данных в столбце
stack: 'total',
sampling: 'average',
emphasis: {
itemStyle: {
shadowBlur: 5,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
opacity: 1
}
},
}
})
const axisConfig = axisBarConfig(params.type, xAxisData, windowConfig)
return {
// label: {
// show: true,
// position: 'inside'
// },
title: title(id, params.countMachines, params.countMachines_with_ml, params.countMachines_without_ml ? params.countMachines_without_ml : countMachinesWithoutMl),
backgroundColor: '#FFF',
legend: {
textStyle: {
fontSize: windowConfig?.bar?.legend?.fontSize
},
orient: 'horizontal',
// top: 360,
bottom: windowConfig?.bar?.legend?.bottom,
// padding: windowConfig?.bar?.legend?.padding,
left: 'center',
itemGap: windowConfig?.bar?.legend?.itemGap,
itemWidth: windowConfig?.bar?.legend?.itemWidth,
itemHeight: windowConfig?.bar?.legend?.itemHeight,
},
grid: {
left: '5%',
width: '90%',
top: 50,
containLabel: true
},
axisConfig,
graphic: [{elements: viewGraphicsPieBar(id, params.period, params.countMachines, title(id, params.countMachinesWithMl, countMachinesWithoutMl), windowConfig, params.isOpenMenu, params.type)}],
series,
windowConfig
}
}
const barTypesConfig = (params, id, windowConfig, enableDataZoom) => {
console.log('params', params)
const isMlType = params.type === typesCharts.typesMl
const isMountType = params.type === 'mount'
const dataWithoutZero = choiceTypeBarTypesCharts(params).updatedData.dataGroupBy.map((el) => {
const result = el.map((item) => {
if(item.value === 0) {
item.value = ''
return item
}
if(item.name === null) {
item.name = 'Нет данных'
return item
}
return item
}).sort((a, b) => {
if(a.groupByKey && b.groupByKey) {
(a.groupByData[a.groupByKey] - b.groupByData[b.groupByKey])
}})
return !isMountType ? result.reverse() : result
})
// console.log('dataWithoutZero', dataWithoutZero)
const series = dataWithoutZero.map((charts, idx) => {
const [title] = choiceTypeBarTypesCharts(params).updatedData.dataCustomTitle.flat().filter((el) => el?.name == charts[0]?.name)
return {
name: `${title?.name} ${title?.count} id:${id}`,
type: 'bar',
tooltip: {
formatter: value => `${value.marker} ${value?.data?.name !== null ? value?.data?.name : 'Нет данных'} ${value?.data?.value}`
},
label: {
show: true,
position: 'inside',
overflow: 'break',
color: '#FFF',
fontWeight: 'bold',
textborderColor: '#ff0000'
// formatter: '{c}'
},
labelLine: {
show: true,
lineStyle: {
// color: 'source',
color: 'black',
}
},
labelLayout: (p) => p.labelRect.width <= p.rect.width ? {hideOverlap: true} : {hideOverlap: false},
// return {x: initCharts.getWidth() - 50, moveOverlap: 'shiftY'}
// return {dx: 50, dy: -100}
scaleLimit: {
min: 1,
max: 5
},
itemStyle: {
borderColor: isMlType ? colorListBar(params.type, charts[0]?.name) : colorList(charts[0]?.name),
color: isMlType ? colorListBar(params.type, charts[0]?.name) : colorList(charts[0]?.name),
// borderRadius: 5,
// borderWidth: 1
},
// barWidth: 30,
// barMinWidth: 25,
barMaxWidth: 35,
barMinHeight: 30,
barGap: '20%',
barCategoryGap: '10%',
groupPadding: 2,
// data: data.map((el, index) => idx === index ? el : ''), // Для одного типа данных в столбце
data: charts,
stack: 'total',
sampling: 'average',
large: true,
largeThreshold: 100,
progressiveThreshold: 1000,
emphasis: {
itemStyle: {
shadowBlur: 5,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
opacity: 1
}
},
}
})
const axisConfig = axisBarConfig(params.type, choiceTypeBarTypesCharts(params).axisData, windowConfig)
return {
// label: {
// show: true,
// position: 'inside'
// },
title: isMountType? title(id, params.countMounts) : title(id, params.countMachines, params.countMachinesWithMl, params.countMachinesWithoutMl),
legend: {
// icon: 'path://M224 320c8.823 0 16 7.177 16 16s-7.177 16-16 16-16-7.177-16-16 7.177-16 16-16m0-32c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zM320 0H128C64 0 0 42.981 0 96v256c0 47.169 50.656 86.391 106.9 94.473l-55.285 55.285c-3.78 3.78-1.103 10.243 4.243 10.243h25.798c3.183 0 6.235-1.264 8.485-3.515L150.627 448h146.745l60.486 60.485a12.002 12.002 0 0 0 8.485 3.515h25.798c5.345 0 8.022-6.463 4.243-10.243L341.1 446.472C397.344 438.391 448 399.169 448 352V96c0-53.019-63-96-128-96zM32 128h384v96H32v-96zm96-96h192c58.237 0 96 37.881 96 64H32c0-32.299 47.552-64 96-64zm192 384H128c-48.448 0-96-31.701-96-64v-96h384v96c0 32.299-47.552 64-96 64z',
// backgroundColor: '#ff0000',
orient: 'horizontal',
textStyle: {
fontSize: windowConfig?.bar?.legend?.fontSize
},
zLevel: 1,
// top: 35,
left: 'center',
bottom: windowConfig?.bar?.legend?.bottom,
itemGap: windowConfig?.bar?.legend?.itemGap,
itemWidth: windowConfig?.bar?.legend?.itemWidth,
itemHeight: windowConfig?.bar?.legend?.itemHeight,
formatter: (e) => {
return e.slice(0, e.indexOf('id') - 1)
}
// data: legendData
},
grid: {
left: '1%',
top: windowConfig?.bar?.grid?.top,
width: windowConfig?.bar?.grid?.width,
containLabel: true,
height: isMountType ? '68%' : 'auto'
},
dataZoom: [
{
type: 'slider',
show: true,
width: 14,
top: windowConfig?.bar?.dataZoom?.top,
right: '3%',
yAxisIndex: 0,
realtime: true,
start: windowConfig?.bar?.dataZoom?.start,
end: 100,
rangeMode: ['value', 'percent'],
zoomLock: true,
brushSelect: false,
minSpan: windowConfig?.bar?.dataZoom?.minSpan,
handleSize: 18,
showDetail: false,
handleIcon: "path://M12 0c-9.432 0-12 2.568-12 12s2.551 12 12 12 12-2.551 12-12-2.568-12-12-12z",
handleStyle: {
color: '#CCCCCC',
borderColor: '#CCCCCC'
}
},
{
type: 'inside',
yAxisIndex: 0,
start: 70,
end: 100,
zoomOnMouseWheel: false,
moveOnMouseWheel: true,
handleSize: 18
},
],
graphic: isMountType ? [{elements: viewGraphicsPieBar(id, '', params.countMounts, title(id), windowConfig, params.isOpenMenu, params.type)}] :
[{elements: viewGraphicsTypesBar(id, title(id, isMlType ? params.countMachinesWithMl : params.countMachines, params.countMachinesWithoutMl), params.groupByKey, params.prevGroupByData, params.filterKey, params.prevGroupByFilters, params.type, params.groupByValue ? params.groupByValue : params.typeId ? params.typeId : null, params.period, params.yAxisData.length, windowConfig, params.isOpenMenu)}],
series,
axisConfig,
windowConfig,
enableDataZoom
}
}
const config = (params) => {
// console.log('params', params)
return {
title: {
text: params.title,
subtextStyle: {
fontSize: 11,
align: 'left'
},
top: params.windowConfig?.title?.top,
left: params.titleLeft ? params.titleLeft : params.windowConfig?.title?.left,
textStyle: {
width: params.windowConfig?.title?.width,
fontSize: params.windowConfig?.title?.fontSize,
overflow: 'break',
},
},
backgroundColor: params.windowConfig?.backgroundColor,
tooltip: {trigger: 'item'},
legend: params.legend,
xAxis: params.axisConfig?.xAxis ? params.axisConfig?.xAxis : '',
yAxis: params.axisConfig?.yAxis ? params.axisConfig?.yAxis : '',
dataZoom: params.enableDataZoom && params.dataZoom ? params.dataZoom : '',
toolbox: {
show: true,
feature: {
mark: {show: true},
// dataView: {show: true, readOnly: false, title: ''},
// magicType: {show: true, type: ['line', 'bar'], title: {line: 'Переключить на линейный график', bar: 'Переключить на график гистограмму'} },
// restore: {show: true, title: 'Обновить'},
// saveAsImage: {show: true, title: ''}
},
iconStyle: {
borderColor: '#333',
// borderWidth: 0,
// borderType: 'solid'
},
emphasis: {iconStyle: {
borderColor: 'rgba(1, 97, 253, 0.3)',
}},
top: 16,
right: 20,
},
graphic: params.graphic ? params.graphic : '',
grid: params.grid ? params.grid : '',
series: params.series
}
}
class ECharts {
constructor(id, sizes = {}) {
this.id = id
this.el = document.getElementById(id)
const clientWidth = window.innerWidth
const clientHeight = window.innerHeight
this.isMobile = clientWidth < 768
this.currentWidth = clientWidth - 100
// const currentheight = clientHeight + 70
this.widthChart = sizes.width ? sizes.width : 450
this.heightChart = sizes.height ? sizes.height : 350
this.mountHeightChart = 350
this.charts = []
this.legendFilterData = []
this.initCharts = echarts.init(this.el, null, {renderer: 'canvas'})
this.initCharts.showLoading()
// window.addEventListener('resize', function() {
// this.initCharts.resize()
// })
console.log('sizes', sizes)
this.getRes = this.el.getAttribute("get-res") == "true"
}
getChart(id) {
if (this.getRes) {
return {isGetChart: true, id}
}
}
makeLineCharts(charts) {
charts.chartId = this.id
this.charts = []
this.charts.push(charts)
this.initCharts.resize(sizeOptions(this.isMobile ? this.currentWidth : this.widthChart, charts.height ? charts.height : this.heightChart))
this.initCharts.setOption(config(lineConfig(charts, this.id, windowConfig(this.isMobile, this.currentWidth))), {
replaceMerge: ["series", "yAxis", "xAxis", "grid"],
})
this.initCharts.hideLoading()
}
makeLinesCharts(charts) {
console.log('charts', charts)
charts.chartId = this.id,
this.charts = []
this.charts.push(charts)
if(this.initCharts) {
this.initCharts.resize(sizeOptions(this.isMobile ? this.currentWidth : this.widthChart, charts.height ? charts.height : this.heightChart))
this.initCharts.setOption(config(linesConfig(this.charts[0], this.id, windowConfig(this.isMobile, this.currentWidth))), {
replaceMerge: ["series", "yAxis", "xAxis", "grid"],
})
}
this.initCharts.hideLoading()
}
makeLinesPackCharts(charts) {
console.log('charts', charts)
charts.chartId = this.id,
this.charts = []
this.charts.push(charts)
if(this.initCharts) {
// this.initCharts.resize(sizeOptions(this.isMobile ? this.currentWidth : this.widthChart, charts.height ? charts.height : this.heightChart))
this.initCharts.setOption(config(linesPackConfig(this.charts[0], this.id, windowConfig(this.isMobile, this.currentWidth))), {
replaceMerge: ["series", "yAxis", "xAxis", "grid"],
})
}
this.initCharts.hideLoading()
}
updateLineCharts(charts) {
if(this.charts[0].xAxisData[this.charts[0].xAxisData.length - 1] !== charts.xAxisData[charts.xAxisData.length - 1]) {
this.charts[0].xAxisData.shift()
this.charts[0].xAxisData.push(charts.xAxisData[charts.xAxisData.length - 1])
this.charts[0].yAxisData.shift()
this.charts[0].yAxisData.push(charts.yAxisData[charts.yAxisData.length - 1])
if(this.initCharts) {
this.initCharts.setOption({xAxis: [{data: this.charts[0].xAxisData}], series: [{data: this.charts[0].yAxisData}]}, false)
}
}
}
updateLinesCharts(charts) {
// console.log('charts', charts)
if(this.charts[0].xAxisData[this.charts[0].xAxisData.length - 1] !== charts.xAxisData[charts.xAxisData.length - 1]) {
this.charts[0].charts.forEach((el, idx) => {
el.value.pop()
el.value.unshift(charts.charts[idx].value[0])
})
this.charts[0].xAxisData.shift()
this.charts[0].xAxisData.push(charts.xAxisData[charts.xAxisData.length - 1])
if(this.initCharts) {
this.initCharts.setOption({xAxis: [{data: this.charts[0].xAxisData}],series: [{data: this.charts[0].charts}]}, false)
}
}
}
makePieCharts(charts) {
charts.chartId = this.id
this.charts = []
this.charts.push(charts)
const initCharts = echarts.init(this.el, null, sizeOptions(this.widthChart, this.heightChart), {renderer: 'canvas'})
initCharts.hideLoading()
initCharts.resize(sizeOptions(this.isMobile ? this.currentWidth : this.widthChart, this.heightChart))
initCharts.setOption(config(pieConfig(charts, this.id, windowConfig(this.isMobile, this.currentWidth))), true)
}
makeBarCharts(charts) {
charts.chartId = this.el.id
this.charts = []
this.charts.push(charts)
const initCharts = echarts.init(document.getElementById(`${this.el.id}`), null, {renderer: 'canvas'})
initCharts.hideLoading()
initCharts.resize(sizeOptions(this.isMobile ? this.currentWidth : this.widthChart, this.heightChart))
initCharts.setOption(config(barConfig(charts, this.el.id, windowConfig(this.isMobile, this.currentWidth))), true)
}
makeBarMountCharts(charts) {
// console.log('charts', charts)
charts.chartId = this.el.id
this.charts = []
this.charts.push(charts)
this.mountHeightChart = charts.height
const initCharts = echarts.init(document.getElementById(`${this.el.id}`), null, {renderer: 'canvas'})
initCharts.hideLoading()
initCharts.resize(sizeOptions(this.isMobile ? this.currentWidth : this.widthChart, charts.height))
initCharts.setOption(config(barTypesConfig(charts, this.el.id, windowConfig(this.isMobile, this.currentWidth))), true)
}
makeBarTypesCharts(charts) {
charts.chartId = this.el.id
this.charts = []
this.charts.push(charts)
console.log('charts - makeBarTypesCharts', charts.legendFilterData)
const initCharts = echarts.init(document.getElementById(`${this.el.id}`), null, {renderer: 'canvas'})
// this.heightChart = 270 + (charts?.yAxisData?.length * 27)
const countCharts = charts?.yAxisData?.length
const enableDataZoom = (countCharts * 35) + 200 > this.heightChart
initCharts.hideLoading()
initCharts.resize(sizeOptions(this.isMobile ? this.currentWidth : this.widthChart, this.heightChart))
initCharts.setOption(config(barTypesConfig(charts, this.el.id, windowConfig(this.isMobile, this.currentWidth, countCharts), enableDataZoom)), true)
let currentLegendCount = 0
if(charts.legendFilterData?.length > 0) {
charts.legendFilterData?.map((legend) => {
if(legend.id === this.el.id) {
if(this.el.id !== 'routes_types_bar_ml') {
charts.xAxisData.map((chart) => {
chart.charts.reduce((acc, el) => {
if(el.name === legend.filterData.name) {
currentLegendCount += Number(el.value)
} else {
return acc
}
}, 0)
})
} else {
charts.xAxisData.map((chart) => {
chart.reduce((acc, el) => {
if(el.charts.name === legend.filterData.name) {
currentLegendCount += Number(el.charts.value)
} else {
return acc
}
}, 0)
})
}
initCharts.dispatchAction({
type: !legend.filterData.value ? 'legendUnSelect' : 'legendSelect',
name: `${legend.filterData.name} ${currentLegendCount} id:${legend.id}`
})
}
currentLegendCount = 0
})
}
}
setMenuChart (data) {
if (data.groupByInfo.chartId) {
this.charts = [data.groupByInfo.data]
const [chart] = this.charts
chart.chartId = data.groupByInfo.chartId
chart.type = data.groupByInfo.type
chart.isOpenMenu = data.openMenuChart
const initCharts = echarts.init(document.getElementById(`${data.groupByInfo.chartId}`), null, {renderer: 'canvas'})
const countCharts = chart?.yAxisData?.length
const enableDataZoom = (chart?.yAxisData?.length * 35) + 200 > this.heightChart
initCharts.hideLoading()
initCharts.resize(sizeOptions(this.isMobile ? this.currentWidth : this.widthChart, data.groupByInfo.chartId.includes('mount') ? chart.height : this.heightChart))
if(data.groupByInfo.chartId.includes('pie')) {
initCharts.setOption(config(pieConfig(chart, data.groupByInfo.chartId, windowConfig(this.isMobile, this.currentWidth))), true)
} else if (data.groupByInfo.chartId.includes('types_bar')) {
initCharts.setOption(config(barTypesConfig(chart, data.groupByInfo.chartId, windowConfig(this.isMobile, this.currentWidth, countCharts), enableDataZoom)), true)
} else if (data.groupByInfo.chartId.includes('mount')) {
initCharts.setOption(config(barTypesConfig(chart, data.groupByInfo.chartId, windowConfig(this.isMobile, this.currentWidth))), true)
} else {
initCharts.setOption(config(barConfig(chart, data.groupByInfo.chartId, windowConfig(this.isMobile, this.currentWidth))), true)
}
}
}
downloadChart (data, updatedData) {
if (data.groupByInfo.chartId) {
this.charts = [data.groupByInfo.data]
const [chart] = this.charts
const countCharts = chart?.yAxisData?.length
const initCharts = echarts.init(document.getElementById(`${data.groupByInfo.chartId}`), null, {renderer: 'canvas'})
const enableDataZoom = (countCharts * 35) + 200 > data.updatedHeight
initCharts.hideLoading()
if(data.groupByInfo.chartId.includes('pie')) {
initCharts.setOption(config(pieConfig(chart, data.groupByInfo.chartId, windowConfig(this.isMobile, this.currentWidth))), true)
this.downloadImage(data.groupByInfo.title, this.prepareImage(initCharts))
} else if (data.groupByInfo.chartId.includes('types_bar')) {
console.log('this.charts', this.charts)
initCharts.resize(sizeOptions(this.isMobile ? this.currentWidth : this.widthChart, data.groupByInfo.updatedHeight))
initCharts.setOption(config(barTypesConfig(chart, data.groupByInfo.chartId, windowConfig(this.isMobile, data.groupByInfo.updatedHeight, countCharts), enableDataZoom)), true)
initCharts.on('finished', () => {
if(data.groupByInfo.isDownloadChart) {
console.log('data', data)
this.downloadImage(data.groupByInfo.title, this.prepareImage(initCharts))
data.groupByInfo.isDownloadChart = false
updatedData({action: 'downloadChart', groupByInfo: {...data.groupByInfo, updatedHeight: this.heightChart, data: this.charts}})
}
})
} else if (data.groupByInfo.chartId.includes('mount')) {
initCharts.setOption(config(barTypesConfig(chart, data.groupByInfo.chartId, windowConfig(this.isMobile, this.currentWidth))), true)
this.downloadImage(data.groupByInfo.title, this.prepareImage(initCharts))
} else {
initCharts.setOption(config(barConfig(chart, data.groupByInfo.chartId, windowConfig(this.isMobile, this.currentWidth))), true)
console.log('data.groupByInfo.title', data.groupByInfo.title)
this.downloadImage(data.groupByInfo.title, this.prepareImage(initCharts))
}
}
}
prepareImage(initCharts) {
const img = new Image()
img.src = initCharts.getDataURL({type: 'canvas'})
const myHeaders = new Headers()
const myRequest = new Request(img.src, {
method: "GET",
headers: myHeaders,
mode: "cors",
cache: "default",
})
return myRequest
}
async downloadImage(name, myRequest) {
try {
const response = await fetch(myRequest)
if (!response.ok) {
throw new Error("Network response was not OK")
}
const myBlob = await response.blob()
const imageURL = URL.createObjectURL(myBlob)
const link = document.createElement('a')
link.href = imageURL
if (link.href) {
const openedImage = window.open(link.href, '_blank')
console.log('openedImage', openedImage)
const linkImg = openedImage.document.createElement('a')
linkImg.href = link.href
linkImg.download = name
openedImage.document.body.appendChild(linkImg)
linkImg.click()
openedImage.document.body.removeChild(linkImg)
}
}
catch (error) {
console.error("Error:", error)
}
}
updateLegendFiltersData(params) {
const [chart] = this.charts
console.log('params - updateLegendFilterData', params)
console.log('chart - updateLegendFilterData', chart)
if (params.id === chart.chartId) {
if(!params.filterData.value) {
this.legendFilterData.push(params)
return this.legendFilterData
} else {
const updatedFiltersData = this.legendFilterData.filter((legend) => legend.filterData.name !== params.filterData.name)
this.legendFilterData = updatedFiltersData
return updatedFiltersData
}
}
}
onClickedBarCharts(updatedData) {
this.initCharts.on('click', (e) => {
// console.log('e', e)
const filterKey = e.data?.filterKey ? e.data?.filterKey : e.data?.filterKey
if(filterKey && e.componentType === 'series' && e.data?.type !== 'mount') {
// console.log('this.charts - nextLayoutChart', this.charts)
updatedData({id: this.id, action: 'nextLayoutChart', data: this.charts, prevGroupByData: e.data.groupByData, prevGroupByFilters: e.data.prevGroupByData, filterKey: filterKey, groupByKey: e.data.groupByKey, groupByValue: e.data.groupByValue ? e.data.groupByValue : null, type: e.data.type})
}
if(e.event?.target?.id && e.info?.typeEl === 'buttonPrevious' && e.componentType === 'graphic') {
// console.log('beforeLayoutChart')
// console.log('e.info', e.info)
updatedData({id: this.el.id, action: 'beforeLayoutChart', filterKey: e.event.target.id, groupByInfo: e.info})
}
if(e.event?.target?.id && e.info?.typeEl === 'menu' && e.componentType === 'graphic') {
e.info.mountHeightChart = this.mountHeightChart
// console.log('this.charts - menu', this.charts)
updatedData({action: 'openMenuChart', groupByInfo: {...e.info, data: this.charts}})
}
if(e.event?.target?.id && (e.info?.typeEl === 'download' || e.info?.typeEl === 'downloadTitle') && e.componentType === 'graphic') {
const fullHeight = e.info.countCharts * 35 + 200
e.info.isDownloadChart = true
updatedData({action: 'downloadChart', groupByInfo: {...e.info, updatedHeight: fullHeight, data: this.charts}})
}
if(e.event?.target?.id && e.info?.typeEl === 'link' && e.componentType === 'graphic') {
const id = e.event?.target?.parent?.id
const idx = id.slice(0, id.indexOf(':'))
const layoutGroupKey = id.slice((id.indexOf('_') + 1), id.indexOf('-')).trim()
const layoutGroupValue = id.slice(id.indexOf('-') + (id.includes('/') ? 3 : 2)).trim()
const isCurrentLayout = Object.values(e.info?.prevGroupByData)[0] === layoutGroupValue
if (isCurrentLayout && Object.keys(e.info.prevGroupByFilters).length > 1) { // Для выхода с первого вложенного слоя группировки
return false
} else {
// console.log('e.info', e.info)
const updatedPrevGroupByFilters = updatedGroupByData(idx, e.info.prevGroupByFilters, layoutGroupKey, layoutGroupValue)
e.info.prevGroupByFilters = updatedPrevGroupByFilters
e.info.prevGroupByData = {[layoutGroupKey]: layoutGroupValue}
updatedData({id: this.el.id, action: 'beforeLayoutChart', filterKey: e.info.filterKey ? e.info.filterKey : null, groupByInfo: e.info})
}
}
if(e.data?.groupByKey === groupByKeys.fiveLayout) {
openMachine(e.data)
}
})
this.initCharts.on('legendselectchanged', (params) => {
if(params.name.indexOf('id:') > 0) {
// Берём все данные производные из имени, так как это активный элемент легенды
const id = params.name.slice((params.name.indexOf('id:') + 3))
const nameIdxEnd = (params.name.indexOf('id:') - 1)
const namePrepare = params.name.slice(0, nameIdxEnd)
const name = namePrepare.slice(0, namePrepare.lastIndexOf(' '))
const updatedLegendFiltersData = this.updateLegendFiltersData({id: this.el.id, filterData: {name: name, value: params.selected[params.name]}})
updatedData({action: 'legendFiltersParams', data: updatedLegendFiltersData})
}
})
}
}
export default ECharts;

View File

@@ -0,0 +1,118 @@
const chartsSetup = {
main: [
{id: 'commun_pie', type: 'pie', maxHeight: '400px', order: 1},
{id: 'commun_pie_cdim', type: 'pie', maxHeight: '400px', order: 2},
{id: 'commun_pie_cdrp', type: 'pie', maxHeight: '400px', order: 3},
{id: 'commun_pie_mpt_types', type: 'pie', maxHeight: '400px', order: 4},
{id: 'commun_pie_gdsm_types', type: 'pie', maxHeight: '400px', order: 5},
{id: 'commun_pie_sm_pss_types', type: 'pie', maxHeight: '400px', order: 6},
{id: 'routes_pie', type: 'pie', maxHeight: '400px', order: 7},
{id: 'rors_pie', type: 'pie', maxHeight: '400px', order: 8},
{id: 'routes_bar', type: 'mainMl', maxHeight: '400px', order: 9},
{id: 'mount_client_type_bar', type: 'mount', height: '350', maxHeight: '400px', order: 10},
{id: 'mount_mounter_type_bar', type: 'mount', height: '500', maxHeight: '600px', order: 11},
],
types: [
{id: 'commun_types_bar', type: 'default', maxHeight: '400px', order: 1},
{id: 'commun_types_bar_cdim', type: 'without_first_layout', maxHeight: '400px', order: 2},
{id: 'commun_types_bar_cdrp', type: 'without_first_layout', maxHeight: '400px', order: 3},
{id: 'commun_types_bar_mpt_types', type: 'types', maxHeight: '400px', order: 4},
{id: 'commun_types_bar_gdsm_types', type: 'types', maxHeight: '400px', order: 5},
{id: 'commun_types_bar_sm_pss_types', type: 'types', maxHeight: '400px', order: 6},
{id: 'routes_types_bar', type: 'default', maxHeight: '400px', order: 7},
{id: 'rors_types_bar', type: 'default', maxHeight: '400px', order: 8},
{id: 'routes_types_bar_ml', type: 'ml', maxHeight: '380px', order: 9},
]
}
const colorList = (name) => {
switch(name) {
case 'На связи': return 'rgb(134, 212, 131, 1)'
case 'Есть МЛ, выходил на связь': return 'rgb(134, 212, 131, 1)'
case 'РОРС': return 'rgb(134, 212, 131, 1)'
case 'Не на связи': return 'rgb(255, 114, 114, 1)'
case 'Нет МЛ, нет связи': return 'rgb(165, 165, 165, 1)'
case 'Не РОРС': return 'rgb(255, 114, 114, 1)'
case 'Нет данных': return 'rgb(165, 165, 165, 1)'
case 'Есть МЛ, нет связи': return 'rgb(255, 114, 114, 1)'
case 'Нет МЛ, выходил на связь': return 'rgb(243, 161, 30, 1)'
}
}
const colorListBar = (type, name) => {
if(type === typesCharts.mainMl || typesCharts.typesMl) {
switch(name) {
case 'Обычные': return 'rgb(134, 212, 131, 1)'
case 'Аварийные': return 'rgb(255, 114, 114, 1)'
case 'Прочие': return 'rgb(196, 161, 201, 1)'
case 'Ремонтные': return 'rgb(228, 202, 163, 1)'
case 'Без выезда': return 'rgb(243, 161, 30, 1)'
case 'СПС без МЛ': return 'rgb(165, 165, 165, 1)'
}
}
}
const orderCharts = (name) => {
switch(name) {
case 'На связи': return 1
case 'Есть МЛ, выходил на связь': return 1
case 'РОРС': return 1
case 'Не на связи': return 2
case 'Не РОРС': return 2
case 'Есть МЛ, нет связи': return 2
case 'Нет МЛ, выходил на связь': return 3
case 'Нет МЛ, нет связи': return 5
case 'Нет данных': return 5
}
}
const title = (id, count, countWithoutMl = '') => {
switch(id) {
case 'commun_pie': return `Связь с СПС`
case 'commun_pie_cdim': return `Связь с СПС, ЦДИМ`
case 'commun_pie_cdrp': return `Связь с СПС, ЦДРП`
case 'commun_types_bar': return `Связь с СПС`
case 'commun_types_bar_cdim': return `Связь с СПС, ЦДИМ`
case 'commun_types_bar_cdrp': return `Связь с СПС, ЦДРП`
case 'commun_pie_mpt_types': return `Наличие связи с МРТ`
case 'commun_pie_gdsm_types': return `Наличие связи с ЖДСМ`
case 'commun_pie_sm_pss_types': return `Наличие связи с СМ`
case 'commun_types_bar_mpt_types': return `Наличие связи с МРТ`
case 'commun_types_bar_gdsm_types': return `Наличие связи с ЖДСМ`
case 'commun_types_bar_sm_pss_types': return `Наличие связи с СМ`
case 'rors_pie': return `РОРС`
case 'rors_types_bar': return `РОРС`
case 'routes_pie': return `Связь с СПС, МЛ`
case 'routes_types_bar': return `Связь с СПС, МЛ`
case 'routes_bar': return `Выданные МЛ`
case 'routes_types_bar_ml': return `Выданные МЛ`
case 'mount_client_type_bar': return `Кол-во монтажей по заказчикам и КР`
case 'mount_mounter_type_bar': return `Кол-во монтажей по исполнителям и КР`
default: return ''
}
}
const openMachine = (data) => {
const [currentEl] = data?.linkData?.filter((el) => el?.machine_type === data?.groupByData[data?.groupByKey])
window.open(`/html/askr_devices/analyze_device/${currentEl.device_number}`, '_blank')
}
const groupByKeys = {
firstLayout: 'org_type_name',
secondLayout: 'railway_name',
thirdLayout: 'org_name',
fourthLayout: 'type',
fiveLayout: 'machine_type',
}
const typesCharts = {
default: 'default',
without_first_layout: 'without_first_layout',
mpt: 'mpt_types',
gdsm: 'gdsm_types',
sm: 'sm_pss_types',
mainMl: 'mainMl',
typesMl: 'ml',
}
export {colorList, colorListBar, orderCharts, title, openMachine, groupByKeys, typesCharts, chartsSetup}

View File

@@ -0,0 +1,381 @@
const axisBarConfig = (typeChart, axisData, windowConfig, names = {}) => {
return typeChart === 'mount' || typeChart === 'mainMl' || typeChart === 'states' || typeChart === 'packLines' ? {
xAxis: {
type: 'category',
name: names.xAxis ? names.xAxis : '',
nameLocation: 'end',
nameGap: 35,
nameTextStyle: {
verticalAlign: 'top',
align: 'right',
padding: typeChart === 'packLines' ? windowConfig?.linesPack.nameXAxis.padding : [25, 40, 0, 0]
},
splitLine: {show: true},
axisLabel: {
show: true,
color: 'black',
fontSize: windowConfig?.bar?.xAxis?.label?.fontSize,
fontWeight: windowConfig?.bar?.xAxis?.label?.fontWeight,
interval: windowConfig?.bar?.xAxis?.label?.interval,
align: windowConfig?.bar?.xAxis?.label?.align,
hideOverlap: true,
},
axisTick: {
show: true,
interval: windowConfig?.bar?.xAxis?.axisTick?.interval,
lineStyle: {
color: 'black',
}
},
data: axisData
},
yAxis: {
type: 'value',
name: names.xAxis ? names.xAxis : '',
// nameLocation: 'end',
// nameGap: 35,
// nameTextStyle: {
// // verticalAlign: 'top',
// // align: 'right',
// padding: [125, 0, 0, 150]
// },
splitLine: {show: true},
axisLine: {
show: true,
lineStyle: {
// dashOffset: 10,
width: 1,
color: 'white',
shadowColor: 'black',
shadowOffsetX: -2
},
},
axisLabel: {
show: true,
color: 'black',
},
axisTick: {
show: true,
lineStyle: {
color: 'black'
}
},
},
} : {
xAxis: {
type: 'value',
name: names.xAxis ? names.xAxis : '',
splitLine: {show: true},
axisLabel: {
show: true,
color: 'black',
fontSize: windowConfig?.bar?.xAxis?.label?.fontSize,
fontWeight: windowConfig?.bar?.xAxis?.label?.fontWeight,
interval: windowConfig?.bar?.xAxis?.label?.interval,
align: windowConfig?.bar?.xAxis?.label?.align,
hideOverlap: true,
},
axisLine: {
show: true,
lineStyle: {
color: 'black',
}
},
axisTick: {
show: true,
interval: windowConfig?.bar?.xAxis?.axisTick?.interval,
lineStyle: {
color: 'black'
}
},
minInterval: 1,
},
yAxis: {
type: 'category',
name: names.yAxis ? names.yAxis : '',
axisLine: {
show: true,
lineStyle: {
// dashOffset: 10,
width: 1,
color: 'white',
shadowColor: 'black',
shadowOffsetX: -2
},
},
// axisLabel: {
// formatter: (yAxisData) => {
// // return yAxisData.map((el) => {
// // return `${el?.name} ${el?.value}`
// // })
// }
// },
axisLabel: {
show: true,
width: windowConfig?.bar?.yAxis?.label?.width,
color: 'black',
fontSize: windowConfig?.bar?.yAxis?.label?.fontSize,
fontWeight: windowConfig?.bar?.yAxis?.label?.fontWeight,
overflow: 'break',
},
axisTick: {
show: true,
lineStyle: {
color: 'black'
}
},
// fontSize: 10,
// offset: -7,
data: axisData,
splitLine: {
show: true
},
// position: windowConfig?.bar?.yAxis?.label?.position,
// offset: windowConfig?.bar?.yAxis?.label?.offset,
zLevel: 3,
// z: 99
},
}
}
const windowConfig = (isMobile, currentWidth, countCharts) => {
const scrollbarParams = (countCharts) => {
if(15 < countCharts && countCharts < 40) return {
start: 85, minSpan: 5
}
if(40 < countCharts && countCharts < 70) return {
start: 90, minSpan: 5
}
if(70 < countCharts && countCharts < 100) return {
start: 95, minSpan: 5
}
if(100 < countCharts) return {
start: 99, minSpan: 3
}
return {start: 70, minSpan: 5}
}
if(isMobile) return {
title: {
width: currentWidth - 90,
top: 3,
left: 'left',
fontSize: 11,
},
backgroundColor: '#FFF',
pie: {
legend: {
itemWidth: 15,
itemHeight: 15,
itemGap: 5,
fontSize: 10
},
graphic: {
period: {
top: 10,
right: 40,
fontSize: 10
},
count: {
top: 'center',
left: 'center',
fontSize: 20
},
}
},
linesPack: {
legend: {
bottom: 0,
itemWidth: 25,
itemHeight: 14,
itemGap: 7,
fontSize: 11
},
nameXAxis: {
padding: [110, 40, 0, 0],
}
},
bar: {
legend: {
bottom: 0,
itemWidth: 10,
itemHeight: 10,
itemGap: 5,
fontSize: 10
},
grid: {
width: '85%',
top: 40
},
yAxis: {
label: {width: 40, fontWeight: 'bold', fontSize: 9, position: 'left', offset: -20}
},
xAxis: {
label: {fontSize: 10, fontWeight: 'bold', interval: 1, align: 'center'},
axisTick: {interval: 1}
},
dataZoom: {
top: 40,
start: scrollbarParams(countCharts).start,
minSpan: scrollbarParams(countCharts).minSpan,
},
graphic: {
period: {
top: 8,
right: 50,
fontSize: 10
},
menu: {
top: 5,
right: 0
},
container: {
top: 27,
right: 0
},
rect: {
top: 8,
right: 10
},
download: {
top: 18,
right: 70
},
downloadTitle: {
top: 23,
right: 20
},
back: {
top: 5,
right: 20,
},
link: {
top: 23,
left: 7,
}
}
}
}
if(!isMobile) return {
title: {
width: 'auto',
top: 3,
left: 'left',
fontSize: 16,
},
backgroundColor: '#FFF',
line: {
legend: {
bottom: 0,
itemWidth: 25,
itemHeight: 14,
itemGap: 7,
fontSize: 11
},
},
linesPack: {
legend: {
bottom: 0,
itemWidth: 25,
itemHeight: 14,
itemGap: 7,
fontSize: 11
},
nameXAxis: {
padding: [110, 40, 0, 0],
}
},
pie: {
legend: {
itemWidth: 15,
itemHeight: 15,
itemGap: 10,
fontSize: 12
},
graphic: {
period: {
top: 10,
right: 30,
fontSize: 12
},
count: {
top: 'center',
left: 'center',
fontSize: 27
},
}
},
bar: {
legend: {
bottom: 10,
itemWidth: 25,
itemHeight: 14,
itemGap: 10,
fontSize: 12
},
grid: {
width: '90%',
top: 50
},
yAxis: {
label: {fontSize: 13, width: 110, fontWeight: 'normal', position: 'left', offset: 0}
},
xAxis: {
label: {fontSize: 10, fontWeight: 'normal', interval: 1, align: 'center'},
axisTick: {interval: 0}
},
dataZoom: {
top: 50,
start: scrollbarParams(countCharts).start,
minSpan: scrollbarParams(countCharts).minSpan,
},
graphic: {
period: {
top: 10,
right: 60,
fontSize: 12
},
menu: {
top: 7,
right: 5
},
container: {
top: 27,
right: 0
},
rect: {
top: 8,
right: 10
},
download: {
top: 18,
right: 70
},
downloadTitle: {
top: 23,
right: 20
},
back: {
top: 7,
right: 30,
},
link: {
top: 28,
left: 7,
}
}
}
}
}
const sizeOptions = (width = 600, height = 400) => {
return {
width: width,
height: height
}
}
export {axisBarConfig, windowConfig, sizeOptions}

View File

@@ -0,0 +1,63 @@
export class EventStore {
constructor(params) {
this.store = {}
this.params = params
}
storeGetData(data, updateFunc) {
if (data.onlyGet) {
return this.store[data.key] || null
}
return updateFunc({
newData: data.newData,
key: data.key,
data: this.store[data.key] || null
})
}
storeSetData(data) {
return this.store[data.key] = data.value
}
storeRemoveData(data) {
return this.store[data.key] = null
}
storeCreateOrGetData(data, updateFunc) {
if (this.store[data.key]) {
return updateFunc({
newData: data.newData,
key: data.key,
data: this.store[data.key]
})
}
if (!this.store[data.key]) {
this.store[data.key] = data.defaultValue
return updateFunc({
newData: data.newData,
key: data.key,
data: data.defaultValue
})
}
}
localStoreGetData(data, updateFunc) {
if (data.onlyGet) {
return JSON.parse(localStorage.getItem(data.key)) || null
}
return updateFunc({
newData: data.newData,
key: data.key,
data: JSON.parse(localStorage.getItem(data.key)) || null
})
}
localStoreSetData(data) {
return localStorage.setItem(data.key, JSON.stringify(data.value))
}
localStoreRemoveData(data) {
return localStorage.removeItem(data.key)
}
}

View File

@@ -0,0 +1,47 @@
export class PushAfterTimeout {
constructor(id, delay = 1000, iconClass = '') {
this.el = document.getElementById(id)
this.timer = null
this.input = this.el.querySelector('input')
this.iconClass = iconClass
this.makeIconWrapper(this.iconClass)
this.addLoader()
this.timer && clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.removeIcon()
this.input.setAttribute("status", "success")
this.addSuccess(delay)
}, delay)
}
addLoader() {
this.wrapper.innerHTML = `
<div role="status">
<svg aria-hidden="true" class="w-4 h-4 text-gray-100 animate-spin dark:text-gray-600 fill-gray-300" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
</svg>
</div>
`
}
addSuccess(delay) {
this.wrapper.innerHTML = `<i class="ri-checkbox-circle-line text-green-500 text-lg"></i>`
this.timer = setTimeout(() => {
this.removeIcon();
}, delay);
}
removeIcon() {
this.wrapper.innerHTML = "";
}
makeIconWrapper(iconClass) {
this.wrapper = document.createElement('div');
this.el.appendChild(this.wrapper);
this.wrapper.classList = `absolute flex items-center justify-center right-[10px] top-[50%] ${iconClass}`;
// this.wrapper.style.right = "10px";
// this.wrapper.style.top = "50%";
this.wrapper.style.transform = "translateY(-50%)";
}
}

View File

@@ -0,0 +1,39 @@
import { createStore, createLogger } from 'vuex';
import { store as main } from '@/store/modules/main';
import { store as machines } from '@/store/modules/machines';
import { store as finder_packs } from '@/store/modules/finder_packs';
import { store as add_users } from '@/store/modules/add_users';
import { store as layout } from '@/store/modules/layout';
import { store as auth } from '@/store/modules/auth';
import { store as last_packs } from '@/store/modules/last_packs';
import { store as last_packs_num } from '@/store/modules/last_packs_num';
import { store as packs } from '@/store/modules/packs';
import { store as services } from '@/store/modules/services';
import { store as logger } from '@/store/modules/logger';
let debug = process.env.NODE_ENV !== 'production';
debug = false;
const plugins = debug ? [createLogger({})] : [];
export const store = createStore({
plugins,
modules: {
main,
machines,
finder_packs,
add_users,
layout,
auth,
last_packs,
last_packs_num,
packs,
services,
logger,
},
});
export function useStore() {
return store;
}

View File

@@ -0,0 +1,588 @@
const columns = [
{
title: "ID",
field: "id",
width: 150
},
{
title: "ФИО",
field: "fio",
width: 150
},
{
title: "Логин",
field: "login",
width: 150
},
{
title: "Действия",
field: "actions",
log: true,
render: `
<div class="flex items-center gap-2">
<a target="_blank" href="/html/admin/manage/{{ id }}?prev_page=/\?current_mode=users"><i class="cursor-pointer ri-pencil-line text-slate-700"></i></a>
</div>
`
}
]
const users = {
columns: [
{
download: false,
field: "id",
headerFilter: "input",
headerFilterPlaceholder: "ID",
minWidth: 80,
print: false,
responsive: 0,
sorter: "string",
title: "ID",
vertAlign: "middle",
width: 150
},
{
download: false,
field: "fio",
headerFilter: "input",
headerFilterPlaceholder: "ФИО",
minWidth: 80,
print: false,
responsive: 0,
sorter: "string",
title: "ФИО",
vertAlign: "middle",
width: 150
},
{
download: false,
field: "login",
headerFilter: "input",
headerFilterPlaceholder: "Логин",
minWidth: 80,
print: false,
responsive: 0,
sorter: "string",
title: "Логин",
vertAlign: "middle",
width: 150
},
{
download: false,
field: "actions",
headerFilter: "input",
headerFilterPlaceholder: "Действия",
log: true,
minWidth: 80,
print: false,
render: "\n <div class=\"flex items-center gap-2\">\n <a target=\"_blank\" href=\"/html/admin/manage/{{ id }}?prev_page=/?current_mode=users\"><i class=\"cursor-pointer ri-pencil-line text-slate-700\"></i></a>\n </div>\n ",
responsive: 0,
sorter: "string",
title: "Действия",
vertAlign: "middle"
}
],
data: [
{
adminGroup: {id: 1, name: "ОРИС"},
adminGroupId: 1,
fio: "Шлыков Сергей Николаевич",
firstName: "Сергей",
id: 1,
insertedAt: "2022-03-10 14:51:07",
lastName: "Шлыков",
locale: "ru-RU",
login: "superuser",
notes: "### cHdkIFN1cGVyc2VjcmV0 ###",
passwordHash: "$2b$12$vZkno0sNHC1mlHYqXHyG7Ow74WfTY5BmgyGnN3G4l.QUygY2MDfA6",
secondName: "Николаевич",
updatedAt: "2024-01-15 13:09:35"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "User MNTR ",
firstName: "MNTR",
id: 3,
insertedAt: "2022-03-14 07:12:51",
lastName: "User",
login: "mntr",
notes: "### cHdkIFZuaWlUZXN0 ###",
passwordHash: "$2b$12$fWS34Bkerqo8rakLvOX1b.QmlhCWZviQoSPDEBSX1QOiKxC3hY6Aq",
updatedAt: "2024-01-15 13:09:35"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Романов Александр ",
firstName: "Александр",
id: 17,
insertedAt: "2022-05-24 15:13:00",
lastName: "Романов",
login: "RomanovAV",
notes: "init pwd: 5Nres551### cHdkIDVOcmVzNTUx ###",
passwordHash: "$2b$12$jFDdtPbWQruN0vNmCkEl3uzaWLZxBBYXQC04az6Caj1prQaAz1j8m",
updatedAt: "2024-01-15 13:09:35"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Шмаков Александр Сергеевич",
firstName: "Александр",
id: 18,
insertedAt: "2022-05-24 15:13:00",
lastName: "Шмаков",
login: "smakov",
notes: "init pwd: 9ofVZTC6### cHdkIDlvZlZaVEM2 ###",
passwordHash: "$2b$12$G./jAcqdse6GWEX5x58hvepFAOeBobvnO7EOWjQeZxLaS4Odqicpe",
secondName: "Сергеевич",
updatedAt: "2024-01-15 13:09:36"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Моисеев Сергей Александрович",
firstName: "Сергей",
id: 19,
insertedAt: "2022-05-24 15:13:00",
lastName: "Моисеев",
login: "Moiseyev",
notes: "init pwd: tVWRFS61### cHdkIHRWV1JGUzYx ###",
passwordHash: "$2b$12$KyUjOK8C1a/EqEmnN7gJYeAwG.4kWXuI2OU1UKXtfSojuIZcnBb6e",
secondName: "Александрович",
updatedAt: "2024-01-15 13:09:36"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Карнаухов Александр Иванович",
firstName: "Александр",
id: 20,
insertedAt: "2022-05-24 15:13:00",
lastName: "Карнаухов",
login: "KarnauhovAI",
notes: "init pwd: TwgTmHGH### cHdkIFR3Z1RtSEdI ###",
passwordHash: "$2b$12$33PWjESsfh7gaDYtddv3EutPFi5bOmqSZBDTp4loiAjLpvhWgj.Hm",
secondName: "Иванович",
updatedAt: "2024-01-15 13:09:36"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Бобров Виталий Владимирович",
firstName: "Виталий",
id: 21,
insertedAt: "2022-05-24 15:13:01",
lastName: "Бобров",
login: "Bobrover",
notes: "init pwd: xOUM96vI### cHdkIHhPVU05NnZJ ###",
passwordHash: "$2b$12$hEameXqEy.FfGJYO7YXnKunud0Wn2ba4GyIaq4WevdA/XmR/iOaM.",
secondName: "Владимирович",
updatedAt: "2024-01-15 13:09:36"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Бурак Денис Александрович",
firstName: "Денис",
id: 22,
insertedAt: "2022-05-24 15:13:01",
lastName: "Бурак",
login: "BurakD",
notes: "init pwd: fzqRutIC### cHdkIGZ6cVJ1dElD ###",
passwordHash: "$2b$12$zUISbezh9hVBAW/U32yWjOCxFYS5vPGPGO92h8P/0TLLYymWqBUGW",
secondName: "Александрович",
updatedAt: "2024-01-15 13:09:37"
},
{
adminGroup: {id: 5, name: "TESTERS"},
adminGroupId: 5,
fio: "User Test ",
firstName: "Test",
id: 2,
insertedAt: "2022-03-14 07:12:51",
lastName: "User",
login: "testuser",
notes: "### cHdkIE1lZ2FwYXNzd29yZA== ###",
passwordHash: "$2b$12$zE14rLaUBdVco9RXE9Hepe0mgkbVIYGa4CwiNrZHbHI0SVCNUVQhW",
updatedAt: "2024-01-15 13:50:49"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Ярцев Андрей Васильевич",
firstName: "Андрей",
id: 16,
insertedAt: "2022-05-24 15:12:59",
lastName: "Ярцев",
login: "Yartsev.Andrey",
notes: "init pwd: iWA8qWsv### cHdkIGlXQThxV3N2 ###",
passwordHash: "$2b$12$EAhw4.dUdnSlki0tiFyKI.9HkzfviOYCxqBj/63CNU68rg0idna9i",
secondName: "Васильевич",
updatedAt: "2024-01-15 13:53:07"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Данилов Александр Николаевич",
firstName: "Александр",
id: 23,
insertedAt: "2022-05-24 15:13:01",
lastName: "Данилов",
login: "Danilov.Aleksandr",
notes: "init pwd: hEVonP0H### cHdkIGhFVm9uUDBI ###",
passwordHash: "$2b$12$B1RtRKd7g6fbqYhXfZ9i/e0cVG9CQzI64D37XplU8yUgHz96EX1HO",
secondName: "Николаевич",
updatedAt: "2024-01-15 13:09:37"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Буторин Евгений Львович",
firstName: "Евгений",
id: 24,
insertedAt: "2022-05-24 15:13:01",
lastName: "Буторин",
login: "butorin",
notes: "init pwd: wLsqjX+H### cHdkIHdMc3FqWCtI ###",
passwordHash: "$2b$12$.MMVjOpL1ol4jR6n/b8S9exedOszcsxPbShcWdYOEggtGFaDjnJuG",
secondName: "Львович",
updatedAt: "2024-01-15 13:09:37"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Иванов Дмитрий ",
firstName: "Дмитрий",
id: 25,
insertedAt: "2022-05-24 15:13:01",
lastName: "Иванов",
login: "ivanovd",
notes: "init pwd: OzaruSHc### cHdkIE96YXJ1U0hj ###",
passwordHash: "$2b$12$GBTNgWIRGtlUlfjneY8CCuwkXI5bYnBvjtWRz/u/MQx0fi9VKXfHW",
updatedAt: "2024-01-15 13:09:37"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Ильичев Михаил Сергеевич",
firstName: "Михаил",
id: 33,
insertedAt: "2022-05-24 15:13:03",
lastName: "Ильичев",
login: "ilyichev.mikhail",
notes: "init pwd: NxLW6Xmt### cHdkIE54TFc2WG10 ###",
passwordHash: "$2b$12$P8oiAcMG5m0XvuKLy4Dtve8hw7xnczVDnFGwo0eP7HkEKijQyNOWG",
secondName: "Сергеевич",
updatedAt: "2024-01-15 13:09:37"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Карелин Артём Дмитриевич",
firstName: "Артём",
id: 34,
insertedAt: "2022-05-24 15:13:03",
lastName: "Карелин",
login: "karelin.artem",
notes: "init pwd: +KD43KXa### cHdkICtLRDQzS1hh ###",
passwordHash: "$2b$12$Z6nhsAQLtG5qDndjgHFhzu/AP9oGWEejtWznjua2T/SRLSutLNy.y",
secondName: "Дмитриевич",
updatedAt: "2024-01-15 13:09:38"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Машкова Юлия Сергеевна",
firstName: "Юлия",
id: 35,
insertedAt: "2022-05-24 15:13:04",
lastName: "Машкова",
login: "mashkova",
notes: "init pwd: fCD8a2ub### cHdkIGZDRDhhMnVi ###",
passwordHash: "$2b$12$D61pVAGUHUXaYF761n0vTuHE7XpO.KG.4FcpfHm3y7IffZuR4OPH2",
secondName: "Сергеевна",
updatedAt: "2024-01-15 13:09:38"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Головко Борис Борисович",
firstName: "Борис",
id: 26,
insertedAt: "2022-05-24 15:13:02",
lastName: "Головко",
login: "GolovkoBB",
notes: "init pwd: T8mlO00M### cHdkIFQ4bWxPMDBN ###",
passwordHash: "$2b$12$BSWzgTFpgit9Vz0hKtGqUuBa.bn6SL.qi7ra8LoYo0hp6AR5NfO1m",
secondName: "Борисович",
updatedAt: "2024-01-15 13:09:38"
},
{
adminGroup: {id: 1, name: "ОРИС"},
adminGroupId: 1,
fio: "Перевязкин Александр Александрович",
firstName: "Александр",
id: 36,
insertedAt: "2022-05-24 15:13:04",
lastName: "Перевязкин",
login: "Perevyazkin.Alexandr",
notes: "init pwd: RCZZfFGe### cHdkIFJDWlpmRkdl ###",
passwordHash: "$2b$12$t0VnSxU8Rirbu/50R3JXTeK4fwefglZO8kNC98yDO1FocnUCVTnsW",
secondName: "Александрович",
updatedAt: "2024-01-15 13:09:39"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Гордейчук Алексей Владимирович",
firstName: "Алексей",
id: 27,
insertedAt: "2022-05-24 15:13:02",
lastName: "Гордейчук",
login: "GordeychukAV",
notes: "init pwd: b9fmKp1y### cHdkIGI5Zm1LcDF5 ###",
passwordHash: "$2b$12$mg7xtqxgDgFovnmqfRy5reb0pL1.XWuXxQzcr6L1OCpAtWRXFrsBW",
secondName: "Владимирович",
updatedAt: "2024-01-15 13:09:39"
},
{
adminGroup: {id: 1, name: "ОРИС"},
adminGroupId: 1,
fio: "Котов Кирилл Юрьевич",
firstName: "Кирилл",
id: 28,
insertedAt: "2022-05-24 15:13:02",
lastName: "Котов",
login: "kotov",
notes: "init pwd: 9la1NqNC### cHdkIDlsYTFOcU5D ###",
passwordHash: "$2b$12$yIgJUi47b8NcPLVMaTh2SezTbSdXe7kbMWlNd92p/JxxeyK6PK5vm",
secondName: "Юрьевич",
updatedAt: "2024-01-15 13:09:39"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Шульгин Алексей Викторович",
firstName: "Алексей",
id: 29,
insertedAt: "2022-05-24 15:13:02",
lastName: "Шульгин",
login: "Schulgin.Aleksey",
notes: "init pwd: MULd4uDy### cHdkIE1VTGQ0dUR5 ###",
passwordHash: "$2b$12$rez8rGGiHvHgkxIffxCyp.Ahne1rfgg5khXKLrwmNvwDVrpal0v1a",
secondName: "Викторович",
updatedAt: "2024-01-15 13:09:39"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Егоров Максим Валерьевич",
firstName: "Максим",
id: 30,
insertedAt: "2022-05-24 15:13:03",
lastName: "Егоров",
login: "Egorov.Maxim",
notes: "init pwd: uV4ZwuGv### cHdkIHVWNFp3dUd2 ###",
passwordHash: "$2b$12$vq32dLtEBAOcvME6h5Bb0.sHGa1DryRwoLcCIyzfcJsisZZ.MvyEi",
secondName: "Валерьевич",
updatedAt: "2024-01-15 13:09:39"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Скляров Алексей Николаевич",
firstName: "Алексей",
id: 31,
insertedAt: "2022-05-24 15:13:03",
lastName: "Скляров",
login: "Sklyarov.Aleksey",
notes: "init pwd: RpE5Hy+3### cHdkIFJwRTVIeSsz ###",
passwordHash: "$2b$12$Y35oJ8/99n5sryYOoNTu5eYV77fIvgIA2OFkQ26Nw1b3FlXepUx9i",
secondName: "Николаевич",
updatedAt: "2024-01-15 13:09:40"
},
{
adminGroup: {id: 1, name: "ОРИС"},
adminGroupId: 1,
fio: "Test Admin ",
firstName: "Admin",
id: 47,
insertedAt: "2023-03-02 08:06:05",
lastName: "Test",
login: "ta112",
notes: "### cHdkIFlROEJ1TG5n ###",
passwordHash: "$2b$12$6nuL1xC72rUt30YyGqqr6.5yNQ2vvvQlj2ZJeLrKJ9evqqDE75zAC",
phone: "444",
updatedAt: "2024-01-15 13:09:43"
},
{
adminGroup: {id: 8, name: "ru_watcher"},
adminGroupId: 8,
fio: "Packs Manger ",
firstName: "Manger",
id: 48,
insertedAt: "2023-03-15 11:25:23",
lastName: "Packs",
login: "packs_manger",
notes: "### cHdkIFBhY2sxc3Nz ###",
passwordHash: "$2b$12$rW9uGhrLum.2pTBYgtumAuy8xCqI9iFiUJ4cprGywYLUKbt/XNW4S",
updatedAt: "2024-01-15 13:09:43"
},
{
adminGroup: {id: 1, name: "ОРИС"},
adminGroupId: 1,
fio: "Иванова Александра Сергеевна",
firstName: "Александра",
id: 37,
insertedAt: "2022-05-25 07:16:20",
lastName: "Иванова",
login: "sascha",
notes: "init pwd: Lenovoinwin### cHdkIExlbm92b2lud2lu ###",
passwordHash: "$2b$12$xLWXBi0AYp.QOohHGaURYOp0cuow5eTT24oh1HNXI.ZcOCtzvsVUG",
secondName: "Сергеевна",
updatedAt: "2024-01-15 13:09:40"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "test_admin_del d d",
firstName: "d",
id: 49,
insertedAt: "2023-03-27 07:31:25",
lastName: "test_admin_del",
login: "test_admin_del",
notes: "### cHdkIHpNSjJ0bVhr ###",
passwordHash: "$2b$12$Z0tEFtGnI1iO9kjFXYpIXOwYldhRlNkIcei002qzAy7HVJa7m/I.e",
secondName: "d",
updatedAt: "2024-01-15 13:09:43"
},
{
adminGroup: {id: 9, name: "kudt_watcher"},
adminGroupId: 9,
fio: "KUDT Watcher ",
firstName: "Watcher",
id: 50,
insertedAt: "2023-03-28 07:55:01",
lastName: "KUDT",
login: "KUDT",
notes: "### cHdkIFdhdGNoS3VkdA== ###",
passwordHash: "$2b$12$r47Teqz/KONX3fx2nVEHtup.NRHynjDqobBmNCV7t7fALQwvFXE.a",
updatedAt: "2024-01-15 13:09:43"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Акулов Егор Пользователь",
firstName: "Егор",
id: 52,
insertedAt: "2023-07-31 12:17:56",
lastName: "Акулов",
login: "AkulEgor",
notes: "### cHdkIDEyQWt1bDEz ###",
passwordHash: "$2b$12$37YN2TmjAZjE9dmZXycI4OBKMGVUg.vLcFKwx0sS7mn96hB8V1Iai",
secondName: "Пользователь",
updatedAt: "2024-01-15 13:09:44"
},
{
adminGroup: {id: 12, name: "free_packs_watcher"},
adminGroupId: 12,
fio: "Кузнецов Александр Юрьевич",
firstName: "Александр",
id: 51,
insertedAt: "2023-04-25 09:25:08",
lastName: "Кузнецов",
login: "KuznetsovAY",
notes: "### cHdkIEt1ejU2N2F5 ###",
passwordHash: "$2b$12$WF7iMKiDxKcWShCGTgWnNu4b1HHCxFIP7YPR60LJlwc/BCBwh575q",
secondName: "Юрьевич",
updatedAt: "2024-01-15 13:09:44"
},
{
adminGroup: {id: 8, name: "ru_watcher"},
adminGroupId: 8,
fio: "Ru Watcher ",
firstName: "Watcher",
id: 46,
insertedAt: "2023-03-02 06:31:45",
lastName: "Ru",
login: "RuWatcher",
notes: "### cHdkIEpiWlBqZE05 ###",
passwordHash: "$2b$12$naoRmmTeR6WYy6UZWHxFBuhwO3s52/p/MCkZlP.KgWsXrk8sIUz4W",
phone: "13",
updatedAt: "2024-01-15 13:09:40"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Ананьев Виталий Сергеевич",
firstName: "Виталий",
id: 38,
insertedAt: "2022-05-31 06:34:02",
lastName: "Ананьев",
login: "ananev.vitaly",
notes: "### cHdkIHNKVWl4U0Zh ###",
passwordHash: "$2b$12$JLiIog4a9z2dt8xFP.s/.OBcLHo4cDJgLpCjCxHVQa1rAjmNCIOZG",
secondName: "Сергеевич",
updatedAt: "2024-01-15 13:09:41"
},
{
adminGroup: {id: 15, name: "Монтаж"},
adminGroupId: 15,
fio: "Карпенко Иван Васильевич",
firstName: "Иван",
id: 39,
insertedAt: "2022-06-02 09:28:49",
lastName: "Карпенко",
login: "karpenko.ivan",
notes: "### cHdkIDdaNXM3Tldq ###",
passwordHash: "$2b$12$she6mqurkOULGSd.PzKxeeMppEDjEMhXPdjuH.zncLkVZEKw0iWT2",
secondName: "Васильевич",
updatedAt: "2024-01-15 13:09:41"
},
{
adminGroup: {id: 5, name: "TESTERS"},
adminGroupId: 5,
email: "HasyanovMark@no-mail.xx",
fio: "Хасянов Марк х",
firstName: "Марк",
id: 53,
insertedAt: "2023-09-22 10:27:21",
lastName: "Хасянов",
login: "HasyanovM",
notes: "### cHdkIG5ZdzE4dTk2 ###",
passwordHash: "$2b$12$Pf2T.EKEWXhE2XBpridtOOuKU6NO5evZoGKGMwg4u1wEtCa1hNxUK",
secondName: "х",
updatedAt: "2024-01-15 13:09:44"
},
{
adminGroup: {id: 6, name: "ПКФ"},
adminGroupId: 6,
fio: "Технологии ПКФ ",
firstName: "ПКФ",
id: 40,
insertedAt: "2022-06-02 11:53:48",
lastName: "Технологии",
login: "pkf_teck",
notes: "### cHdkIEFCWDIwSXU3 ###",
passwordHash: "$2b$12$JFef9mZEyWmPENZM2VTTWePVfGogTAgnu4dFYHNMDhY6RPWb7zRXG",
updatedAt: "2024-01-15 13:09:41"
},
{
adminGroup: {id: 7, name: "ВНИКТ"},
adminGroupId: 7,
fio: "Ушанов Андрей ",
firstName: "Андрей",
id: 41,
insertedAt: "2022-06-06 10:02:15",
lastName: "Ушанов",
login: "AUshanov",
notes: "### cHdkIFBPU2t1anox ###",
passwordHash: "$2b$12$StGexN61k2AGoBnCaC2GSeoYrVRGplRoeOSwXXpj3OJiHoLq9zKCi",
updatedAt: "2024-01-15 13:09:41"
},
],
height: "200px"
}
export { columns, users }

Some files were not shown because too many files have changed in this diff Show More