206.1.13最终vue(1)
This commit is contained in:
@@ -1,15 +1,690 @@
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, onUnmounted, watch, nextTick } from 'vue'
|
||||
|
||||
interface RegionTopPhoneItem {
|
||||
rowIndex: number
|
||||
colIndex: number
|
||||
centerLon: number
|
||||
centerLat: number
|
||||
phoneModel: string
|
||||
os: string
|
||||
osVersion: string
|
||||
totalTraffic: number
|
||||
recordCount: number
|
||||
}
|
||||
|
||||
const mapContainer = ref<HTMLDivElement | null>(null)
|
||||
const operator = ref<string>('CTCC') // 默认选择中国电信
|
||||
const networkType = ref<string>('')
|
||||
const minUploadTraffic = ref<number | null>(null)
|
||||
const maxUploadTraffic = ref<number | null>(null)
|
||||
const minDownloadTraffic = ref<number | null>(null)
|
||||
const maxDownloadTraffic = ref<number | null>(null)
|
||||
const startDate = ref<string>('')
|
||||
const endDate = ref<string>('')
|
||||
const minTrafficThreshold = ref<number>(1000) // 预设流量阈值(KB)
|
||||
const gridRows = ref<number>(20) // 网格行数
|
||||
const gridCols = ref<number>(15) // 网格列数
|
||||
const loading = ref<boolean>(false)
|
||||
const dataLoaded = ref<boolean>(false)
|
||||
const dataList = ref<RegionTopPhoneItem[]>([])
|
||||
|
||||
let map: any = null
|
||||
let overlays: any[] = []
|
||||
let updateMarkersRetryCount = 0 // 重试计数器
|
||||
const MAX_UPDATE_RETRIES = 10 // 最大重试次数
|
||||
// 定义百度地图类型别名,简化访问
|
||||
const BMap = (window as any).BMap
|
||||
|
||||
// 初始化地图(参考需求7的实现方式)
|
||||
const initMap = async () => {
|
||||
await nextTick()
|
||||
|
||||
// 轮询检查百度地图API是否已加载
|
||||
if (!(window as any).BMap) {
|
||||
console.warn('百度地图API未加载,200ms后重试...')
|
||||
setTimeout(initMap, 200)
|
||||
return
|
||||
}
|
||||
else {
|
||||
console.log('百度地图API已加载')
|
||||
}
|
||||
|
||||
if (!mapContainer.value) {
|
||||
console.warn('地图容器未找到,等待100ms后重试')
|
||||
setTimeout(initMap, 100)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('地图容器已找到,开始创建地图实例')
|
||||
|
||||
try {
|
||||
const BMap = (window as any).BMap
|
||||
console.log('开始创建地图实例,容器:', mapContainer.value)
|
||||
|
||||
// 如果之前有地图实例,先清理
|
||||
if (map) {
|
||||
console.log('检测到已有地图实例,先清理')
|
||||
try {
|
||||
map.clearOverlays()
|
||||
} catch (e) {
|
||||
console.warn('清理旧地图实例失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
map = new BMap.Map(mapContainer.value)
|
||||
console.log('地图实例创建成功')
|
||||
|
||||
map.centerAndZoom(new BMap.Point(105.8951, 36.5667), 5) // 默认中国中心
|
||||
console.log('地图中心点设置完成')
|
||||
|
||||
map.enableScrollWheelZoom(true)
|
||||
map.enableDragging(true)
|
||||
console.log('地图交互功能启用完成')
|
||||
|
||||
map.addControl(new BMap.MapTypeControl({ mapTypes: [BMap.MapTypeId?.NORMAL || 1, BMap.MapTypeId?.SATELLITE || 2] }))
|
||||
map.addControl(new BMap.NavigationControl())
|
||||
map.addControl(new BMap.ScaleControl())
|
||||
console.log('地图控件添加完成')
|
||||
|
||||
// 监听地图移动和缩放事件,更新标记(增加防抖和容错)
|
||||
let updateTimer: any = null
|
||||
const debouncedUpdateMarkers = () => {
|
||||
if (updateTimer) clearTimeout(updateTimer)
|
||||
updateTimer = setTimeout(() => {
|
||||
updateMarkers()
|
||||
}, 300)
|
||||
}
|
||||
map.addEventListener('moveend', debouncedUpdateMarkers)
|
||||
map.addEventListener('zoomend', debouncedUpdateMarkers)
|
||||
|
||||
console.log('地图初始化完成')
|
||||
|
||||
// 地图初始化后立即获取数据
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('地图初始化过程中出错:', error)
|
||||
// 即使出错也尝试获取数据
|
||||
if (map) {
|
||||
console.log('地图实例存在,尝试获取数据')
|
||||
fetchData()
|
||||
} else {
|
||||
console.error('地图实例创建失败,无法获取数据')
|
||||
loading.value = false
|
||||
dataLoaded.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建手机logo图标(使用文字作为占位符)
|
||||
const createPhoneIcon = (phoneModel: string, traffic: number): string => {
|
||||
// 使用手机型号的版本号部分(如android4.1.2中的4.1.2)
|
||||
let displayText = phoneModel
|
||||
if (phoneModel.toLowerCase().includes('android')) {
|
||||
// 提取版本号部分
|
||||
const versionMatch = phoneModel.match(/android(\d+\.\d+\.\d+|\d+\.\d+)/i)
|
||||
if (versionMatch) {
|
||||
displayText = versionMatch[1] // 只显示版本号,如4.1.2
|
||||
} else {
|
||||
// 如果没有匹配到版本号,使用前几个字符
|
||||
displayText = phoneModel.length > 4 ? phoneModel.substring(0, 4) : phoneModel
|
||||
}
|
||||
} else {
|
||||
displayText = phoneModel.length > 4 ? phoneModel.substring(0, 4) : phoneModel
|
||||
}
|
||||
|
||||
const size = 40
|
||||
const fontSize = 11
|
||||
|
||||
// 修复XML字符串中的换行问题,避免base64编码错误
|
||||
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
||||
<rect width="${size}" height="${size}" rx="8" fill="#4b9cff" opacity="0.9"/>
|
||||
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle"
|
||||
font-size="${fontSize}" font-weight="bold" fill="white">${displayText}</text>
|
||||
<text x="50%" y="75%" text-anchor="middle" dominant-baseline="middle"
|
||||
font-size="8" fill="white" opacity="0.8">${(traffic / 1000).toFixed(1)}K</text>
|
||||
</svg>`
|
||||
|
||||
return `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgContent)))}`
|
||||
}
|
||||
|
||||
// 更新地图标记(增加容错和延迟处理)
|
||||
const updateMarkers = () => {
|
||||
if (!map || !dataLoaded.value || dataList.value.length === 0) {
|
||||
// 如果没有数据,只清除覆盖物
|
||||
overlays.forEach(overlay => {
|
||||
try {
|
||||
map?.removeOverlay(overlay)
|
||||
} catch (error) {
|
||||
console.warn('移除覆盖物失败:', error)
|
||||
}
|
||||
})
|
||||
overlays = []
|
||||
return
|
||||
}
|
||||
|
||||
// 清除旧的覆盖物
|
||||
overlays.forEach(overlay => {
|
||||
try {
|
||||
map.removeOverlay(overlay)
|
||||
} catch (error) {
|
||||
console.warn('移除覆盖物失败:', error)
|
||||
}
|
||||
})
|
||||
overlays = []
|
||||
|
||||
// 获取地图可视区域 - 增加容错处理
|
||||
let bounds: any = null
|
||||
try {
|
||||
bounds = map.getBounds()
|
||||
// 等待地图边界稳定(解决刚初始化时边界不完整问题)
|
||||
if (!bounds) {
|
||||
if (updateMarkersRetryCount < MAX_UPDATE_RETRIES) {
|
||||
updateMarkersRetryCount++
|
||||
console.log(`地图边界未获取到,${updateMarkersRetryCount}/${MAX_UPDATE_RETRIES}次重试`)
|
||||
setTimeout(() => updateMarkers(), 500)
|
||||
} else {
|
||||
console.warn('地图边界获取失败,已达到最大重试次数,停止重试')
|
||||
updateMarkersRetryCount = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
if (updateMarkersRetryCount < MAX_UPDATE_RETRIES) {
|
||||
updateMarkersRetryCount++
|
||||
console.warn(`获取地图边界失败,${updateMarkersRetryCount}/${MAX_UPDATE_RETRIES}次重试:`, error)
|
||||
setTimeout(() => updateMarkers(), 500)
|
||||
} else {
|
||||
console.warn('获取地图边界失败,已达到最大重试次数,停止重试')
|
||||
updateMarkersRetryCount = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const sw = bounds.getSouthWest()
|
||||
const ne = bounds.getNorthEast()
|
||||
|
||||
if (!sw || !ne || !sw.lat || !sw.lng || !ne.lat || !ne.lng) {
|
||||
if (updateMarkersRetryCount < MAX_UPDATE_RETRIES) {
|
||||
updateMarkersRetryCount++
|
||||
console.warn(`地图边界数据不完整,${updateMarkersRetryCount}/${MAX_UPDATE_RETRIES}次重试更新标记`)
|
||||
setTimeout(() => updateMarkers(), 500)
|
||||
} else {
|
||||
console.warn('地图边界数据不完整,已达到最大重试次数,停止重试')
|
||||
updateMarkersRetryCount = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 重置重试计数器(成功获取边界后)
|
||||
updateMarkersRetryCount = 0
|
||||
|
||||
// 过滤出在可视区域内且流量超过阈值的数据
|
||||
let visibleData = dataList.value.filter(item => {
|
||||
// 检查是否在可视区域内
|
||||
const inBounds = item.centerLat >= sw.lat && item.centerLat <= ne.lat &&
|
||||
item.centerLon >= sw.lng && item.centerLon <= ne.lng
|
||||
// 检查流量是否超过阈值
|
||||
const exceedsThreshold = item.totalTraffic >= minTrafficThreshold.value
|
||||
return inBounds && exceedsThreshold
|
||||
})
|
||||
|
||||
// 再次限制最多显示100条,确保性能
|
||||
if (visibleData.length > 100) {
|
||||
visibleData = visibleData.slice(0, 100)
|
||||
console.log(`可视区域内数据过多,限制显示前100条`)
|
||||
}
|
||||
|
||||
console.log(`准备显示 ${visibleData.length} 个标记点`)
|
||||
|
||||
// 在地图上显示手机logo(使用类型断言避免错误)
|
||||
const BMap = (window as any).BMap
|
||||
visibleData.forEach(item => {
|
||||
try {
|
||||
const point = new BMap.Point(item.centerLon, item.centerLat)
|
||||
|
||||
const icon = new BMap.Icon(
|
||||
createPhoneIcon(item.phoneModel, item.totalTraffic),
|
||||
new BMap.Size(40, 40),
|
||||
{ anchor: new BMap.Size(20, 20) }
|
||||
)
|
||||
|
||||
const marker = new BMap.Marker(point, { icon })
|
||||
|
||||
// 信息窗口
|
||||
const infoWindow = new BMap.InfoWindow(`
|
||||
<div style="padding:8px">
|
||||
<p><strong>手机型号:</strong> ${item.phoneModel}</p>
|
||||
<p><strong>操作系统:</strong> ${item.os}</p>
|
||||
<p><strong>系统版本:</strong> ${item.osVersion || '未指定'}</p>
|
||||
<p><strong>总流量:</strong> ${(item.totalTraffic / 1024).toFixed(2)} MB</p>
|
||||
<p><strong>记录数:</strong> ${item.recordCount}</p>
|
||||
<p><strong>区域:</strong> 第${item.rowIndex + 1}行, 第${item.colIndex + 1}列</p>
|
||||
<p><strong>坐标:</strong> ${item.centerLat.toFixed(6)}, ${item.centerLon.toFixed(6)}</p>
|
||||
</div>
|
||||
`, { width: 200 })
|
||||
|
||||
marker.addEventListener('click', () => map.openInfoWindow(infoWindow, point))
|
||||
map.addOverlay(marker)
|
||||
overlays.push(marker)
|
||||
} catch (error) {
|
||||
console.warn(`创建标记失败 (${item.phoneModel}):`, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
console.log('fetchData 函数被调用')
|
||||
loading.value = true
|
||||
dataLoaded.value = false
|
||||
|
||||
// 清除旧数据
|
||||
dataList.value = []
|
||||
overlays.forEach(overlay => {
|
||||
try {
|
||||
map?.removeOverlay(overlay)
|
||||
} catch (error) {
|
||||
console.warn('移除覆盖物失败:', error)
|
||||
}
|
||||
})
|
||||
overlays = []
|
||||
|
||||
try {
|
||||
const body: any = {}
|
||||
|
||||
// 运营商筛选
|
||||
if (operator.value && operator.value !== 'ALL') {
|
||||
body.operator = operator.value
|
||||
}
|
||||
|
||||
// 网络制式筛选
|
||||
if (networkType.value) {
|
||||
body.networkType = networkType.value
|
||||
}
|
||||
|
||||
// 流量范围(手机流量)
|
||||
if (minUploadTraffic.value !== null) {
|
||||
body.minUploadTraffic = minUploadTraffic.value
|
||||
}
|
||||
if (maxUploadTraffic.value !== null) {
|
||||
body.maxUploadTraffic = maxUploadTraffic.value
|
||||
}
|
||||
if (minDownloadTraffic.value !== null) {
|
||||
body.minDownloadTraffic = minDownloadTraffic.value
|
||||
}
|
||||
if (maxDownloadTraffic.value !== null) {
|
||||
body.maxDownloadTraffic = maxDownloadTraffic.value
|
||||
}
|
||||
|
||||
// 日期范围
|
||||
if (startDate.value) {
|
||||
body.startDate = startDate.value
|
||||
}
|
||||
if (endDate.value) {
|
||||
body.endDate = endDate.value
|
||||
}
|
||||
|
||||
// 网格大小
|
||||
body.gridRows = gridRows.value
|
||||
body.gridCols = gridCols.value
|
||||
|
||||
console.log('准备发送请求,请求体:', body)
|
||||
console.log('请求URL: http://localhost:8081/appTraffic/phoneRegionTop')
|
||||
|
||||
const response = await fetch('http://localhost:8081/appTraffic/phoneRegionTop', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
console.log('收到响应,状态码:', response.status)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json() as RegionTopPhoneItem[]
|
||||
console.log('收到数据:', data.length, '条')
|
||||
|
||||
// 限制只显示前100条数据,避免数据量太大导致地图加载缓慢
|
||||
dataList.value = data.slice(0, 100)
|
||||
console.log('限制显示:', dataList.value.length, '条')
|
||||
|
||||
// 地图已初始化:更新标记
|
||||
if (map) {
|
||||
await nextTick()
|
||||
// 重置重试计数器
|
||||
updateMarkersRetryCount = 0
|
||||
// 增加延迟,确保地图完全渲染后再更新标记
|
||||
setTimeout(() => {
|
||||
updateMarkers()
|
||||
}, 500) // 延迟500ms,确保地图完全加载
|
||||
} else {
|
||||
console.warn('地图未初始化,等待地图初始化后再更新标记')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
alert(`获取数据失败: ${error instanceof Error ? error.message : String(error)}`)
|
||||
dataList.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
dataLoaded.value = true
|
||||
console.log('数据加载完成,loading状态:', loading.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听筛选条件变化(增加防抖,避免频繁请求)
|
||||
const fetchDataDebounced = () => {
|
||||
let timer: any = null
|
||||
return () => {
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
if (map && dataLoaded.value) fetchData()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
const debouncedFetch = fetchDataDebounced()
|
||||
watch([operator, networkType, minUploadTraffic, maxUploadTraffic, minDownloadTraffic, maxDownloadTraffic, startDate, endDate, minTrafficThreshold, gridRows, gridCols], debouncedFetch)
|
||||
|
||||
onMounted(() => {
|
||||
console.log('组件已挂载,开始初始化地图')
|
||||
initMap()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
console.log('Req26组件卸载,清理地图资源')
|
||||
overlays.forEach(overlay => {
|
||||
try {
|
||||
map?.removeOverlay(overlay)
|
||||
} catch (error) {
|
||||
console.warn('移除覆盖物失败:', error)
|
||||
}
|
||||
})
|
||||
// 清理地图实例
|
||||
if (map) {
|
||||
try {
|
||||
map.clearOverlays()
|
||||
} catch (error) {
|
||||
console.warn('清理地图覆盖物失败:', error)
|
||||
}
|
||||
}
|
||||
map = null
|
||||
overlays = []
|
||||
// 重置状态
|
||||
loading.value = false
|
||||
dataLoaded.value = false
|
||||
dataList.value = []
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<!-- 加载遮罩 -->
|
||||
<div v-if="loading || !dataLoaded" class="loading-overlay">
|
||||
<div class="loading-spinner">
|
||||
<div class="spinner"></div>
|
||||
<p class="loading-text">加载数据中...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>业务需求26:热门手机-热门手机分布图</h2>
|
||||
<p>这里展示热门手机分布图的相关内容。</p>
|
||||
|
||||
<div class="filter-bar">
|
||||
<div class="filter-item">
|
||||
<label for="operator">运营商:</label>
|
||||
<select id="operator" v-model="operator" class="select-input" :disabled="loading">
|
||||
<option value="ALL">全部</option>
|
||||
<option value="CMCC">CMCC(中国移动)</option>
|
||||
<option value="CUCC">CUCC(中国联通)</option>
|
||||
<option value="CTCC">CTCC(中国电信)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label for="networkType">网络制式:</label>
|
||||
<select id="networkType" v-model="networkType" class="select-input" :disabled="loading">
|
||||
<option value="">全部</option>
|
||||
<option value="Android">Android</option>
|
||||
<option value="iOS">iOS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label for="startDate">起始日期:</label>
|
||||
<input
|
||||
id="startDate"
|
||||
type="date"
|
||||
v-model="startDate"
|
||||
class="date-input"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label for="endDate">结束日期:</label>
|
||||
<input
|
||||
id="endDate"
|
||||
type="date"
|
||||
v-model="endDate"
|
||||
class="date-input"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label for="minTrafficThreshold">流量阈值(KB):</label>
|
||||
<input
|
||||
id="minTrafficThreshold"
|
||||
type="number"
|
||||
v-model.number="minTrafficThreshold"
|
||||
class="number-input"
|
||||
min="0"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
<button @click="fetchData" class="refresh-btn" :disabled="loading">
|
||||
{{ loading ? '加载中...' : '查询' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar-secondary">
|
||||
<div class="filter-item">
|
||||
<label for="minUploadTraffic">最小上传流量(KB):</label>
|
||||
<input
|
||||
id="minUploadTraffic"
|
||||
type="number"
|
||||
v-model.number="minUploadTraffic"
|
||||
class="number-input"
|
||||
min="0"
|
||||
step="0.1"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label for="maxUploadTraffic">最大上传流量(KB):</label>
|
||||
<input
|
||||
id="maxUploadTraffic"
|
||||
type="number"
|
||||
v-model.number="maxUploadTraffic"
|
||||
class="number-input"
|
||||
min="0"
|
||||
step="0.1"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label for="minDownloadTraffic">最小下载流量(KB):</label>
|
||||
<input
|
||||
id="minDownloadTraffic"
|
||||
type="number"
|
||||
v-model.number="minDownloadTraffic"
|
||||
class="number-input"
|
||||
min="0"
|
||||
step="0.1"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label for="maxDownloadTraffic">最大下载流量(KB):</label>
|
||||
<input
|
||||
id="maxDownloadTraffic"
|
||||
type="number"
|
||||
v-model.number="maxDownloadTraffic"
|
||||
class="number-input"
|
||||
min="0"
|
||||
step="0.1"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label for="gridRows">网格行数:</label>
|
||||
<input
|
||||
id="gridRows"
|
||||
type="number"
|
||||
v-model.number="gridRows"
|
||||
class="number-input"
|
||||
min="1"
|
||||
max="50"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label for="gridCols">网格列数:</label>
|
||||
<input
|
||||
id="gridCols"
|
||||
type="number"
|
||||
v-model.number="gridCols"
|
||||
class="number-input"
|
||||
min="1"
|
||||
max="50"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-bar">
|
||||
<p>地图将可视区域分为{{ gridRows }}×{{ gridCols }}={{ gridRows * gridCols }}个矩形区域,显示每个区域流量Top1的热门手机(流量需超过阈值{{ minTrafficThreshold }}KB)</p>
|
||||
</div>
|
||||
|
||||
<div class="map-container" ref="mapContainer"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filter-bar,
|
||||
.filter-bar-secondary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
background: #f9fafb;
|
||||
border-radius: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.select-input,
|
||||
.text-input,
|
||||
.date-input,
|
||||
.number-input {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.text-input {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 6px 16px;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.refresh-btn:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
}
|
||||
.refresh-btn:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.info-bar {
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 12px;
|
||||
background: #e0f2fe;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 350px);
|
||||
min-height: 500px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #e5e7eb;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.loading-text {
|
||||
font-size: 16px;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user