Browse Source

Merge branch 'lixin' of gitadmin/tuoheng_lc_web into develop

tags/v1.2.0
lixin 1 year ago
parent
commit
025f7c59ed
10 changed files with 433 additions and 174 deletions
  1. +9
    -0
      src/api/dashboard/index.js
  2. +27
    -0
      src/utils/style.js
  3. +1
    -0
      src/views/dashboard/components/ControlPanel.vue
  4. +0
    -124
      src/views/dashboard/components/Echarts.vue
  5. +33
    -2
      src/views/dashboard/components/Extend.vue
  6. +59
    -23
      src/views/dashboard/components/FireAlarm.vue
  7. +10
    -6
      src/views/dashboard/components/OneMap.vue
  8. +145
    -0
      src/views/dashboard/components/SpeedChart.vue
  9. +108
    -9
      src/views/dashboard/components/Underlay.vue
  10. +41
    -10
      src/views/dashboard/components/WarningDrawer.vue

+ 9
- 0
src/api/dashboard/index.js View File

}) })
} }


// 定点飞行
export function pointflight(params) {
return request({
url: `/airport/pointflight`,
method: 'POST',
params
})
}

// 预警已确认 // 预警已确认
export function emergencyList(params = { airportFlyType: 2, statusList: 2 }) { export function emergencyList(params = { airportFlyType: 2, statusList: 2 }) {
return request({ return request({

+ 27
- 0
src/utils/style.js View File

import { Style, Icon, Stroke } from 'ol/style'
import imgGeo from '@/assets/point/geo.png'
export const styleList = {
route: new Style({
stroke: new Stroke({
width: 2,
color: 'red'
})
}),
geoMarker: new Style({
image: new Icon({
anchor: [0.5, 0.5],
src: imgGeo,
crossOrigin: '',
scale: [1, 1],
rotateWithView: true
})
}),
lineRoute: new Style({
stroke: new Stroke({
width: 2,
color: 'yellow',
lineDash: [5]
})
})
}


+ 1
- 0
src/views/dashboard/components/ControlPanel.vue View File

align-items: center; align-items: center;
justify-content: space-around; justify-content: space-around;
overflow: hidden; overflow: hidden;
margin: 0 5px 0 0;
.crcle__panel{ .crcle__panel{
width: 180px; width: 180px;
height: 180px; height: 180px;

+ 0
- 124
src/views/dashboard/components/Echarts.vue View File

<template>
<div class="content" id="myChart">

</div>
</template>
<script>
import * as echarts from 'echarts'
import { ref, reactive, toRefs, watch, onMounted } from 'vue'
export default {
name: 'Echarts',
setup(props) {

const initMyChart = () => {
const chartDom = document.getElementById('myChart');
const myChart = echarts.init(chartDom);
var option;
option = {
legend: {
top: '12',
data: ['速度', '高度'],
icon: 'roundRect',
itemHeight: 1,
itemWidth: 31,
textStyle: {
color: '#ffffff'
}

},
grid: {
top: '60',
bottom: '23',
left: '50',
right: '60'
},
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
show: false
},
yAxis: [
{
type: 'value',
name: '速度(m/s)',
nameTextStyle: {
color: "rgba(255,255,255,1)"
},
axisLabel: {
show: true,
textStyle: {
color: 'rgba(255,255,255,1)'
}
}
},
{
type: 'value',
name: '高度(s)',
position: "right",
nameTextStyle: {
color: "rgba(255,255,255,1)"
},
axisLabel: {
textStyle: {
color: "rgba(255,255,255,1)",
},
formatter: "{value}",
},

}
],
series: [
{
name: '速度',
data: [150, 230, 224, 218, 135, 147, 260],
type: 'line',
symbol: 'none',
smooth: true,
itemStyle: {
normal: {
color: 'rgba(255,141,26,1)',
width: 2
}
},
yAxisIndex: 0,
},
{
name: '高度',
data: [500, 2000, 3000, 1000, 1200, 1300, 2310],
type: 'line',
smooth: true,
symbol: 'none',
yAxisIndex: 1,
itemStyle: {
normal: {
color: 'rgba(165, 214, 63, 1)',
width: 2
}
}
}
]
};

option && myChart.setOption(option);
}
onMounted(
() => {
initMyChart()
})
}
}
</script>
<style>
.content {
position: absolute;
bottom: 16px;
left: 24px;
background-color: rgba(0, 0, 0, 1);
width: 734px;
height: 216px;
font-size: 16px;
font-weight: 400;
color: rgba(255, 255, 255, 1)
}
</style>

+ 33
- 2
src/views/dashboard/components/Extend.vue View File

<span>{{ statusList[item.status] }}</span> <span>{{ statusList[item.status] }}</span>
<n-popconfirm <n-popconfirm
v-if="item.status == 1" v-if="item.status == 1"
@positive-click="handlePositiveClick(item)"
@negative-click="handleNegativeClick"
> >
<template #trigger> <template #trigger>
<span class="table__operate">执行</span> <span class="table__operate">执行</span>
</template> </template>
是否立即开始执行任务? 是否立即开始执行任务?
</n-popconfirm> </n-popconfirm>
<span v-else class="table__operate live">直播</span>
<span
v-else
class="table__operate live"
@click="liveShow(item)"
>直播</span>
</li> </li>
</ul> </ul>
</div> </div>


<script> <script>
import { onMounted, reactive, toRefs } from 'vue' import { onMounted, reactive, toRefs } from 'vue'
import { useRouter } from 'vue-router'
import { startOfDay } from 'date-fns/esm' import { startOfDay } from 'date-fns/esm'
import { getMissionList, getQuestionList } from '@/api/dashboard/index.js' import { getMissionList, getQuestionList } from '@/api/dashboard/index.js'
import { cameraList } from '@/api/basic/monitor.js' import { cameraList } from '@/api/basic/monitor.js'
import camera from '@/assets/icon/camera.png' import camera from '@/assets/icon/camera.png'
import materials from '@/assets/icon/materials.png' import materials from '@/assets/icon/materials.png'
import personnel from '@/assets/icon/personnel.png' import personnel from '@/assets/icon/personnel.png'
import { implement } from '@/api/task/index.js'
const ICON_LIST = { const ICON_LIST = {
'002000': problemSpot_icon, '002000': problemSpot_icon,
'002001': deadTree_icon, '002001': deadTree_icon,
name: 'MapExtend', name: 'MapExtend',
emits: ['send'], emits: ['send'],
setup(props, { emit }) { setup(props, { emit }) {
const router = useRouter()
const data = reactive({ const data = reactive({
selectedTab: 0, selectedTab: 0,
extendList: [ extendList: [
emit('send', { tabs: data.selectedTab, data: ques.message, type: value, ops: 'select' }) emit('send', { tabs: data.selectedTab, data: ques.message, type: value, ops: 'select' })
} }


/**
* 展示直播视频
*/
const liveShow = (rowInfo) => {
rowInfo = JSON.stringify(rowInfo)

router.push({
path: '/taskManage/all',
query: { rowInfo: rowInfo }
})
}

return { return {
...toRefs(data), ...toRefs(data),
...toRefs(warn), ...toRefs(warn),
handlePageChange, handlePageChange,
handleDateChange, handleDateChange,
handleWarnChange, handleWarnChange,
handleQuesChange
handleQuesChange,
handlePositiveClick(row) {
$message.info('机场设备开始自检,请稍等')
implement(row.id).then((res) => {
if (res.code === 0) {
$message.info('操作成功')
}
})
},
handleNegativeClick() {},
liveShow
} }
} }
} }

+ 59
- 23
src/views/dashboard/components/FireAlarm.vue View File

<p class="alarm-title">机场调度</p> <p class="alarm-title">机场调度</p>
<p class="dispatch-detail"> <p class="dispatch-detail">
<span>选择机场:</span> <span>选择机场:</span>
<n-select v-model:value="airValue" placeholder="请选择机场" :options="airpotOptions" />
<n-select v-model:value="airportId" placeholder="请选择机场" :options="airpotOptions" />
<a @click="checkAirport">可用机场列表</a> <a @click="checkAirport">可用机场列表</a>
</p> </p>
<p class="dispatch-detail"> <p class="dispatch-detail">
<li v-for="(item, index) in recoedList" :key="index"> <li v-for="(item, index) in recoedList" :key="index">
<span class="task-time">{{ item.time }}</span> <span class="task-time">{{ item.time }}</span>
<span class="task-name">{{ item.name }}</span> <span class="task-name">{{ item.name }}</span>
<span v-if="item.statusNum === 1" class="task-status on-fly">{{ item.status }}</span>
<span v-if="item.statusNum === 2" class="task-status on-fly" @click="handleExecute(item.emergencyMissionId)">{{ item.status }}</span>
<span v-else class="task-status">{{ item.status }}</span> <span v-else class="task-status">{{ item.status }}</span>
</li> </li>
</ul> </ul>


import { reactive, toRefs, watch } from 'vue' import { reactive, toRefs, watch } from 'vue'
import { EARLY_SOURCE, TASK_STATUS } from '@/utils/dictionary.js' import { EARLY_SOURCE, TASK_STATUS } from '@/utils/dictionary.js'
import { getWarningRecord, getWarningInfo, ignoreWarning, confirmWarning } from '@/api/dashboard/index.js'
import { getWarningRecord, getWarningInfo, ignoreWarning, confirmWarning, pointflight } from '@/api/dashboard/index.js'
// turf 用于简单的空间计算 // turf 用于简单的空间计算
import * as turf from '@turf/turf' import * as turf from '@turf/turf'
export default { export default {
airportShow: false, airportShow: false,
warningInfo: {}, warningInfo: {},
airportSelect: '', airportSelect: '',
airValue: 0,
airportId: null,
warningPic: null, warningPic: null,
recoedList: [], recoedList: [],
warningShow: false, warningShow: false,
}) })
data.fireDetail.time = value?.createTime data.fireDetail.time = value?.createTime


Promise.all([await getWarningInfo(value.id), await getWarningRecord({ warningId: value.id })])
.then(([info, record]) => {
// 分析后的图片
data.warningPic = info?.data?.fileMarkerUrl || null
// 任务id
data.missionId = info?.data?.missionId || null
showUsableAirport(info)
showWarningRecord(record)
})
.catch(err => {
console.log(err)
})
getRecord(value.id)
getAirport(value.id)
}

// 获取记录
const getAirport = async(id) => {
const info = await getWarningInfo(id)
if (info.code === 0) {
// 分析后的图片
data.warningPic = info?.data?.fileMarkerUrl || null
// 任务id
data.missionId = info?.data?.missionId
showUsableAirport(info)
}
}

// 获取记录
const getRecord = async(id) => {
const record = await getWarningRecord({ warningId: id })
if (record.code === 0) {
showWarningRecord(record)
}
} }


// 预警记录列表 // 预警记录列表
const showWarningRecord = (record) => { const showWarningRecord = (record) => {
// 清空原来的应急任务列表
data.recoedList.length = 0 data.recoedList.length = 0
var status = '' var status = ''
var statusNum = 0 var statusNum = 0
} }
}) })
data.recoedList.push({ data.recoedList.push({
// 应急任务创建时间
time: item.createTime, time: item.createTime,
// 应急任务名称
name: item.name, name: item.name,
// 状态
status: status, status: status,
statusNum: statusNum
// 状态编号
statusNum: statusNum,
// 应急任务id
emergencyMissionId: item.emergencyMissionId
}) })
}) })
} }
label: item.name label: item.name
}) })
}) })
data.airValue = data.airpotOptions[0]?.value || null
data.airportId = data.airpotOptions[0]?.value || null
} }


const checkAirport = () => { const checkAirport = () => {
data.warningShow = false data.warningShow = false
} }


const test = () => {
handleExecute()
console.log(data.warningInfo)
const test = async() => {
var airportName = ''
data.airpotOptions?.map((item) => {
if (item.value === data.airportId) {
airportName = item.label
}
})
const res = await pointflight({
airportId: parseInt(data.airportId),
airportName: airportName,
warningId: parseInt(data.warningInfo?.id),
missionId: parseInt(data.missionId),
alt: String(data.flyHeight),
lon: String(data.warningInfo.lng),
lat: String(data.warningInfo.lat)
})

if (res.code === 0) {
getRecord(parseInt(data.warningInfo?.id))
handleExecute(parseInt(data.missionId))
}
} }


// 忽略预警 // 忽略预警
return value + 'm' return value + 'm'
} }


const handleExecute = () => {
emit('start')
const handleExecute = (id) => {
// console.log(id)
emit('start', id)
} }


return { return {

+ 10
- 6
src/views/dashboard/components/OneMap.vue View File

<fire-alarm ref="Warning" :data="warningDetail" :airport="airportsAll" @start="handleExecute" /> <fire-alarm ref="Warning" :data="warningDetail" :airport="airportsAll" @start="handleExecute" />


<WarningDrawer v-model:visible="drawerShow" /> <WarningDrawer v-model:visible="drawerShow" />
</template> </template>


<script> <script>
import * as control from 'ol/control' import * as control from 'ol/control'
import uav_icon from '@/assets/images/airport.png' import uav_icon from '@/assets/images/airport.png'
import warningIcon from '@/assets/gis/images/fire.png' import warningIcon from '@/assets/gis/images/fire.png'
import { reactive, toRefs, onMounted, computed, ref, watch, onBeforeUnmount } from 'vue'
import { reactive, toRefs, onMounted, computed, ref, watch, onBeforeUnmount, provide } from 'vue'
import { Point } from 'ol/geom' import { Point } from 'ol/geom'
import { import {
airportList, getWarning airportList, getWarning
warningList: [], warningList: [],
warningLayers: [], warningLayers: [],
warningDetail: {}, warningDetail: {},
drawerShow: false
drawerShow: false,
missionId: 0
}) })


const getMapOptions = computed(() => { const getMapOptions = computed(() => {
monitorVideo.value?.disposeVideo() monitorVideo.value?.disposeVideo()
} }


const handleExecute = () => {
let abc = ref()
const handleExecute = (id) => {
data.drawerShow = true data.drawerShow = true
abc = id
provide('test', abc)
} }


onMounted(() => { onMounted(() => {
hideProblemInfo, hideProblemInfo,
getMaxZOverlay, getMaxZOverlay,
Warning, Warning,
handleExecute
handleExecute,
abc
} }
} }
} }

+ 145
- 0
src/views/dashboard/components/SpeedChart.vue View File

<template>
<div id="myChart" class="my-chart" />
</template>
<script>
import * as echarts from 'echarts'
import { onMounted, reactive, toRefs, watch } from 'vue'
export default {
name: 'SpeedChart',
props: {
data: {
type: Object,
default: () => {}
}
},
setup(props) {
const data = reactive({
myChart: null,
chartOption: {},
chartDom: null,
chartData: {}
})

watch(() => props.data, (value) => {
if (JSON.stringify(value) !== '{}') {
data.chartData = value
data.myChart.dispose()
initChart()
}
})

const initChart = () => {
data.chartDom = document.getElementById('myChart')
data.myChart = echarts.init(data.chartDom)
data.chartOption = {
legend: {
top: '12',
data: ['速度', '高度'],
icon: 'roundRect',
itemHeight: 1,
itemWidth: 31,
textStyle: {
color: '#ffffff'
}

},
grid: {
top: '60',
bottom: '23',
left: '50',
right: '60'
},
xAxis: {
type: 'category',
data: [0, 1],
show: false
},
yAxis: [
{
type: 'value',
name: '速度(m/s)',
nameTextStyle: {
color: 'rgba(255,255,255,1)'
},
axisLabel: {
show: true,
textStyle: {
color: 'rgba(255,255,255,1)'
}
}
},
{
type: 'value',
name: '高度(s)',
position: 'right',
nameTextStyle: {
color: 'rgba(255,255,255,1)'
},
axisLabel: {
textStyle: {
color: 'rgba(255,255,255,1)'
},
formatter: '{value}'
}

}
],
series: [
{
name: '速度',
data: data.chartData?.speed,
type: 'line',
symbol: 'none',
smooth: true,
itemStyle: {
normal: {
color: 'rgba(255,141,26,1)',
width: 2
}
},
yAxisIndex: 0
},
{
name: '高度',
data: data.chartData?.alt,
type: 'line',
smooth: true,
symbol: 'none',
yAxisIndex: 1,
itemStyle: {
normal: {
color: 'rgba(165, 214, 63, 1)',
width: 2
}
}
}
]
}

data.myChart.setOption(data.chartOption)
}

onMounted(
() => {
initChart()
})

return {
...toRefs(data)
}
}
}
</script>
<style>
.my-chart {
position: relative;
z-index: 99;
background-color: rgba(0, 0, 0, 1);
width: 734px;
height: 220px;
font-size: 16px;
font-weight: 400;
color: rgba(255, 255, 255, 1);
margin: 0 5px 0 0;
}
</style>

+ 108
- 9
src/views/dashboard/components/Underlay.vue View File

</template> </template>


<script> <script>
import { Map, View } from 'ol'
import { XYZ } from 'ol/source'

import { reactive, toRefs, onMounted, computed, watch, onBeforeUnmount, inject, ref } from 'vue'
import { Map, View, Feature } from 'ol'
import 'ol/ol.css'
import { Tile, Vector as VectorLayer } from 'ol/layer'
import TileWMS from 'ol/source/TileWMS.js' import TileWMS from 'ol/source/TileWMS.js'
import { Tile } from 'ol/layer'
import { transform } from 'ol/proj'
import LineString from 'ol/geom/LineString'
import Point from 'ol/geom/Point'
import { XYZ, Vector as VectorSource } from 'ol/source'
import { transform, fromLonLat } from 'ol/proj'
import { getVectorContext } from 'ol/render'
import * as control from 'ol/control' import * as control from 'ol/control'
import { reactive, toRefs, onMounted, computed, ref, onBeforeUnmount } from 'vue'
import { styleList } from '@/utils/style.js'
import { getTrackList } from '@/api/task/index.js'


export default { export default {
name: 'OneMap',
name: 'UnderLay',
props: { props: {
id: { id: {
type: String, type: String,
default: 'underlay' default: 'underlay'
},
data: {
type: Number,
default: 0
} }
}, },
setup(props) { setup(props) {
const monitorVideo = ref()
const data = reactive({ const data = reactive({
map: null, map: null,
drawerShow: false
drawerShow: false,
trackLayer: null,
trackInfo: null,
// 轨迹数据
trackList: [],
id: 0
}) })


const getMapOptions = computed(() => { const getMapOptions = computed(() => {
} }
}) })


let abc = ref()
abc = inject('test')

watch(() => abc, (value) => {
if (value) {
console.log('hhh', value)
} else {
console.log('hhh', value)
}
// getTrackData(id).then(res => {
// if (res.trackList.length > 0) {
// initTrack(formatTradeList(res.trackList), 'route')
// }
// })
})

/** /**
* 初始化地图 * 初始化地图
*/ */
wmsSource.setOpacity(0.3) wmsSource.setOpacity(0.3)
} }


/* 获取轨迹数据 */
const getTrackData = async function(id) {
const res = await getTrackList(id)
const trackList = res.data
data.trackList = trackList
// const trackList = data.trackList
return Promise.resolve({
trackList
})
}

/* 处理轨迹列表 */
const formatTradeList = function(trackList) {
return trackList.map((item) =>
fromLonLat([parseFloat(item.lng), parseFloat(item.lat)])
)
}

/* 初始化轨迹 */
const initTrack = function(coordinate, routeType) {
let vectorLayer = null
const route = new LineString(coordinate)
const routeFeature = new Feature({
type: routeType,
geometry: route
})
const startMarker = new Feature({
type: 'icon',
geometry: new Point(route.getFirstCoordinate())
})
const position = startMarker.getGeometry().clone()
const geoMarker = new Feature({
type: 'geoMarker',
geometry: position
})
geoMarker.setProperties({ type: 'geoMarker' }, false)
if (data.trackLayer) {
data.trackLayer.setSource(
new VectorSource({
features: [geoMarker, routeFeature]
})
)
vectorLayer = data.trackLayer
} else {
vectorLayer = new VectorLayer({
source: new VectorSource({
features: [geoMarker, routeFeature]
}),
style: function(feature) {
return styleList[feature.get('type')]
}
})
}
data.mapData.addLayer(vectorLayer)
data.mapView.fit(route, { padding: [50, 50, 50, 50] })
vectorLayer.on('postrender', moveFeature.bind())
function moveFeature(event) {
/* 计算飞行的百分比 */
const percent = data.videoInfo.currentTime / data.videoInfo.duration
const currentCoordinate = route.getCoordinateAt(percent)
position.setCoordinates(currentCoordinate)
geoMarker.setGeometry(null)
const vectorContext = getVectorContext(event)
vectorContext.setStyle(styleList['geoMarker'])
vectorContext.drawGeometry(position)
data.mapData?.render()
}
}

onMounted(() => { onMounted(() => {
initMap() initMap()
}) })


onBeforeUnmount(() => { onBeforeUnmount(() => {
monitorVideo.value?.disposeVideo()
}) })


return { return {

+ 41
- 10
src/views/dashboard/components/WarningDrawer.vue View File

<p class="side__title">操作</p> <p class="side__title">操作</p>
<div class="side__control"> <div class="side__control">
<n-button>抛投</n-button> <n-button>抛投</n-button>
<n-button>喊话</n-button>
<n-button @click="fanhang">喊话</n-button>
<n-button @click="handleOperate">{{ operate }}</n-button> <n-button @click="handleOperate">{{ operate }}</n-button>
<n-button @click="handleControl">{{ control }}</n-button> <n-button @click="handleControl">{{ control }}</n-button>
<n-button class="control__special" round @click="handleBack">返航</n-button> <n-button class="control__special" round @click="handleBack">返航</n-button>
</div> </div>


<div class="control__panel"> <div class="control__panel">
<ControlPanel v-if="showControl" mode="camera" />
<ControlPanel v-if="showControl" mode="locus" />
<SpeedChart :data="chartData" />
<ControlPanel v-if="showControl" mode="camera" @start="startOrder" @reset="endOrder" />
<ControlPanel v-if="showControl" mode="locus" @start="startOrder" @reset="endOrder" />
</div> </div>

</div> </div>
<div ref="mapRef" class="warn__back"> <div ref="mapRef" class="warn__back">
<Underlay /> <Underlay />
<div ref="videoRef" class="inner"> <div ref="videoRef" class="inner">
111111111111 111111111111
</div> </div>
<Echarts/>
</n-drawer-content> </n-drawer-content>
</n-drawer> </n-drawer>
</template> </template>
import { defineComponent, ref, reactive, toRefs, computed, watch, nextTick } from 'vue' import { defineComponent, ref, reactive, toRefs, computed, watch, nextTick } from 'vue'
import Underlay from './Underlay.vue' import Underlay from './Underlay.vue'
import ControlPanel from './ControlPanel.vue' import ControlPanel from './ControlPanel.vue'
import Echarts from './Echarts.vue'
import SpeedChart from './SpeedChart.vue'
export default defineComponent({ export default defineComponent({
name: 'WarningDrawer', name: 'WarningDrawer',
components: { Underlay, ControlPanel,Echarts },
components: { Underlay, ControlPanel, SpeedChart },
props: { props: {
/* 可见 */ /* 可见 */
visible: { visible: {
data: { data: {
type: Object, type: Object,
default: () => {} default: () => {}
},
id: {
type: Number,
default: 0
} }
}, },
emits: { emits: {
const sideRef = ref() const sideRef = ref()
const mapRef = ref() const mapRef = ref()
const videoRef = ref() const videoRef = ref()

const data = reactive({ const data = reactive({
hasChanged: false, hasChanged: false,
showControl: false, showControl: false,
operate: '悬停', operate: '悬停',
control: '手动控制'
control: '手动控制',
chartData: {},
missionId: 0
}) })


/* 获取抽屉的信息 */ /* 获取抽屉的信息 */
window.dispatchEvent(new Event('resize')) window.dispatchEvent(new Event('resize'))
} }


const fanhang = () => {
data.chartData = {
speed: [300, 200],
alt: [500, 510]
}
}

const startOrder = (params) => {
console.log(params)
}

const endOrder = (params) => {
console.log(params)
}

watch(() => props.visible, (value) => { watch(() => props.visible, (value) => {
if (value) { if (value) {
nextTick(() => { nextTick(() => {
} }
}) })


watch(() => props.id, (id) => {
if (id) {
data.missionId = id
console.log(data.missionId)
}
})

const handleOperate = () => { const handleOperate = () => {
data.operate = data.operate === '悬停' ? '继续飞行' : '悬停' data.operate = data.operate === '悬停' ? '继续飞行' : '悬停'
} }
handleChange, handleChange,
handleOperate, handleOperate,
handleControl, handleControl,
handleBack
handleBack,
fanhang,
startOrder,
endOrder
} }
} }
}) })
} }
} }


.control__panel{
.control__panel {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
display: flex; display: flex;

Loading…
Cancel
Save