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.
 
 
livestock-trading/src/components/MarketEnvironmentMonitor.vue

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>