Initial commit

This commit is contained in:
2024-03-05 11:36:21 +03:00
commit bf2f060b94
212 changed files with 100448 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
<script>
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: ''
},
isCheck: {
type: Boolean,
default: false
},
position: {
type: String,
default: 'row'
},
labelClass: {
type: String,
default: ''
},
switchClass: {
type: String,
default: ''
},
},
emits: ['switched'],
data() {
return {
isChecked: false
}
},
computed: {
},
watch: {
isCheck: function () {
this.isChecked = !this.isChecked
},
},
mounted () {
},
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.$emit('switched', e.target.checked)
}
}
}
</script>
<template>
<label :class="`${position === 'row' ? 'flex-row' : 'flex-col'} ${labelClass} relative flex items-center cursor-pointer`">
<input
v-bind="$attrs"
class="input sr-only peer/checkbox"
type="checkbox"
:checked="isCheck"
@change="setChecked"
>
<span
v-if="position === 'col'"
class="block peer-checked/checkbox:hidden ml-0 text-xs font-medium text-gray-900 dark:text-gray-300"
> {{ firstTitle }}</span>
<span
v-if="position === 'col'"
class="hidden peer-checked/checkbox:block ml-0 text-xs font-medium text-gray-900 dark:text-gray-300"
> {{ secondTitle }} </span>
<div
:class="`${switchClass} relative 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: !isCheck ? firstColor : secondColor}"
/>
<span
v-if="position === 'row'"
class="block peer-checked/checkbox:hidden ml-2 text-sm font-medium text-gray-900 dark:text-gray-300"
> {{ firstTitle }}</span>
<span
v-if="position === 'row'"
class="hidden peer-checked/checkbox:block ml-2 text-sm font-medium text-gray-900 dark:text-gray-300"
> {{ secondTitle }} </span>
</label>
</template>

View File

@@ -0,0 +1,59 @@
<script>
export default {
name: 'Input',
props: {
id: {
default: 'input',
type: String
},
type: {
default: 'text',
type: String
},
name: {
default: '',
type: String
},
inputClass: {
default: '',
type: String
},
placeholder: {
default: '',
type: String
},
required: {
default: false,
type: Boolean
},
value: {
default: '',
type: String
},
onChange: {
default: () => {},
type: Function
}
},
computed: {
},
methods: {
setValue: function (e) {
this.onChange({key: this.name, value: e.target.value})
}
}
}
</script>
<template>
<input
:id="id"
:type="type"
:name="name"
:class="`flex w-full rounded-lg border-zinc-300 py-1 px-2 text-zinc-900 focus:outline-none focus:ring-4 text-sm sm:leading-6 border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5 ${inputClass}`"
:placeholder="placeholder"
:required="required"
:value="value"
:on:change="setValue"
>
</template>

View File

@@ -0,0 +1,36 @@
<script>
import {mapMutations, mapGetters} from 'vuex'
export default {
name: 'PageHeader',
computed: {
...mapGetters('proxy', ['selectedSite']),
},
methods: {
...mapMutations('proxy', ['setIsSaveData', 'setIsDeleteData']),
saveData: function () {
console.log('saveData')
return this.setIsSaveData(true)
},
}
}
</script>
<template>
<div class="flex">
<a
href="/#"
class="text-white block w-fit bg-slate-700 hover:bg-blue-600 focus:outline-none focus:ring-4 focus:ring-blue-300 font-medium rounded-full text-sm text-center px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
<i class="ri-reply-line" />
На главную
</a>
<button
:class="`w-fit text-white font-medium rounded-full text-sm px-5 py-2.5 text-center me-2 mx-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 ${!selectedSite ? 'bg-blue-200' : 'bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300'}`"
:disabled="!selectedSite"
@click="saveData"
>
<i class="ri-save-line cursor-pointer" />
Сохранить изменения
</button>
</div>
</template>

View File

@@ -0,0 +1,69 @@
<script>
export default {
name: 'Textarea',
props: {
id: {
default: 'input',
type: String
},
type: {
default: 'text',
type: String
},
name: {
default: '',
type: String
},
textareaClass: {
default: '',
type: String
},
placeholder: {
default: '',
type: String
},
required: {
default: false,
type: Boolean
},
value: {
default: '',
type: String
},
onChange: {
default: () => {},
type: Function
},
focus: {
default: () => {},
type: Function
},
blur: {
default: () => {},
type: Function
}
},
computed: {
},
methods: {
setValue: function (e) {
this.onChange({key: this.name, value: e.target.value})
},
}
}
</script>
<template>
<textarea
:id="id"
:type="type"
:name="name"
:class="`border p-2.5 w-full text-sm bg-gray-50 rounded-lg border-gray-300 focus:ring-blue-500 focus:border-blue-500 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 ${textareaClass}`"
:placeholder="placeholder"
:required="required"
:value="value"
:on:change="setValue"
@focus="focus"
@blur="blur"
/>
</template>

View File

@@ -0,0 +1,30 @@
<script>
import {mapActions} from 'vuex'
export default {
name: 'Control',
props: { },
methods: {
...mapActions('proxy', ["breakSavingSite", "addNewRouteLayout"]),
}
}
</script>
<template>
<div class="mb-4 flex items-center">
<button
type="button"
class="w-fit text-white bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 font-medium rounded-full text-sm px-5 py-2.5 text-center me-2 mr-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
@click="addNewRouteLayout"
>
Добавить роут
</button>
<button
type="button"
class="w-fit text-white bg-slate-700 hover:bg-slate-800 focus:outline-none focus:ring-4 focus:ring-slate-300 font-medium rounded-full text-sm px-5 py-2.5 text-center me-2 dark:bg-slate-600 dark:hover:bg-slate-700 dark:focus:ring-slate-800"
@click="breakSavingSite"
>
Закрыть
</button>
</div>
</template>

View File

@@ -0,0 +1,306 @@
<script>
import {mapActions, mapGetters} from 'vuex'
import { FwbSpinner } from 'flowbite-vue'
import Input from '@atoms/Input.vue'
import DoubleSwitch from '@atoms/DoubleSwitch.vue'
import Textarea from '@atoms/Textarea.vue'
export default {
name: 'RouterRow',
components: {Input, Textarea, DoubleSwitch, FwbSpinner},
props: {
id: {
default: -1,
type: Number
},
},
data() {
return {
timer: null,
state: "active",
textAreaRows: 1,
isRemoving: false,
}
},
computed: {
...mapGetters('proxy', ["selectedSite", "routes", 'routeOptions', 'routesLib']),
path() {
return this.routesLib[this.id]?.path || ""
},
role() {
return this.routesLib[this.id]?.role || ""
},
description() {
return this.routesLib[this.id]?.description || ""
},
errorPercent() {
if (!this.routesLib[this.id]?.cb_error_threshold_percentage && this.routesLib[this.id]?.cb_error_threshold_percentage !== 0) return ""
return this.routesLib[this.id]?.cb_error_threshold_percentage
},
intervalDuration() {
if (!this.routesLib[this.id]?.cb_interval_duration && this.routesLib[this.id]?.cb_interval_duration !== 0) return ""
return this.routesLib[this.id]?.cb_interval_duration
},
minRequests() {
if (!this.routesLib[this.id]?.cb_min_requests && this.routesLib[this.id]?.cb_min_requests !== 0) return ""
return this.routesLib[this.id]?.cb_min_requests
},
openStateTimeout() {
if (!this.routesLib[this.id]?.cb_open_state_timeout && this.routesLib[this.id]?.cb_open_state_timeout !== 0) return ""
return this.routesLib[this.id]?.cb_open_state_timeout
},
requestLimit() {
if (!this.routesLib[this.id]?.cb_request_limit && this.routesLib[this.id]?.cb_request_limit !== 0) return ""
return this.routesLib[this.id]?.cb_request_limit
},
deepness() {
if (!this.routesLib[this.id]?.deepness && this.routesLib[this.id]?.deepness !== 0) return ""
return this.routesLib[this.id]?.deepness
},
order() {
if (!this.routesLib[this.id]?.order && this.routesLib[this.id]?.order !== 0) return ""
return this.routesLib[this.id]?.order
},
isCbOn() {
return this.routesLib[this.id]?.is_cb_on
},
isOnline() {
return this.routesLib[this.id]?.is_online
},
},
methods: {
...mapActions('proxy', ["updateRouteRow", "removeRoute", "breateAddingRoute"]),
toggle() {
this.open = !this.open
this.onToggle({isOpen: this.open})
},
setValue(key, value) {
this.updateRouteRow({
...this.routesLib[this.id],
[key]: value
})
},
setInputValue(params) {
const value = params.isNumber ? parseFloat(params.value) : params.value
this.updateRouteRow({
...this.routesLib[this.id],
[params.key]: value
})
},
removeCurrentRoute() {
this.removeRoute({id: this.id})
},
expandTextArea() {
this.textAreaRows = 8
},
decreaseTextArea() {
this.textAreaRows = 1
}
},
}
</script>
<template>
<div class="w-full flex flex-row gap-2 mb-2 items-end">
<div class="w-fit">
<DoubleSwitch
name="isCbOn"
firstColor="#8b5cf6"
secondColor="#2563eb"
firstTitle="Без защиты"
secondTitle="Защита"
:isCheck="isCbOn"
position="col"
labelClass="pb-2"
switchClass="mt-2.5"
@switched="(v) => setValue('is_cb_on', v)"
/>
</div>
<div class="w-fit">
<DoubleSwitch
name="isOnline"
firstColor="#8b5cf6"
secondColor="#2563eb"
firstTitle="Офлайн"
secondTitle="Онлайн"
:isCheck="isOnline"
position="col"
labelClass="pb-2"
switchClass="mt-2.5"
@switched="(v) => setValue('is_online', v)"
/>
</div>
<div class="w-full">
<div class="mr-2 mb-1 min-w-[80px] text-xs">
Путь
</div>
<Input
name="path"
:value="path"
inputClass="!w-[100%] py-1.5"
placeholder="Указать путь"
:onChange="setInputValue"
/>
</div>
<div class="w-fit min-w-[70px]">
<div class="mr-2 mb-1 text-xs">
Роль
</div>
<select
:value="role"
class="w-full rounded-lg border-gray-300 py-1.5 px-2"
@change="(e) => setValue('role', parseFloat(e.target.value))"
>
<option
v-for="(el, idx) in routeOptions"
:key="{idx}"
:value="el.value"
>
{{ el.name }}
</option>
</select>
</div>
<div class="w-full translate-y-1.5">
<div class="mr-2 mb-1 max-w-[80px] text-xs">
Описание
</div>
<Textarea
name="description"
:value="description"
textareaClass="py-2 resize-none "
placeholder="Описание..."
:rows="textAreaRows"
:focus="expandTextArea"
:blur="decreaseTextArea"
:onChange="setInputValue"
/>
</div>
<div class="w-fit min-w-[80px]">
<div class="mr-2 mb-1 text-xs">
Ошибка, %
</div>
<Input
name="cb_error_threshold_percentage"
:value="`${errorPercent}`"
inputClass="!w-[100%] py-1.5"
placeholder="Процент ошибки"
:onChange="(v) => setInputValue({...v, isNumber: true})"
/>
</div>
<div class="w-fit min-w-[80px]">
<div class="mr-2 mb-1 text-xs">
Интервал
</div>
<Input
name="cb_interval_duration"
:value="`${intervalDuration}`"
inputClass="!w-[100%] py-1.5"
placeholder="Интервал"
:onChange="(v) => setInputValue({...v, isNumber: true})"
/>
</div>
<div class="w-fit min-w-[80px]">
<div class="mr-2 mb-1 text-xs">
Кол-во запросов
</div>
<Input
name="cb_min_requests"
:value="`${minRequests}`"
inputClass="!w-[100%] py-1.5"
placeholder="Кол-во запросов"
:onChange="(v) => setInputValue({...v, isNumber: true})"
/>
</div>
<div class="w-fit min-w-[80px]">
<div class="mr-2 mb-1 text-xs">
Тайм-аут состояния
</div>
<Input
name="cb_open_state_timeout"
:value="`${openStateTimeout}`"
inputClass="!w-[100%] py-1.5"
placeholder="Тайм-аут состояния"
:onChange="(v) => setInputValue({...v, isNumber: true})"
/>
</div>
<div class="w-fit min-w-[80px]">
<div class="mr-2 mb-1 text-xs">
Лимит запросов
</div>
<Input
name="cb_request_limit"
:value="`${requestLimit}`"
inputClass="!w-[100%] py-1.5"
placeholder="Лимит запросов"
:onChange="(v) => setInputValue({...v, isNumber: true})"
/>
</div>
<div class="w-fit min-w-[80px]">
<div class="mr-2 mb-1 text-xs">
Глубина запросов
</div>
<Input
name="deepness"
:value="`${deepness}`"
inputClass="!w-[100%] py-1.5"
placeholder="Глубина запросов"
:onChange="(v) => setInputValue({...v, isNumber: true})"
/>
</div>
<div class="w-fit min-w-[80px]">
<div class="mr-2 mb-1 text-xs">
Порядок сортировки
</div>
<Input
name="order"
:value="`${order}`"
inputClass="!w-[100%] py-1.5"
placeholder="Порядок сортировки"
:onChange="(v) => setInputValue({...v, isNumber: true})"
/>
</div>
<div class="flex items-center col-span-1">
<div
v-if="!isRemoving"
class=" transition-all h-9 w-9 mr-4 rounded-md bg-red-700 hover:bg-red-600 cursor-pointer text-white flex items-center justify-center"
@click="() => isRemoving = true"
>
<i class="ri-delete-bin-line" />
</div>
<div
v-if="isRemoving"
class=" transition-all h-9 w-24 mr-4 rounded-md bg-gray-700 hover:bg-gray-600 cursor-pointer text-white flex items-center justify-center"
@click="() => isRemoving = false"
>
Отменить
</div>
<div
v-if="isRemoving"
class=" transition-all h-9 w-24 mr-4 rounded-md bg-red-700 hover:bg-red-600 cursor-pointer text-white flex items-center justify-center"
@click="removeCurrentRoute({id})"
>
Удалить
</div>
<div
v-if="timer"
class="flex items-center opacity-75"
>
<span class="mr-2 font-w-700 text-slate-900">Сохранение</span>
<fwb-spinner
v-if="timer"
size="6"
/>
</div>
<div
v-if="state === 'success'"
class="flex items-center "
>
<span class="mr-2 text-slate-700 font-w-700">Сохранено</span>
<i
class="ri-check-line text-green-600"
/>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,26 @@
<script>
import { FwbSpinner } from 'flowbite-vue'
import { mapGetters} from 'vuex'
export default {
name: 'RoutesLoader',
components: {FwbSpinner},
computed: {
...mapGetters('proxy', ["routesState"]),
},
}
</script>
<template>
<div
v-if="routesState === 'loading'"
class="w-full flex justify-center items-center"
>
<div class="flex items-center">
<fwb-spinner
size="8"
class="mr-2"
/> Загрузка...
</div>
</div>
</template>

View File

@@ -0,0 +1,51 @@
<script>
import {mapGetters} from 'vuex'
import Control from './Controll.vue'
import RoutesLoader from './RoutesLoader.vue'
export default {
name: 'RoutesList',
components: {RoutesLoader, Control},
props: {
name: {
default: "",
type: String
},
port: {
default: "",
type: String
},
},
data() {
return {
open: false,
}
},
computed: {
...mapGetters('proxy', ['routesState']),
},
watch: {
routesState: function (newVal) {
this.open = newVal === 'active' ? true : false
}
},
methods: {
toggle () {
this.open = !this.open
this.onToggle({isOpen: this.open})
}
}
}
</script>
<template>
<div
v-if="open"
class="flex flex-col w-full"
>
<Control />
<slot />
</div>
<RoutesLoader />
</template>

View File

@@ -0,0 +1,108 @@
<!-- eslint-disable vue/prop-name-casing -->
<script>
import {mapActions, mapGetters} from 'vuex'
import Input from '@atoms/Input.vue'
import Textarea from '@atoms/Textarea.vue'
import DoubleSwitch from '@atoms/DoubleSwitch.vue'
export default {
name: 'EditCard',
components: {Input, Textarea, DoubleSwitch},
props: {
id: {
default: -1,
type: Number
},
},
computed: {
...mapGetters('proxy', ["selectedSite"]),
},
mounted() {
},
methods: {
...mapActions('proxy', ["editSelectedSite", "breakSavingSite", "breakeAddingSite"]),
editData (params) {
this.editSelectedSite(params)
},
breakSavingSiteDate() {
if (this.id == -1) {
this.breakeAddingSite()
} else {
this.editable_name = this.name
this.editable_port = this.port
this.breakSavingSite()
}
}
}
}
</script>
<template>
<div class="flex justify-between items-center mb-2">
<div class="w-fit mr-2">
<DoubleSwitch
name="isCbOn"
firstColor="#8b5cf6"
secondColor="#2563eb"
firstTitle="Офлайн"
secondTitle="Онлайн"
:isCheck="selectedSite.is_online"
position="col"
labelClass="items-start pb-2"
switchClass="mt-1"
@switched="(e) => editData({key: 'is_online', value: e})"
/>
</div>
<Input
name="name"
:value="selectedSite.name"
inputClass="!w-[90%] py-2"
placeholder="Указать путь"
:onChange="editData"
/>
<div>
<i
v-tippy="{ content: 'закрыть сервис' }"
class="ri-close-line ml-2 cursor-pointer"
@click="breakSavingSiteDate"
/>
</div>
</div>
<div class="flex items-center mb-2 w-full">
<span class="mr-2 min-w-[80px]">Порт:</span>
<Input
name="port"
:value="`${selectedSite.port}`"
inputClass="py-2"
placeholder="Порт"
:onChange="editData"
/>
</div>
<div class="flex items-center mb-2 w-full">
<span class="mr-2 min-w-[80px]">IP proxy:</span>
<Input
name="proxy_ip"
:value="selectedSite.proxy_ip"
inputClass="py-2"
placeholder="IP proxy"
:onChange="editData"
/>
</div>
<div class="flex items-center w-full mb-2">
<span class="mr-2 min-w-[80px]">IP устр-ва:</span>
<Input
name="device_ip"
:value="selectedSite.device_ip"
inputClass="py-2"
placeholder="IP устройства"
:onChange="editData"
/>
</div>
<Textarea
name="description"
:value="selectedSite.description"
textareaClass="py-2"
placeholder="Описание..."
:onChange="editData"
/>
</template>

View File

@@ -0,0 +1,165 @@
<!-- eslint-disable vue/prop-name-casing -->
<script>
import {mapActions, mapGetters} from 'vuex'
import EditCard from './EditCard.vue'
export default {
name: 'SiteCard',
components: {EditCard},
props: {
id: {
default: -1,
type: Number
},
name: {
default: "",
type: String
},
port: {
default: "",
type: String
},
device_ip: {
default: "",
type: String
},
proxy_ip: {
default: "",
type: String
},
status: {
default: "disable",
type: String
},
description: {
default: "disable",
type: String
},
site: {
default: () => ({}),
type: Object
}
},
data () {
return {
isDelete: false,
}
},
computed: {
...mapGetters('proxy', ["selectedSite", "routes", 'isSaveData']),
__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`
}
},
watch: {
isSaveData: function (newVal) {
console.log('newVal', newVal)
if (newVal) {
this.saveSiteData()
}
},
},
mounted() {
},
methods: {
...mapActions('proxy', ["uploadSiteRoutes", "saveSite", "createNewSite", 'removeSite', 'updateRoutesWithApi']),
toggle () {
this.open = !this.open
this.onToggle({isOpen: this.open})
},
saveSiteData () {
if (this.selectedSite.id == -1) {
const data = {
name: this.selectedSite.name,
port: this.selectedSite.port,
device_ip: this.selectedSite.device_ip,
proxy_ip: this.selectedSite.proxy_ip,
description: this.selectedSite.description
}
this.createNewSite(data)
} else {
this.updateRoutesWithApi(this.selectedSite)
this.saveSite(this.selectedSite)
}
},
deleteSite (v) {
this.isDelete = v
}
}
}
</script>
<template>
<div
v-if="!selectedSite || selectedSite.id !== id"
href="#"
class="block w-full p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
>
<div class="flex justify-between items-center mb-1">
<h5
v-tippy="{ content: name }"
class="max-w-[200px] text-xl font-bold tracking-tight text-slate-600 dark:text-white truncate"
>
{{ name }}
</h5>
<div class="relative">
<i
v-tippy="{ content: 'редактировать сервис' }"
class="ri-pencil-line cursor-pointer"
@click="uploadSiteRoutes(site)"
/>
<i
v-tippy="{ content: 'удалить сервис' }"
class="ri-delete-bin-line cursor-pointer ml-2"
@click="deleteSite(true)"
/>
<div
v-if="isDelete"
class="absolute flex flex-row top-0 right-0 p-3 bg-white rounded-lg shadow-lg z-10"
>
<button
class="flex items-center text-xs text-white bg-blue-700 p-1 mr-2 rounded-lg shadow"
@click="deleteSite(false)"
>
Отменить
</button>
<button
class="flex items-center p-1 text-xs text-white bg-slate-700 rounded-lg shadow"
@click="removeSite(id)"
>
Удалить
</button>
</div>
</div>
</div>
<div class="w-full h-[1px] bg-slate-200 mb-4" />
<div class="font-normal text-sm text-gray-700 dark:text-gray-400 mb-4 flex -translate-x-2">
<span class="flex border-slate-300 mr-4 bg-slate-100 rounded-full py-1 px-2 items-center w-full max-w-[50%]">
<span class="inline-block flex mr-2 font-w-700"> статус:</span> <span class="mr-2">{{ status }}</span> <div class="min-h-2 min-w-2 bg-green-700 rounded-full" />
</span>
<span class="flex border-slate-300 bg-slate-100 rounded-full py-1 px-2 grow">
<span class="inline-block flex mr-2 font-w-700"> Порт:</span> <span>{{ port }}</span>
</span>
</div>
<p class=" font-normal text-sm text-gray-700 dark:text-gray-400 mb-2 flex">
<span class="min-w-[80px] font-w-700 inline-block flex"> IP proxy:</span> {{ proxy_ip }}
</p>
<p class=" font-normal text-sm text-gray-700 dark:text-gray-400 mb-2 flex">
<span class="min-w-[80px] font-w-700 inline-block flex"> IP устр-ва:</span> {{ device_ip }}
</p>
<p class=" font-normal text-sm text-gray-700 dark:text-gray-400 mb-2 flex ">
<span class="min-w-[80px] font-w-700 inline-block flex"> Описание:</span> <span
v-tippy="{ content: description }"
class="truncate"
>{{ description }}</span>
</p>
</div>
<div
v-if="selectedSite && selectedSite.id === id"
href="#"
class="block w-full p-6 bg-white border-4 border-blue-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
>
<EditCard :id="id" />
</div>
</template>

View File

@@ -0,0 +1,82 @@
<script>
// import {ref} from 'vue'
import {mapActions, mapGetters} from 'vuex'
import { FwbSpinner } from 'flowbite-vue'
import SiteCard from "./SiteCard.vue"
import NewSiteButton from "../../NewSiteButton.vue"
export default {
name: 'SiteList',
components: {FwbSpinner, SiteCard, NewSiteButton},
data () {
return {
dynamicHeight: null
}
},
computed: {
...mapGetters('proxy', ["sites", "routes", "sitesState", "newSite", 'isDeleteData']),
},
mounted () {
this.$nextTick(function () {
this.maxHeight()
})
},
updated () {
this.$nextTick(function () {
this.maxHeight()
})
},
methods: {
...mapActions('proxy', ["uploadSites"]),
maxHeight () {
setTimeout(() => {
const containerHeight = this.$refs.sitesList?.offsetHeight
const clientHeight = document.documentElement.clientHeight
const delHeight = 400
const currHeight = containerHeight + delHeight
if ((currHeight) > clientHeight) {
this.dynamicHeight = clientHeight - delHeight
}
}, 100)
},
}
}
</script>
<template>
{{
}}
<div
v-if="sitesState === 'active'"
ref="sitesList"
:class="`${dynamicHeight ? 'shadow-lg p-3' : ''} grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 3xl:grid-cols-6 gap-5 overflow-y-auto mb-14`"
:style="{maxHeight: `${dynamicHeight}px`}"
>
<NewSiteButton />
<SiteCard
v-for="site in sites"
:id="site.id"
:key="site.name"
:site="site"
:name="site.name"
:port="`${site.port}`"
:device_ip="site.device_ip"
:proxy_ip="site.proxy_ip"
:status="site.status"
:description="site.description"
/>
</div>
<div
v-if="sitesState === 'loading'"
class="flex w-full justify-center"
>
<fwb-spinner
size="8"
class="mr-2"
/>
<span class="text-2xl text-slate-700">
Загрузка сайтов...
</span>
</div>
</template>

View File

View File

@@ -0,0 +1,33 @@
<script>
import {mapActions} from 'vuex'
export default {
name: 'NewSiteButton',
props: {
name: {
default: "",
type: String
},
port: {
default: "",
type: String
},
site: {
default: () => ({}),
type: Object
}
},
methods: {
...mapActions('proxy', ['addNewSiteLayout'])
}
}
</script>
<template>
<div
class="cursor-pointer flex items-center justify-center w-full p-6 bg-white border border-gray-200 rounded-lg shadow hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 text-2xl font-w-700 text-slate-700"
@click="addNewSiteLayout"
>
Добавить сайт
</div>
</template>