| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121 |
- <template>
- <div class="jsqc-screen">
- <!-- ==================== 顶部标题栏 ==================== -->
- <div class="screen-header">
- <div class="header-left">
- <span class="header-time">{{ currentDateTime }}</span>
- </div>
- <div class="header-center">
- <h1 class="header-title">排水管网安全运行监测系统</h1>
- </div>
- <div class="header-right">
- </div>
- </div>
- <!-- ==================== 主体三栏布局 ==================== -->
- <div class="screen-body">
- <!-- ========== 左侧面板 ========== -->
- <div class="panel-left">
- <!-- 天气预报 -->
- <div class="panel-box">
- <div class="panel-title">
- <span class="title-icon">▶</span> 天气预报
- </div>
- <div class="weather-cards">
- <div class="weather-card" v-for="(w, i) in weatherList" :key="i">
- <div class="weather-date">{{ w.date }}</div>
- <div class="weather-icon">{{ getWeatherIcon(w.text) }}</div>
- <div class="weather-desc">{{ w.desc }}</div>
- <div class="weather-temp">{{ w.tempRange }}</div>
- <div class="weather-wind">{{ w.wind }}</div>
- </div>
- </div>
- </div>
- <!-- 监测点概况 -->
- <div class="panel-box">
- <div class="panel-title">
- <span class="title-icon">▶</span> 监测点概况
- </div>
- <div class="pie-chart-wrapper">
- <div id="jsqcPieChart" class="chart-area"></div>
- <div class="pie-legend">
- <div class="legend-item" v-for="(item, i) in pieData" :key="i">
- <span class="legend-dot" :style="{ background: item.color }"></span>
- <span class="legend-label">{{ item.name }}</span>
- <span class="legend-value">{{ item.value }}</span>
- </div>
- </div>
- </div>
- </div>
- <!-- 实时数据 -->
- <div class="panel-box">
- <div class="panel-title">
- <span class="title-icon">▶</span> 实时数据
- <span class="refresh-time">刷新: {{ refreshTime }}</span>
- </div>
- <div class="realtime-table-wrap">
- <table class="dark-table">
- <thead>
- <tr>
- <th>序号</th>
- <th>监测点</th>
- <th>时间</th>
- <th>积水量(cm)</th>
- <th>级别</th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="(row, i) in realtimeData" :key="i">
- <td>{{ i + 1 }}</td>
- <td :title="row.name">{{ row.name }}</td>
- <td>{{ row.time }}</td>
- <td>{{ row.depth }}</td>
- <td>
- <span :class="'level-tag level-' + row.levelClass">{{ row.level }}</span>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
- </div>
- <!-- ========== 中间地图区域 ========== -->
- <div class="panel-center">
- <div id="jsqcMap" class="map-container"></div>
- </div>
- <!-- ========== 右侧面板 ========== -->
- <div class="panel-right">
- <!-- 积水统计 -->
- <div class="panel-box">
- <div class="panel-title">
- <span class="title-icon">▶</span> 积水统计
- </div>
- <div id="jsqcLineChart" class="chart-area-sm"></div>
- </div>
- </div>
- </div>
- <!-- 积水深度曲线弹窗 -->
- <div v-if="depthModalVisible" class="depth-modal" @click.self="closeDepthModal">
- <div class="depth-modal-content">
- <div class="depth-modal-header">
- <span class="depth-modal-title">{{ selectedPointName }} - 积水深度曲线</span>
- <span class="depth-modal-close" @click="closeDepthModal">×</span>
- </div>
- <div class="depth-modal-tabs">
- <div
- v-for="tab in depthTabs"
- :key="tab.value"
- class="depth-modal-tab"
- :class="{ active: currentDepthTab === tab.value }"
- @click="switchDepthTab(tab.value)"
- >{{ tab.label }}</div>
- </div>
- <div id="jsqcDepthChart" class="depth-chart-area"></div>
- </div>
- </div>
- </div>
- </template>
- <script setup name="Jsqc">
- import { ref, onMounted, onUnmounted, nextTick } from 'vue'
- import * as echarts from 'echarts'
- import { useRouter } from 'vue-router'
- import { ElMessage } from 'element-plus'
- import locationIcon from '@/assets/images/location.png'
- import { getWaterloggingPage } from '@/api/drainage'
- const router = useRouter()
- // ==================== 实时时间 ====================
- const currentDateTime = ref('')
- let timer = null
- function updateTime() {
- const now = new Date()
- const pad = n => String(n).padStart(2, '0')
- const weekDays = ['日', '一', '二', '三', '四', '五', '六']
- const dateStr = `${now.getFullYear()}/${pad(now.getMonth() + 1)}/${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())} 星期${weekDays[now.getDay()]}`
- currentDateTime.value = dateStr
- }
- // ==================== 刷新时间 ====================
- const refreshTime = ref('')
- function updateRefreshTime() {
- const now = new Date()
- const pad = n => String(n).padStart(2, '0')
- refreshTime.value = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
- }
- // ==================== 天气数据 ====================
- const weatherList = ref([])
- // 百度地图 AK(与 index.html 中加载百度地图 SDK 使用同一个)
- const BAIDU_MAP_AK = 'mvSNSN4CPN6WJs2Xi6sQFNmWwCLRgU6T'
- // 天气文字 -> emoji 图标映射
- function getWeatherIcon(text) {
- if (!text) return '☀️'
- const t = String(text)
- if (t.includes('晴')) return '☀️'
- if (t.includes('多云')) return '⛅'
- if (t.includes('阴')) return '☁️'
- if (t.includes('雨') && t.includes('雪')) return '🌨️'
- if (t.includes('雪')) return '❄️'
- if (t.includes('雷')) return '⛈️'
- if (t.includes('雨')) return '🌧️'
- if (t.includes('雾') || t.includes('霾')) return '🌫️'
- return '☁️'
- }
- // 简易 JSONP 调用百度天气服务(服务端未开放 CORS,需用 JSONP)
- function jsonpRequest(url) {
- return new Promise((resolve, reject) => {
- const callbackName = 'jsqcWeatherCallback_' + Date.now()
- const script = document.createElement('script')
- script.src = url + (url.includes('?') ? '&' : '?') + 'callback=' + callbackName
- script.onerror = () => {
- document.head.removeChild(script)
- delete window[callbackName]
- reject(new Error('天气接口加载失败'))
- }
- window[callbackName] = (data) => {
- document.head.removeChild(script)
- delete window[callbackName]
- resolve(data)
- }
- document.head.appendChild(script)
- })
- }
- async function loadWeatherData() {
- try {
- const url = `/baidu-weather/weather/v1/?district_id=431222&data_type=all&ak=${BAIDU_MAP_AK}&output=json`
- const response = await fetch(url)
- const res = await response.json()
- if (res && (res.status === 0 || res.status === '0') && res.result && Array.isArray(res.result.forecasts)) {
- weatherList.value = res.result.forecasts.slice(0, 3).map(day => {
- const dateStr = day.date || ''
- const monthDay = dateStr.length >= 10 ? `${dateStr.slice(5, 7)}/${dateStr.slice(8, 10)}` : dateStr
- return {
- date: monthDay,
- text: day.text_day || day.text_night || '多云',
- desc: day.text_day || '多云',
- tempRange: `${day.low || '--'}°C ~ ${day.high || '--'}°C`,
- wind: `${day.wd_day || ''} ${day.wc_day || ''}`
- }
- })
- } else {
- console.warn('百度天气返回异常:', res)
- useMockWeather()
- }
- } catch (e) {
- console.error('加载天气数据失败:', e)
- useMockWeather()
- }
- }
- // 天气接口失败时使用模拟数据兜底
- function useMockWeather() {
- const now = new Date()
- const pad = n => String(n).padStart(2, '0')
- const formatDate = (offset) => {
- const d = new Date(now.getTime() + offset * 24 * 60 * 60 * 1000)
- return `${pad(d.getMonth() + 1)}/${pad(d.getDate())}`
- }
- weatherList.value = [
- { date: formatDate(0), text: '多云', desc: '多云', tempRange: '18°C ~ 26°C', wind: '东南风 2级' },
- { date: formatDate(1), text: '小雨', desc: '小雨', tempRange: '16°C ~ 23°C', wind: '东风 3级' },
- { date: formatDate(2), text: '阴', desc: '阴', tempRange: '15°C ~ 22°C', wind: '北风 2级' }
- ]
- }
- // ==================== 监测点概况 ====================
- const pieData = ref([
- { name: '未积水', value: 0, color: '#67c23a' },
- { name: '轻度积水', value: 0, color: '#409eff' },
- { name: '中度积水', value: 0, color: '#e6a23c' },
- { name: '严重积水', value: 0, color: '#f56c6c' }
- ])
- // ==================== 实时数据 ====================
- const realtimeData = ref([])
- // ==================== 地图监测点数据 ====================
- const mapPoints = ref([])
- // ==================== 积水等级映射 ====================
- // 后端 waterloggingLevel 约定:0=未积水,1=轻度,2=中度,3=严重
- function getWaterloggingLevelInfo(level) {
- const map = {
- 0: { label: '未积水', levelClass: 'none' },
- 1: { label: '轻度积水', levelClass: 'light' },
- 2: { label: '中度积水', levelClass: 'moderate' },
- 3: { label: '严重积水', levelClass: 'severe' }
- }
- return map[level] || map[Number(level)] || { label: '未积水', levelClass: 'none' }
- }
- // 安全解析 waterloggingPoints(后端返回 JSON 字符串,需转为数组使用)
- function parseWaterloggingPoints(raw) {
- if (!raw) return []
- if (Array.isArray(raw)) return raw
- if (typeof raw === 'string') {
- try {
- const parsed = JSON.parse(raw)
- return Array.isArray(parsed) ? parsed : []
- } catch (e) {
- console.warn('waterloggingPoints JSON 解析失败:', raw, e)
- return []
- }
- }
- return []
- }
- // ==================== API数据加载 ====================
- async function loadWaterloggingData() {
- try {
- const res = await getWaterloggingPage(1, 999)
- // 兼容直接返回 Page 对象(无 code 包装)和 AjaxResult 包装两种格式
- const pageData = res && res.code !== undefined ? res.data : res
- const list = Array.isArray(pageData.records) ? pageData.records : (Array.isArray(pageData) ? pageData : [])
- // 构建地图点位:展示每个易积水点的所有子点位
- const allMapPoints = []
- list.forEach(p => {
- const levelInfo = getWaterloggingLevelInfo(p.waterloggingLevel)
- const subPoints = parseWaterloggingPoints(p.waterloggingPoints)
- if (subPoints.length > 0) {
- subPoints.forEach(sp => {
- if (sp.lng && sp.lat) {
- allMapPoints.push({
- name: sp.name || p.pointName || '未命名',
- lng: Number(sp.lng),
- lat: Number(sp.lat),
- depth: sp.depth ?? p.waterVolume ?? 0,
- level: levelInfo.label,
- levelClass: levelInfo.levelClass,
- parentId: p.id,
- parentName: p.pointName
- })
- }
- })
- } else if (p.longitude && p.latitude) {
- // 兼容无子点位的旧数据:使用主点坐标
- allMapPoints.push({
- name: p.pointName || '未命名',
- lng: Number(p.longitude),
- lat: Number(p.latitude),
- depth: p.waterVolume ?? 0,
- level: levelInfo.label,
- levelClass: levelInfo.levelClass,
- parentId: p.id,
- parentName: p.pointName
- })
- }
- })
- mapPoints.value = allMapPoints
- // 构建实时数据表(按子点位展开)
- const pad = n => String(n).padStart(2, '0')
- const now = new Date()
- const timeStr = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
- realtimeData.value = allMapPoints.map((p, idx) => ({
- name: p.name,
- time: timeStr,
- depth: p.depth,
- level: p.level,
- levelClass: p.levelClass
- })).slice(0, 50)
- // 更新 pieData 统计(按 waterloggingLevel 统计各等级数量)
- const levelCount = { 0: 0, 1: 0, 2: 0, 3: 0 }
- list.forEach(p => {
- const key = Number(p.waterloggingLevel ?? 0)
- if (levelCount[key] !== undefined) levelCount[key]++
- else levelCount[0]++
- })
- pieData.value = [
- { name: '未积水', value: levelCount[0], color: '#67c23a' },
- { name: '轻度积水', value: levelCount[1], color: '#409eff' },
- { name: '中度积水', value: levelCount[2], color: '#e6a23c' },
- { name: '严重积水', value: levelCount[3], color: '#f56c6c' }
- ]
- } catch (e) {
- console.error('加载易积水点数据失败:', e)
- ElMessage.error('加载易积水点数据失败')
- }
- }
- // ==================== 导航方法 ====================
- function goBack() {
- router.push('/')
- }
- function goAdmin() {
- router.push('/')
- }
- // ==================== 地图初始化 ====================
- let mapInstance = null
- function initMap() {
- const container = document.getElementById('jsqcMap')
- if (!container || typeof BMapGL === 'undefined') {
- console.warn('BMapGL 未加载或容器不存在')
- return
- }
- try {
- mapInstance = new BMapGL.Map('jsqcMap')
- mapInstance.enableScrollWheelZoom(true)
- // 添加标记点
- mapPoints.value.forEach(point => {
- addMapMarker(point)
- })
- // 根据所有点位自适应视野
- if (mapPoints.value.length > 0) {
- const viewPoints = mapPoints.value.map(p => new BMapGL.Point(p.lng, p.lat))
- mapInstance.setViewport(viewPoints)
- // 确保缩放级别不低于14,避免地图显示太小
- if (mapInstance.getZoom() < 14) {
- mapInstance.setZoom(14)
- }
- } else {
- mapInstance.centerAndZoom(new BMapGL.Point(110.393, 28.452), 14)
- }
- } catch (e) {
- console.error('百度地图初始化失败:', e)
- }
- }
- function addMapMarker(point) {
- const levelColor = {
- '未积水': '#67c23a',
- '轻度积水': '#409eff',
- '中度积水': '#e6a23c',
- '严重积水': '#f56c6c'
- }
- const color = levelColor[point.level] || '#409eff'
- const bPoint = new BMapGL.Point(point.lng, point.lat)
- const html = `<div style="text-align:center;cursor:pointer;">
- <div style="color:#fff;background:${color};border-radius:4px;padding:2px 6px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;">${point.name}</div>
- <div style="color:#fff;font-size:10px;background:rgba(0,0,0,0.6);border-radius:3px;padding:1px 4px;margin-bottom:2px;">水位:${point.depth}cm ${point.level}</div>
- <div><img src="${locationIcon}" style="width:24px;height:24px;display:block;margin:0 auto;"/></div>
- </div>`
- const label = new BMapGL.Label(html, {
- position: bPoint,
- offset: new BMapGL.Size(-30, -55)
- })
- label.setStyle({
- border: 'none',
- background: 'transparent',
- padding: '0'
- })
- mapInstance.addOverlay(label)
- // 点击标记打开积水深度曲线弹窗
- label.addEventListener('click', function () {
- openDepthModal(point)
- })
- }
- // ==================== ECharts 图表 ====================
- let pieChart = null
- let lineChart = null
- function initPieChart() {
- const dom = document.getElementById('jsqcPieChart')
- if (!dom) return
- pieChart = echarts.init(dom)
- const total = pieData.value.reduce((s, d) => s + d.value, 0)
- pieChart.setOption({
- tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
- legend: { show: false },
- graphic: [{
- type: 'text',
- left: 'center',
- top: '38%',
- style: { text: '积水点个数', fill: '#a0cfff', fontSize: 12, textAlign: 'center' }
- }, {
- type: 'text',
- left: 'center',
- top: '52%',
- style: { text: String(total), fill: '#fff', fontSize: 22, fontWeight: 'bold', textAlign: 'center' }
- }],
- series: [{
- type: 'pie',
- radius: ['45%', '70%'],
- center: ['50%', '50%'],
- label: { show: false },
- labelLine: { show: false },
- data: pieData.value.map(d => ({ name: d.name, value: d.value, itemStyle: { color: d.color } })),
- emphasis: { scale: true, scaleSize: 5 }
- }]
- })
- }
- async function initLineChart() {
- // 统计最近6个月积水事件数
- const dom = document.getElementById('jsqcLineChart')
- if (!dom) return
- lineChart = echarts.init(dom)
- try {
- const res = await getWaterloggingPage(1, 9999)
- const pageData = res && res.code !== undefined ? res.data : res
- const list = Array.isArray(pageData.records) ? pageData.records : (Array.isArray(pageData) ? pageData : [])
- const now = new Date()
- const xData = []
- const yData = []
- for (let i = 5; i >= 0; i--) {
- const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
- const month = d.getMonth() + 1
- const year = d.getFullYear()
- xData.push(`${month}月`)
- const count = list.filter(item => {
- const t = item.time || item.dataTime || item.updateTime || item.createTime || ''
- if (!t) return false
- const itemDate = new Date(t)
- return itemDate.getFullYear() === year && itemDate.getMonth() + 1 === month
- }).length
- yData.push(count)
- }
- lineChart.setOption({
- tooltip: { trigger: 'axis' },
- grid: { left: 40, right: 10, top: 10, bottom: 25 },
- xAxis: {
- type: 'category',
- data: xData,
- axisLabel: { color: '#7ec8e3', fontSize: 10 },
- axisLine: { lineStyle: { color: '#1a3a5c' } }
- },
- yAxis: {
- type: 'value',
- name: '次',
- nameTextStyle: { color: '#7ec8e3', fontSize: 10 },
- axisLabel: { color: '#7ec8e3', fontSize: 10 },
- splitLine: { lineStyle: { color: '#1a3a5c' } },
- axisLine: { show: false }
- },
- series: [{
- type: 'bar',
- data: yData,
- barWidth: 12,
- itemStyle: {
- color: '#00e5ff',
- borderRadius: [2, 2, 0, 0]
- }
- }]
- })
- } catch (e) {
- console.error('积水统计加载失败:', e)
- }
- }
- // ==================== 积水深度曲线弹窗 ====================
- const depthModalVisible = ref(false)
- const selectedPointName = ref('')
- const currentDepthTab = ref('24h')
- const depthTabs = [
- { label: '24小时', value: '24h' },
- { label: '7天', value: '7d' },
- { label: '15天', value: '15d' },
- { label: '30天', value: '30d' }
- ]
- let depthChart = null
- function openDepthModal(point) {
- selectedPointName.value = point.name
- currentDepthTab.value = '24h'
- depthModalVisible.value = true
- nextTick(() => {
- initDepthChart()
- })
- }
- function closeDepthModal() {
- depthModalVisible.value = false
- if (depthChart) {
- depthChart.dispose()
- depthChart = null
- }
- }
- function switchDepthTab(tab) {
- currentDepthTab.value = tab
- initDepthChart()
- }
- function generateDepthData(tab) {
- let labels = []
- let values = []
- const now = new Date()
- const pad = n => String(n).padStart(2, '0')
- if (tab === '24h') {
- for (let i = 0; i < 24; i++) {
- labels.push(`${pad(i)}:00`)
- const base = i < 8 || i > 20 ? Math.random() * 3 : Math.random() * 15 + 5
- values.push(Math.round(base * 10) / 10)
- }
- } else {
- const count = tab === '7d' ? 7 : tab === '15d' ? 15 : 30
- for (let i = count - 1; i >= 0; i--) {
- const d = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
- labels.push(`${pad(d.getMonth() + 1)}-${pad(d.getDate())}`)
- values.push(Math.round((Math.random() * 20 + Math.random() * 5) * 10) / 10)
- }
- }
- return { labels, values }
- }
- function initDepthChart() {
- const dom = document.getElementById('jsqcDepthChart')
- if (!dom) return
- if (depthChart) depthChart.dispose()
- depthChart = echarts.init(dom)
- const { labels, values } = generateDepthData(currentDepthTab.value)
- depthChart.setOption({
- tooltip: { trigger: 'axis' },
- grid: { left: 50, right: 20, top: 20, bottom: 30 },
- xAxis: {
- type: 'category',
- data: labels,
- axisLabel: { color: '#7ec8e3', fontSize: 10 },
- axisLine: { lineStyle: { color: '#1a3a5c' } }
- },
- yAxis: {
- type: 'value',
- name: 'cm',
- nameTextStyle: { color: '#7ec8e3', fontSize: 11 },
- axisLabel: { color: '#7ec8e3', fontSize: 10 },
- splitLine: { lineStyle: { color: '#1a3a5c' } },
- axisLine: { show: false }
- },
- series: [{
- type: 'line',
- data: values,
- smooth: true,
- symbol: 'circle',
- symbolSize: 6,
- lineStyle: { color: '#409eff', width: 2 },
- itemStyle: { color: '#409eff' },
- areaStyle: {
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: 'rgba(64, 158, 255, 0.4)' },
- { offset: 1, color: 'rgba(64, 158, 255, 0.05)' }
- ])
- }
- }]
- })
- }
- // ==================== 生命周期 ====================
- onMounted(async () => {
- updateTime()
- timer = setInterval(() => {
- updateTime()
- updateRefreshTime()
- }, 1000)
- updateRefreshTime()
- // 先加载后端数据,再初始化地图和图表
- await Promise.all([
- loadWaterloggingData(),
- loadWeatherData()
- ])
- // 延迟初始化确保 DOM 渲染完成
- setTimeout(() => {
- initMap()
- initPieChart()
- initLineChart()
- }, 300)
- })
- onUnmounted(() => {
- if (timer) clearInterval(timer)
- if (pieChart) pieChart.dispose()
- if (lineChart) lineChart.dispose()
- if (depthChart) depthChart.dispose()
- if (mapInstance) mapInstance = null
- })
- // 监听窗口大小变化,重绘图表
- window.addEventListener('resize', () => {
- if (pieChart) pieChart.resize()
- if (lineChart) lineChart.resize()
- if (depthChart) depthChart.resize()
- })
- </script>
- <style scoped>
- /* ==================== 全局暗色大屏 ==================== */
- .jsqc-screen {
- width: 100%;
- height: 100vh;
- background: #0a1929;
- color: #fff;
- font-family: 'Microsoft YaHei', sans-serif;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- }
- /* ==================== 顶部标题栏 ==================== */
- .screen-header {
- height: 64px;
- background: linear-gradient(180deg, #0d2137 0%, #0a1929 100%);
- border-bottom: 1px solid rgba(64, 158, 255, 0.3);
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0 24px;
- flex-shrink: 0;
- position: relative;
- }
- .screen-header::after {
- content: '';
- position: absolute;
- bottom: 0;
- left: 10%;
- right: 10%;
- height: 1px;
- background: linear-gradient(90deg, transparent, #409eff, transparent);
- }
- .header-left {
- flex: 1;
- text-align: left;
- }
- .header-time {
- font-size: 15px;
- color: #a0cfff;
- letter-spacing: 1px;
- }
- .header-center {
- flex: 2;
- text-align: center;
- }
- .header-title {
- font-size: 26px;
- font-weight: 700;
- margin: 0;
- letter-spacing: 4px;
- background: linear-gradient(180deg, #fff 0%, #7ec8e3 100%);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- text-shadow: none;
- }
- .header-right {
- flex: 1;
- text-align: right;
- display: flex;
- justify-content: flex-end;
- gap: 10px;
- }
- .header-btn {
- background: rgba(64, 158, 255, 0.15);
- border: 1px solid rgba(64, 158, 255, 0.4);
- color: #a0cfff;
- padding: 6px 16px;
- border-radius: 4px;
- font-size: 13px;
- cursor: pointer;
- transition: all 0.3s;
- }
- .header-btn:hover {
- background: rgba(64, 158, 255, 0.3);
- border-color: #409eff;
- color: #fff;
- }
- /* ==================== 主体三栏 ==================== */
- .screen-body {
- flex: 1;
- display: flex;
- padding: 12px;
- gap: 12px;
- min-height: 0;
- }
- .panel-left,
- .panel-right {
- width: 22%;
- min-width: 260px;
- display: flex;
- flex-direction: column;
- gap: 12px;
- min-height: 0;
- }
- .panel-center {
- flex: 1;
- display: flex;
- flex-direction: column;
- min-height: 0;
- position: relative;
- }
- /* ==================== 面板盒子 ==================== */
- .panel-box {
- background: rgba(6, 30, 65, 0.8);
- border: 1px solid rgba(64, 158, 255, 0.2);
- border-radius: 6px;
- padding: 12px;
- display: flex;
- flex-direction: column;
- flex: 1;
- min-height: 0;
- overflow: hidden;
- }
- .panel-title {
- font-size: 14px;
- font-weight: 600;
- color: #7ec8e3;
- margin-bottom: 10px;
- display: flex;
- align-items: center;
- gap: 4px;
- flex-shrink: 0;
- }
- .title-icon {
- color: #409eff;
- font-size: 10px;
- }
- .refresh-time {
- margin-left: auto;
- font-size: 11px;
- color: #607080;
- font-weight: 400;
- }
- /* ==================== 天气预报 ==================== */
- .weather-cards {
- display: flex;
- gap: 8px;
- flex-shrink: 0;
- }
- .weather-card {
- flex: 1;
- background: rgba(20, 60, 110, 0.5);
- border: 1px solid rgba(64, 158, 255, 0.15);
- border-radius: 6px;
- padding: 10px 8px;
- text-align: center;
- }
- .weather-date {
- font-size: 12px;
- color: #a0cfff;
- margin-bottom: 4px;
- }
- .weather-icon {
- font-size: 24px;
- margin: 4px 0;
- }
- .weather-desc {
- font-size: 12px;
- color: #d0e8ff;
- margin-bottom: 2px;
- }
- .weather-temp {
- font-size: 11px;
- color: #7ec8e3;
- }
- .weather-wind {
- font-size: 10px;
- color: #607080;
- margin-top: 2px;
- }
- /* ==================== 监测点概况 ==================== */
- .pie-chart-wrapper {
- flex: 1;
- display: flex;
- align-items: center;
- min-height: 0;
- }
- .chart-area {
- width: 55%;
- height: 100%;
- min-height: 120px;
- }
- .pie-legend {
- width: 45%;
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .legend-item {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 12px;
- }
- .legend-dot {
- width: 10px;
- height: 10px;
- border-radius: 2px;
- flex-shrink: 0;
- }
- .legend-label {
- color: #a0cfff;
- flex: 1;
- }
- .legend-value {
- color: #fff;
- font-weight: 600;
- }
- /* ==================== 实时数据表格 ==================== */
- .realtime-table-wrap {
- flex: 1;
- overflow-y: auto;
- min-height: 0;
- }
- .dark-table {
- width: 100%;
- border-collapse: collapse;
- font-size: 12px;
- }
- .dark-table th {
- background: rgba(20, 60, 110, 0.6);
- color: #7ec8e3;
- padding: 7px 6px;
- text-align: center;
- border-bottom: 1px solid rgba(64, 158, 255, 0.2);
- font-weight: 500;
- white-space: nowrap;
- }
- .dark-table td {
- padding: 6px;
- text-align: center;
- color: #d0e8ff;
- border-bottom: 1px solid rgba(64, 158, 255, 0.08);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 80px;
- }
- .dark-table tbody tr:nth-child(odd) {
- background: rgba(10, 25, 50, 0.4);
- }
- .dark-table tbody tr:nth-child(even) {
- background: rgba(20, 40, 70, 0.3);
- }
- .dark-table tbody tr:hover {
- background: rgba(64, 158, 255, 0.1);
- }
- .level-tag {
- padding: 1px 6px;
- border-radius: 3px;
- font-size: 11px;
- }
- .level-none {
- color: #67c23a;
- background: rgba(103, 194, 58, 0.15);
- }
- .level-light {
- color: #409eff;
- background: rgba(64, 158, 255, 0.15);
- }
- .level-moderate {
- color: #e6a23c;
- background: rgba(230, 162, 60, 0.15);
- }
- .level-severe {
- color: #f56c6c;
- background: rgba(245, 108, 108, 0.15);
- }
- /* ==================== 中间地图 ==================== */
- .map-container {
- flex: 1;
- border-radius: 6px;
- overflow: hidden;
- border: 1px solid rgba(64, 158, 255, 0.2);
- min-height: 0;
- }
- .map-tabs {
- display: flex;
- gap: 0;
- background: rgba(6, 30, 65, 0.9);
- border: 1px solid rgba(64, 158, 255, 0.2);
- border-radius: 6px;
- margin-top: 8px;
- flex-shrink: 0;
- overflow: hidden;
- }
- .map-tab {
- flex: 1;
- text-align: center;
- padding: 8px 4px;
- font-size: 12px;
- color: #a0cfff;
- cursor: pointer;
- transition: all 0.3s;
- border-right: 1px solid rgba(64, 158, 255, 0.1);
- }
- .map-tab:last-child {
- border-right: none;
- }
- .map-tab.active {
- background: rgba(64, 158, 255, 0.25);
- color: #fff;
- font-weight: 600;
- }
- .map-tab:hover {
- background: rgba(64, 158, 255, 0.15);
- }
- .chart-area-sm {
- width: 100%;
- flex: 1;
- min-height: 130px;
- }
- /* ==================== 滚动条样式 ==================== */
- ::-webkit-scrollbar {
- width: 4px;
- }
- ::-webkit-scrollbar-track {
- background: rgba(10, 25, 50, 0.3);
- }
- ::-webkit-scrollbar-thumb {
- background: rgba(64, 158, 255, 0.3);
- border-radius: 2px;
- }
- ::-webkit-scrollbar-thumb:hover {
- background: rgba(64, 158, 255, 0.5);
- }
- /* ==================== 积水深度曲线弹窗 ==================== */
- .depth-modal {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.7);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
- }
- .depth-modal-content {
- width: 720px;
- max-width: 90vw;
- background: rgba(6, 30, 65, 0.95);
- border: 1px solid rgba(64, 158, 255, 0.4);
- border-radius: 8px;
- padding: 16px;
- box-shadow: 0 0 20px rgba(64, 158, 255, 0.2);
- }
- .depth-modal-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 12px;
- }
- .depth-modal-title {
- font-size: 15px;
- font-weight: 600;
- color: #7ec8e3;
- }
- .depth-modal-close {
- font-size: 22px;
- color: #a0cfff;
- cursor: pointer;
- line-height: 1;
- }
- .depth-modal-close:hover {
- color: #fff;
- }
- .depth-modal-tabs {
- display: flex;
- gap: 8px;
- margin-bottom: 12px;
- }
- .depth-modal-tab {
- flex: 1;
- text-align: center;
- padding: 6px 4px;
- font-size: 12px;
- color: #a0cfff;
- background: rgba(10, 25, 50, 0.6);
- border: 1px solid rgba(64, 158, 255, 0.2);
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.3s;
- }
- .depth-modal-tab.active {
- background: rgba(64, 158, 255, 0.3);
- color: #fff;
- border-color: rgba(64, 158, 255, 0.6);
- }
- .depth-modal-tab:hover {
- background: rgba(64, 158, 255, 0.2);
- }
- .depth-chart-area {
- width: 100%;
- height: 320px;
- }
- </style>
|