feat: 新增地图选点组件
This commit is contained in:
parent
d1acb12b40
commit
a4d97d7e56
@ -82,11 +82,12 @@
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="路况位置" prop="event.roadConditionLocation">
|
||||
<el-input v-model="formData.event.roadConditionLocation" placeholder="请选择">
|
||||
<template #suffix>
|
||||
<el-icon class="location-icon"><LocationFilled /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<PositionPickerDialog
|
||||
v-model="formData.event.roadConditionLocation"
|
||||
:initial-longitude="formData.event.startStakeLng"
|
||||
:initial-latitude="formData.event.startStakeLat"
|
||||
@change="() => formRef?.validateField('event.roadConditionLocation')"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
@ -250,10 +251,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { LocationFilled } from '@element-plus/icons-vue'
|
||||
import BlockItem from '@/component/BlockItem.vue'
|
||||
import FileUpload from '@/component/FileUpload/FileUpload.vue'
|
||||
import NumberInput from '@/component/NumberInput/NumberInput.vue'
|
||||
import PositionPickerDialog from '../components/PositionPickerDialog.vue'
|
||||
import RoadRoutesSelect from '../components/RoadRoutesSelect.vue'
|
||||
import YHZSelect from '../components/YHZSelect.vue'
|
||||
import MaterialList from '../components/MaterialList.vue'
|
||||
@ -321,10 +322,6 @@ const {
|
||||
}
|
||||
}
|
||||
|
||||
.location-icon {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.unit-text {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
|
||||
@ -68,7 +68,7 @@ export const useIceDisasterReport = () => {
|
||||
'event.district': [{ required: true, message: '请选择所属区县', trigger: 'change' }],
|
||||
'event.routeNo': [{ required: true, message: '请选择线路编号', trigger: 'change' }],
|
||||
'event.occurLocation': [{ required: true, message: '请输入发生地点', trigger: 'blur' }],
|
||||
'event.roadConditionLocation': [{ required: true, message: '请输入路况位置', trigger: 'blur' }],
|
||||
'event.roadConditionLocation': [{ required: true, message: '请选择路况位置', trigger: ['change', 'blur'] }],
|
||||
'event.startStakeNo': [{ required: true, message: '请输入起点桩号', trigger: 'blur' }],
|
||||
'event.startStakeLng': [{ required: true, message: '请输入起点桩经度', trigger: 'blur' }],
|
||||
'event.startStakeLat': [{ required: true, message: '请输入起点桩纬度', trigger: 'blur' }],
|
||||
|
||||
@ -118,11 +118,12 @@
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="路况位置" prop="event.occurLocation">
|
||||
<el-input v-model="formData.event.occurLocation" placeholder="请选择">
|
||||
<template #suffix>
|
||||
<el-icon class="location-icon"><LocationFilled /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<PositionPickerDialog
|
||||
v-model="formData.event.occurLocation"
|
||||
:initial-longitude="formData.event.startStakeLng"
|
||||
:initial-latitude="formData.event.startStakeLat"
|
||||
@change="() => formRef?.validateField('event.occurLocation')"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
@ -337,10 +338,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { LocationFilled } from '@element-plus/icons-vue'
|
||||
import BlockItem from '@/component/BlockItem.vue'
|
||||
import FileUpload from '@/component/FileUpload/FileUpload.vue'
|
||||
import NumberInput from '@/component/NumberInput/NumberInput.vue'
|
||||
import PositionPickerDialog from '../components/PositionPickerDialog.vue'
|
||||
import RoadRoutesSelect from '../components/RoadRoutesSelect.vue'
|
||||
import YHZSelect from '../components/YHZSelect.vue'
|
||||
import LossList from './WaterDisasterLossListPC.vue'
|
||||
@ -409,10 +410,6 @@ const {
|
||||
}
|
||||
}
|
||||
|
||||
.location-icon {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.unit-text {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
|
||||
@ -50,7 +50,7 @@ export const useWaterDisasterReport = () => {
|
||||
'event.damageCount': [{ required: true, message: '请输入水毁处数', trigger: 'blur' }],
|
||||
'event.district': [{ required: true, message: '请选择所属区县', trigger: 'change' }],
|
||||
'event.routeNo': [{ required: true, message: '请选择线路编号', trigger: 'change' }],
|
||||
'event.occurLocation': [{ required: true, message: '请输入路况位置', trigger: 'blur' }],
|
||||
'event.occurLocation': [{ required: true, message: '请选择路况位置', trigger: ['change', 'blur'] }],
|
||||
'event.blockedPointName': [{ required: true, message: '请输入阻断点小地名', trigger: 'blur' }],
|
||||
'event.startStakeNo': [{ required: true, message: '请输入起点桩号', trigger: 'blur' }],
|
||||
'event.startStakeLng': [{ required: true, message: '请输入起点桩经度', trigger: 'blur' }],
|
||||
|
||||
@ -0,0 +1,529 @@
|
||||
<template>
|
||||
<div class="position-picker" @click="openDialog">
|
||||
<el-input :model-value="modelValue" :placeholder="placeholder" readonly>
|
||||
<template #suffix>
|
||||
<el-icon class="trigger-icon"><LocationFilled /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="坐标点选取"
|
||||
width="72%"
|
||||
top="6vh"
|
||||
class="position-dialog"
|
||||
:append-to-body="true"
|
||||
@opened="handleDialogOpened"
|
||||
>
|
||||
<div class="dialog-toolbar">
|
||||
<div class="toolbar-item">
|
||||
<span class="toolbar-label">经度:</span>
|
||||
<el-input :model-value="displayLongitude" readonly />
|
||||
</div>
|
||||
<div class="toolbar-item">
|
||||
<span class="toolbar-label">纬度:</span>
|
||||
<el-input :model-value="displayLatitude" readonly />
|
||||
</div>
|
||||
<div class="toolbar-item toolbar-item--address">
|
||||
<span class="toolbar-label">地点:</span>
|
||||
<el-input v-model="searchKeyword" placeholder="请输入地点名称" @keyup.enter="handleSearch" />
|
||||
</div>
|
||||
<el-button type="primary" :loading="searching" @click.stop="handleSearch">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<div class="map-panel">
|
||||
<div ref="mapContainerRef" class="map-container"></div>
|
||||
<div v-if="mapError" class="map-state map-state--error">
|
||||
<p>{{ mapError }}</p>
|
||||
<el-button type="primary" plain @click="retryMapLoad">重新加载</el-button>
|
||||
</div>
|
||||
<div v-else-if="!leafletReady" class="map-state">地图加载中...</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-tip">支持地图点击选点和地点搜索,确定后会回填到路况位置。</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import L from 'leaflet'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png'
|
||||
import markerIcon from 'leaflet/dist/images/marker-icon.png'
|
||||
import markerShadow from 'leaflet/dist/images/marker-shadow.png'
|
||||
import { LocationFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { AMAP_CONFIG, AMAP_ENDPOINTS } from '../../3DSituationalAwarenessRefactor/config/amap'
|
||||
|
||||
const DEFAULT_CENTER = {
|
||||
longitude: 106.551643,
|
||||
latitude: 29.563761
|
||||
}
|
||||
|
||||
delete L.Icon.Default.prototype._getIconUrl
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: markerIcon2x,
|
||||
iconUrl: markerIcon,
|
||||
shadowUrl: markerShadow
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择'
|
||||
},
|
||||
initialLongitude: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
initialLatitude: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const leafletReady = ref(false)
|
||||
const mapError = ref('')
|
||||
const searching = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const mapContainerRef = ref(null)
|
||||
|
||||
const selectedLongitude = ref(null)
|
||||
const selectedLatitude = ref(null)
|
||||
const selectedAddress = ref('')
|
||||
|
||||
const draftLongitude = ref(null)
|
||||
const draftLatitude = ref(null)
|
||||
const draftAddress = ref('')
|
||||
|
||||
let mapInstance = null
|
||||
let markerInstance = null
|
||||
let reverseGeocodeToken = 0
|
||||
|
||||
const normalizeCoordinate = (value) => {
|
||||
if (value === '' || value === null || value === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
const normalized = Number(value)
|
||||
return Number.isFinite(normalized) ? normalized : null
|
||||
}
|
||||
|
||||
const displayLongitude = computed(() => {
|
||||
return draftLongitude.value === null ? '' : draftLongitude.value.toFixed(6)
|
||||
})
|
||||
|
||||
const displayLatitude = computed(() => {
|
||||
return draftLatitude.value === null ? '' : draftLatitude.value.toFixed(6)
|
||||
})
|
||||
|
||||
const currentCenter = () => {
|
||||
const longitude = selectedLongitude.value ?? normalizeCoordinate(props.initialLongitude) ?? DEFAULT_CENTER.longitude
|
||||
const latitude = selectedLatitude.value ?? normalizeCoordinate(props.initialLatitude) ?? DEFAULT_CENTER.latitude
|
||||
|
||||
return {
|
||||
longitude,
|
||||
latitude
|
||||
}
|
||||
}
|
||||
|
||||
const resolveInitialSelection = () => {
|
||||
if (selectedLongitude.value !== null && selectedLatitude.value !== null) {
|
||||
return {
|
||||
longitude: selectedLongitude.value,
|
||||
latitude: selectedLatitude.value
|
||||
}
|
||||
}
|
||||
|
||||
const longitude = normalizeCoordinate(props.initialLongitude)
|
||||
const latitude = normalizeCoordinate(props.initialLatitude)
|
||||
if (longitude !== null && latitude !== null) {
|
||||
return {
|
||||
longitude,
|
||||
latitude
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const syncDraftFromSelected = () => {
|
||||
const initialSelection = resolveInitialSelection()
|
||||
draftLongitude.value = initialSelection?.longitude ?? null
|
||||
draftLatitude.value = initialSelection?.latitude ?? null
|
||||
draftAddress.value = selectedAddress.value || props.modelValue || ''
|
||||
searchKeyword.value = draftAddress.value
|
||||
}
|
||||
|
||||
const setMarkerPosition = (longitude, latitude, shouldPan = true) => {
|
||||
if (!mapInstance || longitude === null || latitude === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const latLng = [latitude, longitude]
|
||||
if (!markerInstance) {
|
||||
markerInstance = L.marker(latLng, {
|
||||
draggable: true
|
||||
}).addTo(mapInstance)
|
||||
|
||||
markerInstance.on('dragend', () => {
|
||||
const markerLatLng = markerInstance.getLatLng()
|
||||
updateDraftPosition(markerLatLng.lng, markerLatLng.lat, true)
|
||||
})
|
||||
} else {
|
||||
markerInstance.setLatLng(latLng)
|
||||
}
|
||||
|
||||
if (shouldPan) {
|
||||
mapInstance.setView(latLng, Math.max(mapInstance.getZoom(), 15))
|
||||
}
|
||||
}
|
||||
|
||||
const reverseGeocode = async (longitude, latitude) => {
|
||||
const token = ++reverseGeocodeToken
|
||||
const url = `${AMAP_CONFIG.baseUrl}${AMAP_ENDPOINTS.regeocode}`
|
||||
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
params: {
|
||||
key: AMAP_CONFIG.webServiceKey,
|
||||
location: `${longitude},${latitude}`,
|
||||
extensions: 'base',
|
||||
output: 'json'
|
||||
},
|
||||
timeout: AMAP_CONFIG.timeout
|
||||
})
|
||||
|
||||
if (token !== reverseGeocodeToken) {
|
||||
return
|
||||
}
|
||||
|
||||
if (response.data.status !== '1') {
|
||||
throw new Error(response.data.info || '逆地理编码失败')
|
||||
}
|
||||
|
||||
const formattedAddress = response.data.regeocode?.formatted_address || ''
|
||||
draftAddress.value = formattedAddress
|
||||
searchKeyword.value = formattedAddress
|
||||
} catch (_error) {
|
||||
if (token !== reverseGeocodeToken) {
|
||||
return
|
||||
}
|
||||
|
||||
draftAddress.value = draftAddress.value || `${latitude.toFixed(6)}, ${longitude.toFixed(6)}`
|
||||
}
|
||||
}
|
||||
|
||||
const updateDraftPosition = (longitude, latitude, shouldResolveAddress = false) => {
|
||||
draftLongitude.value = Number(longitude.toFixed(6))
|
||||
draftLatitude.value = Number(latitude.toFixed(6))
|
||||
setMarkerPosition(draftLongitude.value, draftLatitude.value, false)
|
||||
|
||||
if (shouldResolveAddress) {
|
||||
reverseGeocode(draftLongitude.value, draftLatitude.value)
|
||||
}
|
||||
}
|
||||
|
||||
const initMap = async () => {
|
||||
mapError.value = ''
|
||||
leafletReady.value = false
|
||||
|
||||
try {
|
||||
await nextTick()
|
||||
|
||||
if (!mapContainerRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const center = currentCenter()
|
||||
|
||||
if (!mapInstance) {
|
||||
mapInstance = L.map(mapContainerRef.value, {
|
||||
zoomControl: true,
|
||||
attributionControl: false
|
||||
}).setView([center.latitude, center.longitude], 13)
|
||||
|
||||
L.tileLayer('https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', {
|
||||
subdomains: ['1', '2', '3', '4'],
|
||||
maxZoom: 18,
|
||||
minZoom: 3
|
||||
}).addTo(mapInstance)
|
||||
|
||||
mapInstance.on('click', (event) => {
|
||||
updateDraftPosition(event.latlng.lng, event.latlng.lat, true)
|
||||
})
|
||||
} else {
|
||||
mapInstance.setView([center.latitude, center.longitude], mapInstance.getZoom() || 13)
|
||||
}
|
||||
|
||||
if (draftLongitude.value !== null && draftLatitude.value !== null) {
|
||||
setMarkerPosition(draftLongitude.value, draftLatitude.value)
|
||||
} else if (markerInstance) {
|
||||
markerInstance.remove()
|
||||
markerInstance = null
|
||||
}
|
||||
leafletReady.value = true
|
||||
requestAnimationFrame(() => {
|
||||
mapInstance?.invalidateSize()
|
||||
})
|
||||
|
||||
if (!draftAddress.value && draftLongitude.value !== null && draftLatitude.value !== null) {
|
||||
reverseGeocode(draftLongitude.value, draftLatitude.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('地图初始化失败:', error)
|
||||
mapError.value = '地图初始化失败,请检查网络后重试'
|
||||
}
|
||||
}
|
||||
|
||||
const openDialog = () => {
|
||||
syncDraftFromSelected()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDialogOpened = async () => {
|
||||
await initMap()
|
||||
requestAnimationFrame(() => {
|
||||
mapInstance?.invalidateSize()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSearch = async () => {
|
||||
const keyword = searchKeyword.value.trim()
|
||||
if (!keyword) {
|
||||
ElMessage.warning('请输入地点名称')
|
||||
return
|
||||
}
|
||||
|
||||
searching.value = true
|
||||
try {
|
||||
const url = `${AMAP_CONFIG.baseUrl}${AMAP_ENDPOINTS.geocode}`
|
||||
const response = await axios.get(url, {
|
||||
params: {
|
||||
key: AMAP_CONFIG.webServiceKey,
|
||||
address: keyword,
|
||||
output: 'json'
|
||||
},
|
||||
timeout: AMAP_CONFIG.timeout
|
||||
})
|
||||
|
||||
if (response.data.status !== '1') {
|
||||
throw new Error(response.data.info || '地点搜索失败')
|
||||
}
|
||||
|
||||
const geocode = response.data.geocodes?.[0]
|
||||
if (!geocode?.location) {
|
||||
throw new Error('未找到对应地点')
|
||||
}
|
||||
|
||||
const [longitude, latitude] = geocode.location.split(',').map(Number)
|
||||
draftAddress.value = geocode.formatted_address || keyword
|
||||
searchKeyword.value = draftAddress.value
|
||||
updateDraftPosition(longitude, latitude)
|
||||
setMarkerPosition(longitude, latitude)
|
||||
} catch (error) {
|
||||
console.error('地点搜索失败:', error)
|
||||
ElMessage.error(error.message || '地点搜索失败,请重试')
|
||||
} finally {
|
||||
searching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const retryMapLoad = () => {
|
||||
initMap()
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (draftLongitude.value === null || draftLatitude.value === null) {
|
||||
ElMessage.warning('请先选择坐标点')
|
||||
return
|
||||
}
|
||||
|
||||
selectedLongitude.value = draftLongitude.value
|
||||
selectedLatitude.value = draftLatitude.value
|
||||
selectedAddress.value = draftAddress.value || searchKeyword.value.trim() || `${draftLatitude.value}, ${draftLongitude.value}`
|
||||
|
||||
emit('update:modelValue', selectedAddress.value)
|
||||
emit('change', {
|
||||
longitude: selectedLongitude.value,
|
||||
latitude: selectedLatitude.value,
|
||||
address: selectedAddress.value
|
||||
})
|
||||
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedAddress.value = value
|
||||
if (!dialogVisible.value) {
|
||||
draftAddress.value = value
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (mapInstance) {
|
||||
mapInstance.remove()
|
||||
mapInstance = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.position-picker {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.trigger-icon {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.dialog-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.2fr) auto;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.toolbar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.toolbar-item--address {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.toolbar-label {
|
||||
flex-shrink: 0;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.map-panel {
|
||||
position: relative;
|
||||
height: min(600px, calc(100vh - 290px));
|
||||
min-height: 320px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map-state {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
background: rgba(245, 247, 250, 0.92);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.map-state--error {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.dialog-tip {
|
||||
margin-top: 12px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
:deep(.leaflet-container) {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
:deep(.leaflet-control-zoom a) {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
:deep(.position-dialog) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.position-dialog .el-dialog) {
|
||||
max-height: 88vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.position-dialog .el-dialog__body) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-height: 900px) {
|
||||
.map-panel {
|
||||
height: calc(100vh - 320px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.dialog-toolbar {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.dialog-toolbar {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.map-panel {
|
||||
height: calc(100vh - 400px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user