You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
478 lines
14 KiB
478 lines
14 KiB
<template>
|
|
<BaseCard title="市场环境监控">
|
|
<div class="monitor-content">
|
|
<div v-if="loading" class="state-box">加载中...</div>
|
|
<div v-else class="environment-grid">
|
|
<div
|
|
v-for="item in environmentData"
|
|
:key="item.key"
|
|
class="env-card"
|
|
>
|
|
<div class="env-head">
|
|
<img class="env-icon" :src="item.icon" :alt="item.name" />
|
|
<span class="env-name">{{ item.name }}</span>
|
|
</div>
|
|
<div class="env-body">
|
|
<div class="env-value-row">
|
|
<span class="env-value">{{ item.displayValue }}</span>
|
|
<span v-if="item.unit" class="env-unit">{{ item.unit }}</span>
|
|
</div>
|
|
<span class="env-status" :class="item.statusClass">{{ item.statusText }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</BaseCard>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
import BaseCard from './BaseCard.vue'
|
|
|
|
const API_URL = '/api/dashboard/market-environment'
|
|
|
|
const ICONS = {
|
|
temperature: '/images/温度.png',
|
|
humidity: '/images/湿度.png',
|
|
airQuality: '/images/空气质量.png',
|
|
pressure: '/images/大气压强.png',
|
|
uv: '/images/紫外线.png',
|
|
rainfall: '/images/降雨量.png',
|
|
windDirection: '/images/风向.png',
|
|
windForce: '/images/风力.png'
|
|
}
|
|
|
|
const metrics = ref({
|
|
temperature: null,
|
|
humidity: null,
|
|
aqi: null,
|
|
airPressure: null,
|
|
uvIndex: null,
|
|
rainfall: null,
|
|
windDirection: null,
|
|
windPower: null
|
|
})
|
|
|
|
const metricTags = ref({})
|
|
const metricUnits = ref({})
|
|
|
|
const loading = ref(false)
|
|
|
|
const formatMetric = (value, digits = 1) => {
|
|
if (value === null || value === undefined || value === '') {
|
|
return null
|
|
}
|
|
const num = Number(value)
|
|
if (Number.isNaN(num)) {
|
|
return null
|
|
}
|
|
if (digits === 0) {
|
|
return Math.round(num)
|
|
}
|
|
return Number(num.toFixed(digits))
|
|
}
|
|
|
|
const tagToStatusClass = (tag) => {
|
|
const good = ['适宜', '舒适', '优', '良', '正常', '无雨', '微风']
|
|
const warn = ['偏热', '偏冷', '干燥', '潮湿', '轻度污染', '中度污染', '重度污染', '异常', '中雨', '大雨', '强风', '大风', '强', '很强']
|
|
if (!tag) return 'status-muted'
|
|
if (good.includes(tag)) return 'status-good'
|
|
if (warn.includes(tag)) return 'status-warn'
|
|
return 'status-info'
|
|
}
|
|
|
|
const resolveStatus = (metricKey, fallback) => {
|
|
const tag = metricTags.value[metricKey]
|
|
if (tag) {
|
|
return { text: tag, statusClass: tagToStatusClass(tag) }
|
|
}
|
|
return fallback
|
|
}
|
|
const getTemperatureStatus = (value) => {
|
|
if (value === null) return { text: '--', statusClass: 'status-muted' }
|
|
if (value >= 15 && value <= 25) return { text: '适宜', statusClass: 'status-good' }
|
|
if (value > 25 && value <= 30) return { text: '偏热', statusClass: 'status-warn' }
|
|
if (value < 15 && value >= 5) return { text: '偏冷', statusClass: 'status-warn' }
|
|
return { text: '异常', statusClass: 'status-warn' }
|
|
}
|
|
|
|
const getHumidityStatus = (value) => {
|
|
if (value === null) return { text: '--', statusClass: 'status-muted' }
|
|
if (value >= 40 && value <= 70) return { text: '舒适', statusClass: 'status-good' }
|
|
if (value < 40) return { text: '干燥', statusClass: 'status-warn' }
|
|
return { text: '潮湿', statusClass: 'status-warn' }
|
|
}
|
|
|
|
const getAqiStatus = (aqi) => {
|
|
if (aqi === null) return { text: '--', statusClass: 'status-muted' }
|
|
if (aqi <= 50) return { text: '优', statusClass: 'status-good' }
|
|
if (aqi <= 100) return { text: '良', statusClass: 'status-good' }
|
|
if (aqi <= 150) return { text: '轻度污染', statusClass: 'status-warn' }
|
|
if (aqi <= 200) return { text: '中度污染', statusClass: 'status-warn' }
|
|
return { text: '重度污染', statusClass: 'status-warn' }
|
|
}
|
|
|
|
const getPressureStatus = (value) => {
|
|
if (value === null) return { text: '--', statusClass: 'status-muted' }
|
|
if (value >= 1000 && value <= 1025) return { text: '正常', statusClass: 'status-good' }
|
|
return { text: '异常', statusClass: 'status-warn' }
|
|
}
|
|
|
|
const getUvStatus = (value) => {
|
|
if (value === null) return { text: '--', statusClass: 'status-muted' }
|
|
if (value <= 2) return { text: '弱', statusClass: 'status-good' }
|
|
if (value <= 5) return { text: '中等', statusClass: 'status-info' }
|
|
if (value <= 7) return { text: '强', statusClass: 'status-warn' }
|
|
return { text: '很强', statusClass: 'status-warn' }
|
|
}
|
|
|
|
const getRainfallStatus = (value) => {
|
|
if (value === null) return { text: '--', statusClass: 'status-muted' }
|
|
if (value < 0.1) return { text: '无雨', statusClass: 'status-good' }
|
|
if (value < 5) return { text: '小雨', statusClass: 'status-info' }
|
|
if (value < 15) return { text: '中雨', statusClass: 'status-warn' }
|
|
return { text: '大雨', statusClass: 'status-warn' }
|
|
}
|
|
|
|
const getWindDirectionText = (degree) => {
|
|
if (degree === null) {
|
|
return '--'
|
|
}
|
|
const directions = [
|
|
{ min: 0, max: 22.5, name: '北风' },
|
|
{ min: 22.5, max: 67.5, name: '东北风' },
|
|
{ min: 67.5, max: 112.5, name: '东风' },
|
|
{ min: 112.5, max: 157.5, name: '东南风' },
|
|
{ min: 157.5, max: 202.5, name: '南风' },
|
|
{ min: 202.5, max: 247.5, name: '西南风' },
|
|
{ min: 247.5, max: 292.5, name: '西风' },
|
|
{ min: 292.5, max: 337.5, name: '西北风' },
|
|
{ min: 337.5, max: 360, name: '北风' }
|
|
]
|
|
const normalized = ((Number(degree) % 360) + 360) % 360
|
|
const direction = directions.find((item) => normalized >= item.min && normalized < item.max)
|
|
return direction ? direction.name : '--'
|
|
}
|
|
|
|
const getWindLevelStatus = (level) => {
|
|
if (level === null) return { text: '--', statusClass: 'status-muted' }
|
|
if (level <= 3) return { text: '微风', statusClass: 'status-good' }
|
|
if (level <= 5) return { text: '和风', statusClass: 'status-info' }
|
|
if (level <= 7) return { text: '强风', statusClass: 'status-warn' }
|
|
return { text: '大风', statusClass: 'status-warn' }
|
|
}
|
|
|
|
const displayOrDash = (value) => {
|
|
if (value === null || value === undefined || value === '') {
|
|
return '--'
|
|
}
|
|
return value
|
|
}
|
|
|
|
const environmentData = computed(() => {
|
|
const temperature = formatMetric(metrics.value.temperature, 1)
|
|
const humidity = formatMetric(metrics.value.humidity, 0)
|
|
const aqi = formatMetric(metrics.value.aqi, 0)
|
|
const airPressure = formatMetric(metrics.value.airPressure, 0)
|
|
const uvIndex = formatMetric(metrics.value.uvIndex, 0)
|
|
const rainfall = formatMetric(metrics.value.rainfall, 1)
|
|
const windDirection = formatMetric(metrics.value.windDirection, 0)
|
|
const windPower = formatMetric(metrics.value.windPower, 0)
|
|
|
|
const tempStatus = resolveStatus('temperature', getTemperatureStatus(temperature))
|
|
const humidityStatus = resolveStatus('humidity', getHumidityStatus(humidity))
|
|
const aqiStatus = resolveStatus('aqi', getAqiStatus(aqi))
|
|
const pressureStatus = resolveStatus('airPressure', getPressureStatus(airPressure))
|
|
const uvStatus = resolveStatus('uvIndex', getUvStatus(uvIndex))
|
|
const rainfallStatus = resolveStatus('rainfall', getRainfallStatus(rainfall))
|
|
const windLevelStatus = resolveStatus('windPower', getWindLevelStatus(windPower))
|
|
|
|
const windDirectionTag = metricTags.value.windDirection
|
|
const windDirectionDisplay = windDirectionTag || getWindDirectionText(windDirection)
|
|
const windDirectionSub = windDirection === null
|
|
? '--'
|
|
: `${windDirection}${metricUnits.value.windDirection || '°'}`
|
|
|
|
return [
|
|
{
|
|
key: 'temperature',
|
|
name: '温度',
|
|
icon: ICONS.temperature,
|
|
displayValue: displayOrDash(temperature),
|
|
unit: metricUnits.value.temperature || '℃',
|
|
statusText: tempStatus.text,
|
|
statusClass: tempStatus.statusClass
|
|
},
|
|
{
|
|
key: 'humidity',
|
|
name: '湿度',
|
|
icon: ICONS.humidity,
|
|
displayValue: displayOrDash(humidity),
|
|
unit: metricUnits.value.humidity || '%RH',
|
|
statusText: humidityStatus.text,
|
|
statusClass: humidityStatus.statusClass
|
|
},
|
|
{
|
|
key: 'airQuality',
|
|
name: '空气质量',
|
|
icon: ICONS.airQuality,
|
|
displayValue: displayOrDash(aqi),
|
|
unit: metricUnits.value.aqi || 'AQI',
|
|
statusText: aqiStatus.text,
|
|
statusClass: aqiStatus.statusClass
|
|
},
|
|
{
|
|
key: 'pressure',
|
|
name: '大气压强',
|
|
icon: ICONS.pressure,
|
|
displayValue: displayOrDash(airPressure),
|
|
unit: metricUnits.value.airPressure || 'hPa',
|
|
statusText: pressureStatus.text,
|
|
statusClass: pressureStatus.statusClass
|
|
},
|
|
{
|
|
key: 'uv',
|
|
name: '紫外线',
|
|
icon: ICONS.uv,
|
|
displayValue: displayOrDash(uvIndex),
|
|
unit: metricUnits.value.uvIndex || '级',
|
|
statusText: uvStatus.text,
|
|
statusClass: uvStatus.statusClass
|
|
},
|
|
{
|
|
key: 'rainfall',
|
|
name: '降雨量',
|
|
icon: ICONS.rainfall,
|
|
displayValue: displayOrDash(rainfall),
|
|
unit: metricUnits.value.rainfall || 'mm',
|
|
statusText: rainfallStatus.text,
|
|
statusClass: rainfallStatus.statusClass
|
|
},
|
|
{
|
|
key: 'windDirection',
|
|
name: '风向',
|
|
icon: ICONS.windDirection,
|
|
displayValue: windDirectionDisplay,
|
|
unit: '',
|
|
statusText: windDirectionSub,
|
|
statusClass: windDirection === null ? 'status-muted' : 'status-info'
|
|
},
|
|
{
|
|
key: 'windForce',
|
|
name: '风力',
|
|
icon: ICONS.windForce,
|
|
displayValue: displayOrDash(windPower),
|
|
unit: metricUnits.value.windPower || '级',
|
|
statusText: windLevelStatus.text,
|
|
statusClass: windLevelStatus.statusClass
|
|
}
|
|
]
|
|
})
|
|
|
|
const loadData = async () => {
|
|
loading.value = true
|
|
try {
|
|
const response = await fetch(API_URL)
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}`)
|
|
}
|
|
const result = await response.json()
|
|
if (result.code !== undefined && result.code !== 1) {
|
|
throw new Error(result.message || '接口返回异常')
|
|
}
|
|
const payload = result.data ?? result
|
|
metrics.value = {
|
|
temperature: payload.temperature ?? null,
|
|
humidity: payload.humidity ?? null,
|
|
aqi: payload.aqi ?? null,
|
|
airPressure: payload.airPressure ?? payload.air_pressure ?? null,
|
|
uvIndex: payload.uvIndex ?? payload.uv_index ?? null,
|
|
rainfall: payload.rainfall ?? null,
|
|
windDirection: payload.windDirection ?? payload.wind_direction ?? null,
|
|
windPower: payload.windPower ?? payload.wind_power ?? null
|
|
}
|
|
metricTags.value = {
|
|
temperature: payload.temperatureTag ?? null,
|
|
humidity: payload.humidityTag ?? null,
|
|
aqi: payload.aqiTag ?? null,
|
|
airPressure: payload.airPressureTag ?? null,
|
|
uvIndex: payload.uvIndexTag ?? null,
|
|
rainfall: payload.rainfallTag ?? null,
|
|
windDirection: payload.windDirectionTag ?? null,
|
|
windPower: payload.windPowerTag ?? null
|
|
}
|
|
metricUnits.value = {
|
|
temperature: payload.temperatureUnit ?? null,
|
|
humidity: payload.humidityUnit ?? null,
|
|
aqi: payload.aqiUnit ?? null,
|
|
airPressure: payload.airPressureUnit ?? null,
|
|
uvIndex: payload.uvIndexUnit ?? null,
|
|
rainfall: payload.rainfallUnit ?? null,
|
|
windDirection: payload.windDirectionUnit ?? null,
|
|
windPower: payload.windPowerUnit ?? null
|
|
}
|
|
} catch (error) {
|
|
console.error('加载市场环境监控失败:', error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await loadData()
|
|
window.addEventListener('dataReloaded', loadData)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('dataReloaded', loadData)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.monitor-content {
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.state-box {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 100%;
|
|
height: 100%;
|
|
color: rgba(255, 255, 255, 0.65);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.environment-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
grid-template-rows: auto auto;
|
|
gap: 20px;
|
|
width: 100%;
|
|
height: 100%;
|
|
align-content: center;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.env-card {
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
aspect-ratio: 148 / 104;
|
|
height: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
background: linear-gradient(180deg, rgba(8, 72, 88, 0.42) 0%, rgba(4, 38, 52, 0.58) 100%);
|
|
border: 1px solid rgba(16, 150, 161, 0.65);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.env-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
width: 100%;
|
|
height: 30px;
|
|
padding: 0 8px;
|
|
box-sizing: border-box;
|
|
background: #025f66;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.env-icon {
|
|
width: 22px;
|
|
height: 22px;
|
|
object-fit: contain;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.env-name {
|
|
font-family: 'Microsoft YaHei', sans-serif;
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
color: rgba(255, 255, 255, 0.92);
|
|
white-space: nowrap;
|
|
line-height: 22px;
|
|
}
|
|
|
|
.env-body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.env-value-row {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 4px;
|
|
width: 100%;
|
|
padding: 0 6px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.env-value {
|
|
font-family: 'Microsoft YaHei', sans-serif;
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
color: #0ce3fc;
|
|
}
|
|
|
|
.env-unit {
|
|
font-family: 'Microsoft YaHei', sans-serif;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #0ce3fc;
|
|
}
|
|
|
|
.env-status {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 120px;
|
|
height: 20px;
|
|
margin-bottom: 8px;
|
|
padding: 0 8px;
|
|
align-self: center;
|
|
flex-shrink: 0;
|
|
box-sizing: border-box;
|
|
border-radius: 999px;
|
|
border: 1px solid #06c7be;
|
|
font-family: 'Microsoft YaHei', sans-serif;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
line-height: 1;
|
|
background: transparent;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.status-good {
|
|
color: #0ca34c;
|
|
}
|
|
|
|
.status-info {
|
|
color: #106dae;
|
|
}
|
|
|
|
.status-warn {
|
|
color: #bf7412;
|
|
}
|
|
|
|
.status-muted {
|
|
color: rgba(255, 255, 255, 0.45);
|
|
}
|
|
</style>
|
|
|