first commit
This commit is contained in:
254
src/views/business/drone/cockpit.vue
Normal file
254
src/views/business/drone/cockpit.vue
Normal file
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<div class="fh2-container">
|
||||
<div id="cockpit-header-container"></div>
|
||||
<div ref="Detail" class="project-details router-cockpit">
|
||||
<div id="project-app-container"></div>
|
||||
<div id="project-middle-container"></div>
|
||||
<div id="project-right-micro-app" class="right-micro-app">
|
||||
<div class="maps-micro-app">
|
||||
<div class="cockpit-left-border-container"></div>
|
||||
<div id="project-map-app-placeholder" class="map-app-placeholder">
|
||||
<div class="cockpit-dock-live-container"></div>
|
||||
<div id="map-app-global" class="map-app-container"></div>
|
||||
<div class="cockpit-bottom-border-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-right">
|
||||
<div id="cockpit-app-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
// import { useStore } from 'vuex'
|
||||
|
||||
const Detail = ref(null)
|
||||
// const store = useStore()
|
||||
// const data = computed(() => store.state.mapStore.uavs.data)
|
||||
const ip = '198.16.74.210'
|
||||
|
||||
onMounted(() => {
|
||||
// ******* 接入步骤 4 - START:使用组件前初始化配置(具体配置参数请按实际替换) *******
|
||||
if(window.FH2) {
|
||||
window.FH2.initConfig({
|
||||
serverUrl: `http://${ip}:30812`,
|
||||
wssUrl: `ws://${ip}:30812/duplex/web`,
|
||||
hostUrl: `http://${ip}`,
|
||||
prjId: '4bd996b8-5201-4e5d-82b1-6879be360c20', // 项目 id,按实际传入
|
||||
projectToken: 'eyJhbGciOiJIUzUxMiIsImNyaXQiOlsidHlwIiwiYWxnIiwia2lkIl0sImtpZCI6IjU3YmQyNmEwLTYyMDktNGE5My1hNjg4LWY4NzUyYmU1ZDE5MSIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50IjoiIiwiZXhwIjoyMDc1OTQ3NzIyLCJuYmYiOjE3NjA0MTQ5MjIsIm9yZ2FuaXphdGlvbl91dWlkIjoiOWRmMjlmYTgtNGI5OS00MThlLWJhMmQtMGY5ZWY5ZWVlMzkyIiwicHJvamVjdF91dWlkIjoiIiwic3ViIjoiZmgyIiwidXNlcl9pZCI6IjE3NjA0MTQxMDkzNTcwMDI0MjkifQ.DC_aS37W2fkqOjCtfvysDfhTn-4XVn3_IrXBnPD9rICGyrIBKBG3oPldeW_pqele5H_gCn1EgM0KXcbDgvq-dw' // 组织密钥,按实际传入
|
||||
})
|
||||
|
||||
// ******* 接入步骤 5 - START:加载组件,传入航线ID *******
|
||||
window.FH2.loadCockpit('cockpit-app-container', {
|
||||
gateway_sn: data.value.gatewaySn, // 网关 sn,按实际传入
|
||||
drone_sn: data.value.droneSn // 飞机 sn,按实际传入
|
||||
})
|
||||
}
|
||||
// // 使用轮询方式等待内容加载完成后再滚动
|
||||
// waitForContentAndScroll()
|
||||
})
|
||||
const waitForContentAndScroll = () => {
|
||||
let attempts = 0
|
||||
let previousScrollWidth = 0
|
||||
let stableCount = 0
|
||||
const maxAttempts = 30
|
||||
const checkAndScroll = () => {
|
||||
attempts++
|
||||
|
||||
if (Detail.value) {
|
||||
// 检查目标容器是否已加载内容
|
||||
const mapContainer = document.getElementById('map-app-global')
|
||||
|
||||
// 判断内容是否加载完成
|
||||
const isContentLoaded = mapContainer && mapContainer.children.length > 0
|
||||
// console.log('Content loaded:', isContentLoaded, mapContainer)
|
||||
// 获取当前尺寸
|
||||
const currentScrollWidth = Detail.value.scrollWidth
|
||||
const clientWidth = Detail.value.clientWidth
|
||||
|
||||
// console.log('Size check:', {
|
||||
// attempt: attempts,
|
||||
// scrollWidth: currentScrollWidth,
|
||||
// clientWidth: clientWidth,
|
||||
// isContentLoaded: isContentLoaded
|
||||
// })
|
||||
|
||||
// 检查尺寸是否稳定(连续几次尺寸相同)
|
||||
if (currentScrollWidth === previousScrollWidth && currentScrollWidth > clientWidth) {
|
||||
stableCount++
|
||||
} else {
|
||||
stableCount = 0
|
||||
}
|
||||
previousScrollWidth = currentScrollWidth
|
||||
// 当内容加载完成且尺寸稳定,或者达到最大尝试次数时执行滚动
|
||||
if (isContentLoaded && stableCount >= 2 && currentScrollWidth > clientWidth ||
|
||||
attempts >= maxAttempts) {
|
||||
|
||||
// 强制重新计算布局
|
||||
forceLayoutRecalculation(() => {
|
||||
executeScroll()
|
||||
})
|
||||
} else {
|
||||
// 内容未加载完成或尺寸不稳定,继续等待
|
||||
setTimeout(checkAndScroll, 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 开始检查
|
||||
setTimeout(checkAndScroll, 300)
|
||||
}
|
||||
const forceLayoutRecalculation = (callback) => {
|
||||
if (!Detail.value) {
|
||||
if (callback) {
|
||||
callback()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 方法1: 强制重排
|
||||
Detail.value.style.display = 'none'
|
||||
void Detail.value.offsetHeight // 强制重排
|
||||
Detail.value.style.display = 'flex'
|
||||
|
||||
// 方法2: 临时修改overflow触发重新计算
|
||||
const originalOverflow = Detail.value.style.overflow
|
||||
Detail.value.style.overflow = 'hidden'
|
||||
void Detail.value.offsetWidth
|
||||
Detail.value.style.overflow = originalOverflow
|
||||
|
||||
setTimeout(() => {
|
||||
if (callback) {
|
||||
callback()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const executeScroll = () => {
|
||||
if (!Detail.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// 重新获取准确的尺寸
|
||||
const scrollWidth = Detail.value.scrollWidth
|
||||
const clientWidth = Detail.value.clientWidth
|
||||
const maxScroll = scrollWidth - clientWidth
|
||||
|
||||
// console.log('Final size calculation:', {
|
||||
// scrollWidth,
|
||||
// clientWidth,
|
||||
// maxScroll
|
||||
// })
|
||||
|
||||
if (maxScroll > 0) {
|
||||
const targetPosition = 1388
|
||||
const scrollPosition = targetPosition > maxScroll ? maxScroll : targetPosition
|
||||
|
||||
// 修复:正确处理 scrollPosition 为 0 的情况
|
||||
const finalPosition = typeof scrollPosition === 'number' ? scrollPosition : 645
|
||||
|
||||
// 激活滚动机制后再执行滚动
|
||||
activateScrollMechanism(() => {
|
||||
Detail.value.scrollTo({
|
||||
left: finalPosition,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
|
||||
// console.log('Scrolled to position:', finalPosition,
|
||||
// 'Max scroll:', maxScroll,
|
||||
// 'Container width:', clientWidth,
|
||||
// 'Scroll width:', scrollWidth)
|
||||
})
|
||||
} else {
|
||||
// console.log('No scrollable content, but content is loaded')
|
||||
// 即使没有滚动内容,也尝试激活滚动机制
|
||||
activateScrollMechanism(() => {
|
||||
Detail.value.scrollTo({
|
||||
left: 645,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const activateScrollMechanism = (callback) => {
|
||||
if (!Detail.value) {
|
||||
if (callback) {
|
||||
callback()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 预激活滚动机制
|
||||
Detail.value.scrollTo({ left: 1, behavior: 'instant' })
|
||||
|
||||
setTimeout(() => {
|
||||
Detail.value.scrollTo({ left: 0, behavior: 'instant' })
|
||||
|
||||
setTimeout(() => {
|
||||
if (callback) {
|
||||
callback()
|
||||
}
|
||||
}, 50)
|
||||
}, 20)
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.fh2-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#project-app-container>div, #cockpit-app-container>div,
|
||||
.map-app-container>div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.project-details {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: scroll;
|
||||
// &::-webkit-scrollbar {display: none;}
|
||||
}
|
||||
|
||||
.project-details .right-micro-app {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 1388px;
|
||||
}
|
||||
|
||||
.project-details .right-micro-app .maps-micro-app {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.project-details .right-micro-app .map-app-placeholder {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.project-details .right-micro-app .map-app-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.cockpit-app-ant-modal-root{
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
}
|
||||
.cockpit-app-ant-popover{
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
</style>
|
||||
665
src/views/business/drone/control.vue
Normal file
665
src/views/business/drone/control.vue
Normal file
File diff suppressed because one or more lines are too long
389
src/views/business/drone/device.vue
Normal file
389
src/views/business/drone/device.vue
Normal file
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div class="device-wrapper">
|
||||
|
||||
<div :class="['device-content', isFold ? 'fold' : '']">
|
||||
<div class="tab-wrapper">
|
||||
<div :class="['tab-button', current === index ? 'active' : '']" v-for="(item, index) in tabs" :key="index"
|
||||
@click="toggleDevice(index)">{{ item.label }}</div>
|
||||
</div>
|
||||
<div class="list-wrapper">
|
||||
<template v-if="current === 0">
|
||||
<el-tree :data="tableData" ref="treeRef" :props="{ label: 'videoName', children: 'children' }" node-key="id"
|
||||
@node-click="handleNodeClick">
|
||||
<template v-slot="{ node }">
|
||||
<img src="@/assets/images/livePreview/icon-monitor.png" alt="">
|
||||
<span>{{ node.label }}</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="list-item" v-for="item in tableData" :key="item.id" @click="handleDevice(item)">
|
||||
<span>{{ item.videoName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="fold-button" @click="toggleFold">
|
||||
<template v-if="isFold">
|
||||
<span>展开</span>
|
||||
<img src="@/assets/images/drone/icon-arrow-right.png" alt="">
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>收起</span>
|
||||
<img src="@/assets/images/drone/icon-arrow-left.png" alt="">
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { videoCameraFindPage, findUavPage, findEnvPage } from '@/api/device.js'
|
||||
import { Message } from '@element-plus/icons-vue'
|
||||
|
||||
const preview = reactive({
|
||||
visible: false,
|
||||
codes: []
|
||||
})
|
||||
|
||||
const current = ref(1)
|
||||
const isFold = ref(false)
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: '监控设备',
|
||||
value: 'monitor'
|
||||
},
|
||||
{
|
||||
label: '无人机设备',
|
||||
value: 'plane'
|
||||
},
|
||||
{
|
||||
label: '环境监测设备',
|
||||
value: 'environment'
|
||||
},
|
||||
{
|
||||
label: '感知设备',
|
||||
value: 'AIS'
|
||||
}
|
||||
]
|
||||
const tableData = ref([])
|
||||
|
||||
const initMonitor = () => {
|
||||
const params = new FormData()
|
||||
params.append('pageNo', 1)
|
||||
params.append('pageSize', 999)
|
||||
videoCameraFindPage(params).then(res => {
|
||||
if (res.success) {
|
||||
const data = res.result.records
|
||||
const groupedData = {}
|
||||
data.forEach(item => {
|
||||
const belong = item.videoBelong
|
||||
if (!groupedData[belong]) {
|
||||
groupedData[belong] = []
|
||||
}
|
||||
groupedData[belong].push(item)
|
||||
})
|
||||
|
||||
// 转换为树形结构
|
||||
const result = Object.keys(groupedData).map(belong => {
|
||||
const children = groupedData[belong]
|
||||
if (children.length === 1) {
|
||||
// 只有一个子元素,直接返回该子元素
|
||||
return children[0]
|
||||
}
|
||||
// 有多个子元素,返回分组节点
|
||||
return {
|
||||
videoName: belong,
|
||||
children: children
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
tableData.value = result
|
||||
} else {
|
||||
Message.error(res.msg || '查询失败!')
|
||||
}
|
||||
})
|
||||
}
|
||||
const initPlane = () => {
|
||||
const params = new FormData()
|
||||
params.append('pageNo', 1)
|
||||
params.append('pageSize', 999)
|
||||
findUavPage(params).then(res => {
|
||||
if (res.success) {
|
||||
tableData.value = res.result.records
|
||||
} else {
|
||||
Message.error(res.msg || '查询失败!')
|
||||
}
|
||||
})
|
||||
}
|
||||
const initEnvironment = () => {
|
||||
const params = new FormData()
|
||||
params.append('pageNo', 1)
|
||||
params.append('pageSize', 999)
|
||||
params.append('videoType', '航道流速设备,气象观测站设备')
|
||||
findEnvPage(params).then(res => {
|
||||
if (res.success) {
|
||||
tableData.value = res.result.records
|
||||
} else {
|
||||
Message.error(res.msg || '查询失败!')
|
||||
}
|
||||
})
|
||||
}
|
||||
const initAIS = () => {
|
||||
const params = new FormData()
|
||||
params.append('pageNo', 1)
|
||||
params.append('pageSize', 999)
|
||||
params.append('videoType', 'AIS基站设备')
|
||||
findEnvPage(params).then(res => {
|
||||
if (res.success) {
|
||||
tableData.value = res.result.records
|
||||
} else {
|
||||
Message.error(res.msg || '查询失败!')
|
||||
}
|
||||
})
|
||||
}
|
||||
const initMap = {
|
||||
'monitor': initMonitor,
|
||||
'plane': initPlane,
|
||||
'environment': initEnvironment,
|
||||
'AIS': initAIS
|
||||
}
|
||||
|
||||
const initData = () => {
|
||||
tableData.value = []
|
||||
const tabValue = tabs[current.value].value
|
||||
const initFunction = initMap[tabValue]
|
||||
if (initFunction) {
|
||||
try {
|
||||
initFunction()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
const toggleDevice = (index) => {
|
||||
current.value = index
|
||||
}
|
||||
const handleDevice = (item) => {
|
||||
preview.visible = true
|
||||
}
|
||||
const handleNodeClick = (node) => {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
// 雷云一体机一个视频
|
||||
if (node.videoName.indexOf('雷云') !== -1) {
|
||||
preview.codes = ['0f69d447002a40629e15f8a251fc46fa']
|
||||
} else {
|
||||
preview.codes = ['b7f9fff8e3504e419cb2f5fad5b2845c', '9b49bdc8988e430baad9ca727fed545a', 'e0a42cd6cbf940a6a4fc0aa789d43a04']
|
||||
}
|
||||
preview.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
const toggleFold = () => {
|
||||
isFold.value = !isFold.value
|
||||
}
|
||||
|
||||
watch(() => current.value, () => {
|
||||
initData()
|
||||
}, { immediate: true })
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.device-wrapper {
|
||||
position: absolute;
|
||||
left: 45px;
|
||||
top: 89px;
|
||||
bottom: 411px;
|
||||
|
||||
.device-content {
|
||||
width: 401px;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
background: linear-gradient( 90deg, #0C1929 0%, rgba(12,25,41,0.6) 100%);
|
||||
border-radius: 0px 0px 0px 0px;
|
||||
border: 1px solid;
|
||||
border-image: linear-gradient(270deg, rgba(42, 159, 255, 0.2), rgba(42, 159, 255, 0.8)) 1 1;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
&.fold {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.tab-wrapper {
|
||||
width: 28px;
|
||||
height: 100%;
|
||||
|
||||
.tab-button {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
// height: 130px;
|
||||
color: #FFFFFF;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
line-height: 28px;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: upright;
|
||||
background-color: #2061bd;
|
||||
border-radius: 3px 0 0 28px;
|
||||
cursor: pointer;
|
||||
margin-bottom: -7px;
|
||||
opacity: .6;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
border-radius: 0 28px 0 28px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
position: absolute;
|
||||
background: radial-gradient(circle at 28px 3px, transparent 28px, #2162bd 28px);
|
||||
top: -24px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
position: absolute;
|
||||
background: radial-gradient(circle at 0px 25px, transparent 28px, #2061bd 28px);
|
||||
bottom: -24px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #2061BD;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
overflow: auto;
|
||||
background: transparent;
|
||||
|
||||
.list-item {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding-left: 20px;
|
||||
background-color: rgba(24, 128, 254, 0.3);
|
||||
border-radius: 3px 3px 3px 3px;
|
||||
font-size: 14px;
|
||||
color: #E1F1FAFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(24, 128, 254, 0);
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-image: url('@/assets/images/livePreview/icon-monitor.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
color: #0EF8F8FF;
|
||||
background-color: rgba(24, 128, 254, 0.5);
|
||||
border: 1px solid #5FA4FF;
|
||||
|
||||
&::before {
|
||||
background-image: url('@/assets/images/livePreview/icon-monitor-active.png');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-tree {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node) {
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
background-color: rgba(24, 128, 254, 0.3);
|
||||
border-radius: 3px 3px 3px 3px;
|
||||
font-size: 14px;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(24, 128, 254, 0);
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
|
||||
.el-tree-node__content {
|
||||
height: 44px;
|
||||
border: 1px solid transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #E1F1FAFF;
|
||||
}
|
||||
|
||||
&:focus>.el-tree-node__content,
|
||||
.el-tree-node__content:hover {
|
||||
color: #0EF8F8FF;
|
||||
background-color: rgba(24, 128, 254, 0.5);
|
||||
border: 1px solid #5FA4FF;
|
||||
|
||||
&::before {
|
||||
background-image: url('@/assets/images/livePreview/icon-monitor-active.png');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.fold-button {
|
||||
position: absolute;
|
||||
right: -17px;
|
||||
top: 0;
|
||||
width: 17px;
|
||||
height: 82px;
|
||||
background-image: url('@/assets/images/drone/subscript-background-vertical.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
color: #FFFFFFCC;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
16
src/views/business/drone/index.vue
Normal file
16
src/views/business/drone/index.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
<DeviceComponent/>
|
||||
<ControlComponent/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import DeviceComponent from './device.vue'
|
||||
import ControlComponent from './control.vue'
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user