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.
1159 lines
34 KiB
1159 lines
34 KiB
<template>
|
|
<div class="health-dashboard">
|
|
<!-- 主内容区域 -->
|
|
<div class="dashboard-main">
|
|
<!-- 左侧:驼圈列表 -->
|
|
<div class="col-left">
|
|
<div class="card pen-list-card">
|
|
<div class="card-header">
|
|
<span class="title">🐫 驼圈列表</span>
|
|
<span class="sub">共 {{ pens.length }} 个圈舍</span>
|
|
</div>
|
|
<div class="pen-grid">
|
|
<div
|
|
v-for="pen in pens"
|
|
:key="pen.name"
|
|
class="pen-item"
|
|
:class="{ active: selectedPen === pen.name }"
|
|
@click="selectPen(pen.name)"
|
|
>
|
|
<div class="pen-name">{{ pen.name }}</div>
|
|
<div class="pen-count">{{ pen.count }}峰</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card stats-card">
|
|
<div class="card-header">
|
|
<span class="title">📊 健康统计总览</span>
|
|
</div>
|
|
<div class="stats-grid">
|
|
<div class="stat-item">
|
|
<div class="stat-value">
|
|
<animate-number :from="0" :to="healthStats.totalCamels" duration="3000" />
|
|
<span class="unit">峰</span>
|
|
</div>
|
|
<div class="stat-label">总骆驼数</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">
|
|
<animate-number :from="0" :to="healthStats.healthyRate" duration="3000" />
|
|
<span class="unit">%</span>
|
|
</div>
|
|
<div class="stat-label">健康率</div>
|
|
<div class="stat-trend up">↑ 2.3%</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">
|
|
<animate-number :from="0" :to="healthStats.avgTemp" duration="3000" :decimals="1" />
|
|
<span class="unit">°C</span>
|
|
</div>
|
|
<div class="stat-label">平均体温</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">
|
|
<animate-number :from="0" :to="healthStats.warningCount" duration="3000" />
|
|
<span class="unit">峰</span>
|
|
</div>
|
|
<div class="stat-label">异常预警</div>
|
|
<div class="stat-trend danger">需关注</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card alert-card">
|
|
<div class="card-header">
|
|
<span class="title">⚠️ 健康预警</span>
|
|
<span class="badge">{{ healthAlerts.length }}</span>
|
|
</div>
|
|
<div class="alert-list">
|
|
<div v-for="(alert, idx) in healthAlerts" :key="idx" class="alert-item">
|
|
<span class="alert-level" :class="alert.level">{{ alert.levelText }}</span>
|
|
<div class="alert-content">
|
|
<div class="alert-title">{{ alert.title }}</div>
|
|
<div class="alert-desc">{{ alert.desc }}</div>
|
|
</div>
|
|
<span class="alert-time">{{ alert.time }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 中间:驼圈详情 + 健康指标趋势 -->
|
|
<div class="col-center">
|
|
<div class="card pen-detail-card">
|
|
<div class="card-header">
|
|
<span class="title">📍 {{ selectedPen }} · 圈舍详情</span>
|
|
<span class="sub">{{ getPenCount(selectedPen) }}峰骆驼</span>
|
|
</div>
|
|
<div class="pen-detail">
|
|
<div class="detail-item">
|
|
<div class="detail-label">平均体温</div>
|
|
<div class="detail-value">{{ penHealthData.avgTemp }}<span class="unit">°C</span></div>
|
|
<div class="detail-status" :class="getTempStatus(penHealthData.avgTemp)">{{ getTempStatusText(penHealthData.avgTemp) }}</div>
|
|
</div>
|
|
<div class="detail-item">
|
|
<div class="detail-label">空气质量</div>
|
|
<div class="detail-value">{{ penHealthData.airQuality }}<span class="unit">AQI</span></div>
|
|
<div class="detail-status" :class="getAirStatus(penHealthData.airQuality)">{{ getAirStatusText(penHealthData.airQuality) }}</div>
|
|
</div>
|
|
<div class="detail-item">
|
|
<div class="detail-label">湿度</div>
|
|
<div class="detail-value">{{ penHealthData.humidity }}<span class="unit">%</span></div>
|
|
<div class="detail-status" :class="getHumidityStatus(penHealthData.humidity)">{{ getHumidityStatusText(penHealthData.humidity) }}</div>
|
|
</div>
|
|
<div class="detail-item">
|
|
<div class="detail-label">通风指数</div>
|
|
<div class="detail-value">{{ penHealthData.ventilation }}<span class="unit">级</span></div>
|
|
<div class="detail-status normal">良好</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 健康指标趋势图 -->
|
|
<div class="card chart-card">
|
|
<div class="card-header">
|
|
<span class="title">📈 健康指标趋势 (7天)</span>
|
|
<div class="chart-tabs">
|
|
<button
|
|
v-for="metric in chartMetrics"
|
|
:key="metric.key"
|
|
:class="{ active: activeMetric === metric.key }"
|
|
@click="switchMetric(metric.key)"
|
|
>
|
|
{{ metric.name }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div ref="healthChart" class="chart-container"></div>
|
|
</div>
|
|
|
|
<!-- 个体骆驼健康数据 -->
|
|
<div class="card camel-list-card">
|
|
<div class="card-header">
|
|
<span class="title">🐪 个体骆驼健康数据</span>
|
|
<span class="sub">点击骆驼查看详细健康报告</span>
|
|
</div>
|
|
<div class="camel-search">
|
|
<input
|
|
type="text"
|
|
v-model="camelSearch"
|
|
placeholder="搜索骆驼编号..."
|
|
class="search-input"
|
|
/>
|
|
</div>
|
|
<div class="camel-grid">
|
|
<div
|
|
v-for="camel in filteredCamels"
|
|
:key="camel.id"
|
|
class="camel-item"
|
|
:class="{ warning: camel.healthStatus !== 'healthy' }"
|
|
@click="selectCamel(camel)"
|
|
>
|
|
<div class="camel-avatar">
|
|
<span class="camel-icon">🐪</span>
|
|
<span v-if="camel.healthStatus !== 'healthy'" class="warning-dot"></span>
|
|
</div>
|
|
<div class="camel-info">
|
|
<div class="camel-id">{{ camel.id }}</div>
|
|
<div class="camel-temp">{{ camel.temperature }}°C</div>
|
|
</div>
|
|
<div class="camel-status" :class="camel.healthStatus">
|
|
{{ getHealthStatusText(camel.healthStatus) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 右侧:个体骆驼详细健康报告 -->
|
|
<div class="col-right">
|
|
<div class="card health-report-card">
|
|
<div class="card-header">
|
|
<span class="title">📋 个体健康报告</span>
|
|
<span v-if="selectedCamel" class="sub">{{ selectedCamel.id }}</span>
|
|
</div>
|
|
<div v-if="selectedCamel" class="health-report">
|
|
<div class="report-header">
|
|
<div class="camel-avatar-large">🐪</div>
|
|
<div class="camel-basic">
|
|
<div class="camel-name">{{ selectedCamel.id }}</div>
|
|
<div class="camel-location">圈舍: {{ selectedCamel.pen }}</div>
|
|
</div>
|
|
<div class="health-badge" :class="selectedCamel.healthStatus">
|
|
{{ getHealthStatusText(selectedCamel.healthStatus) }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="report-metrics">
|
|
<div class="metric-row">
|
|
<div class="metric">
|
|
<div class="metric-label">体温</div>
|
|
<div class="metric-value" :class="getTempWarning(selectedCamel.temperature)">
|
|
{{ selectedCamel.temperature }}<span class="unit">°C</span>
|
|
</div>
|
|
<div class="metric-range">正常范围: 36.5-38.5°C</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-label">心率</div>
|
|
<div class="metric-value">{{ selectedCamel.heartRate }}<span class="unit">bpm</span></div>
|
|
<div class="metric-range">正常范围: 40-60 bpm</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-label">呼吸频率</div>
|
|
<div class="metric-value">{{ selectedCamel.respRate }}<span class="unit">次/分</span></div>
|
|
<div class="metric-range">正常范围: 8-16次/分</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="metric-row">
|
|
<div class="metric">
|
|
<div class="metric-label">活动量</div>
|
|
<div class="metric-value">{{ selectedCamel.activity }}<span class="unit">步</span></div>
|
|
<div class="metric-progress">
|
|
<div class="progress-bar" :style="{ width: (selectedCamel.activity / 8000 * 100) + '%' }"></div>
|
|
</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-label">饮水次数</div>
|
|
<div class="metric-value">{{ selectedCamel.waterIntake }}<span class="unit">次/日</span></div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-label">采食量</div>
|
|
<div class="metric-value">{{ selectedCamel.feedIntake }}<span class="unit">kg</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="report-timeline">
|
|
<div class="timeline-title">📅 近期健康记录</div>
|
|
<div class="timeline-list">
|
|
<div v-for="record in selectedCamel.healthRecords" :key="record.date" class="timeline-item">
|
|
<div class="timeline-date">{{ record.date }}</div>
|
|
<div class="timeline-content">
|
|
<div class="timeline-event">{{ record.event }}</div>
|
|
<div class="timeline-detail">{{ record.detail }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="no-selection">
|
|
<span class="placeholder-icon">🐪</span>
|
|
<p>点击左侧骆驼查看详细健康报告</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 健康建议 -->
|
|
<div class="card advice-card">
|
|
<div class="card-header">
|
|
<span class="title">💡 健康管理建议</span>
|
|
</div>
|
|
<div class="advice-list">
|
|
<div class="advice-item" v-for="(advice, idx) in healthAdvice" :key="idx">
|
|
<span class="advice-icon">{{ advice.icon }}</span>
|
|
<div class="advice-content">
|
|
<div class="advice-title">{{ advice.title }}</div>
|
|
<div class="advice-desc">{{ advice.desc }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import * as echarts from 'echarts';
|
|
import dayjs from 'dayjs';
|
|
import 'dayjs/locale/zh-cn';
|
|
|
|
export default {
|
|
name: 'HealthDashboard',
|
|
data() {
|
|
return {
|
|
// 驼圈数据 (根据Excel)
|
|
pens: [
|
|
{ name: 'A1圈', count: 35 }, { name: 'A2圈', count: 35 },
|
|
{ name: 'B1圈', count: 35 }, { name: 'B2圈', count: 35 },
|
|
{ name: 'C1圈', count: 35 }, { name: 'C2圈', count: 35 },
|
|
{ name: 'D1圈', count: 35 }, { name: 'D2圈', count: 35 },
|
|
{ name: 'E1圈', count: 35 }, { name: 'E2圈', count: 35 },
|
|
{ name: 'F1圈', count: 35 }, { name: 'F2圈', count: 35 },
|
|
{ name: 'G1圈', count: 35 }, { name: 'G2圈', count: 35 },
|
|
{ name: 'H1圈', count: 35 }, { name: 'H2圈', count: 35 },
|
|
{ name: 'I1圈', count: 35 }, { name: 'I2圈', count: 35 },
|
|
{ name: 'K1圈', count: 35 }, { name: 'K2圈', count: 35 }
|
|
],
|
|
selectedPen: 'A1圈',
|
|
|
|
// 健康统计数据
|
|
healthStats: {
|
|
totalCamels: 700,
|
|
healthyRate: 94.2,
|
|
avgTemp: 37.6,
|
|
warningCount: 42
|
|
},
|
|
|
|
// 健康预警
|
|
healthAlerts: [
|
|
{ level: 'high', levelText: '紧急', title: 'A1圈骆驼体温异常', desc: '编号A1-023体温39.2°C', time: '09:32' },
|
|
{ level: 'medium', levelText: '警告', title: 'B2圈空气质量下降', desc: '氨气浓度超标', time: '09:15' },
|
|
{ level: 'low', levelText: '提示', title: 'C1圈活动量偏低', desc: '3峰骆驼活动量低于正常值', time: '08:47' },
|
|
{ level: 'medium', levelText: '警告', title: 'E2圈饮水异常', desc: '2峰骆驼饮水次数减少', time: '07:22' }
|
|
],
|
|
|
|
// 圈舍健康数据
|
|
penHealthData: {
|
|
avgTemp: 37.5,
|
|
airQuality: 68,
|
|
humidity: 58,
|
|
ventilation: 3
|
|
},
|
|
|
|
// 图表配置
|
|
chartMetrics: [
|
|
{ key: 'temperature', name: '体温' },
|
|
{ key: 'activity', name: '活动量' },
|
|
{ key: 'healthRate', name: '健康率' }
|
|
],
|
|
activeMetric: 'temperature',
|
|
|
|
// 骆驼数据 (每个圈舍35峰骆驼)
|
|
camels: [],
|
|
camelSearch: '',
|
|
selectedCamel: null,
|
|
|
|
// 趋势数据
|
|
trendData: {
|
|
temperature: [37.2, 37.3, 37.4, 37.5, 37.6, 37.5, 37.4],
|
|
activity: [5200, 5400, 5600, 5800, 5900, 6100, 6000],
|
|
healthRate: [92.5, 93.1, 93.6, 93.8, 94.0, 94.1, 94.2]
|
|
},
|
|
|
|
// 健康建议
|
|
healthAdvice: [
|
|
{ icon: '🌡️', title: '体温监控', desc: 'A1圈有2峰骆驼体温偏高,建议隔离观察' },
|
|
{ icon: '💨', title: '通风改善', desc: 'B2圈空气质量下降,建议开启通风设备' },
|
|
{ icon: '💧', title: '饮水管理', desc: 'E2圈饮水异常,检查饮水设备是否故障' },
|
|
{ icon: '🏃', title: '活动促进', desc: 'C1圈活动量偏低,建议增加驱赶活动' }
|
|
],
|
|
|
|
// ECharts实例
|
|
healthChartIns: null,
|
|
|
|
// 时间标签
|
|
timeLabels: []
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
filteredCamels() {
|
|
if (!this.camelSearch) {
|
|
return this.camels.filter(c => c.pen === this.selectedPen);
|
|
}
|
|
return this.camels.filter(c =>
|
|
c.pen === this.selectedPen &&
|
|
c.id.toLowerCase().includes(this.camelSearch.toLowerCase())
|
|
);
|
|
}
|
|
},
|
|
|
|
mounted() {
|
|
this.getCurrentDateTime();
|
|
this.initTimeLabels();
|
|
this.initCamelData();
|
|
this.initHealthChart();
|
|
window.addEventListener('resize', this.handleResize);
|
|
},
|
|
|
|
beforeDestroy() {
|
|
if (this.healthChartIns) this.healthChartIns.dispose();
|
|
window.removeEventListener('resize', this.handleResize);
|
|
},
|
|
|
|
methods: {
|
|
getCurrentDateTime() {
|
|
const dateDom = document.querySelector('#cloudDate');
|
|
const timeDom = document.querySelector('#cloudTime');
|
|
const update = () => {
|
|
const now = dayjs().locale('zh-cn');
|
|
if (dateDom) dateDom.innerHTML = now.format('MM月DD日 dddd');
|
|
if (timeDom) timeDom.innerHTML = now.format('HH:mm:ss');
|
|
};
|
|
update();
|
|
setInterval(update, 1000);
|
|
},
|
|
|
|
initTimeLabels() {
|
|
for (let i = 6; i >= 0; i--) {
|
|
this.timeLabels.push(dayjs().subtract(i, 'day').format('MM/DD'));
|
|
}
|
|
},
|
|
|
|
initCamelData() {
|
|
// 为每个圈舍生成35峰骆驼数据
|
|
const allCamels = [];
|
|
const statuses = ['healthy', 'warning', 'critical'];
|
|
const statusWeights = [0.85, 0.12, 0.03];
|
|
|
|
this.pens.forEach(pen => {
|
|
for (let i = 1; i <= pen.count; i++) {
|
|
const random = Math.random();
|
|
let healthStatus = 'healthy';
|
|
if (random < statusWeights[0]) healthStatus = 'healthy';
|
|
else if (random < statusWeights[0] + statusWeights[1]) healthStatus = 'warning';
|
|
else healthStatus = 'critical';
|
|
|
|
const baseTemp = 37.5;
|
|
const tempOffset = healthStatus === 'healthy' ? (Math.random() - 0.5) * 0.8 :
|
|
healthStatus === 'warning' ? 0.8 + Math.random() * 0.5 :
|
|
1.3 + Math.random() * 0.7;
|
|
|
|
allCamels.push({
|
|
id: `${pen.name}-${String(i).padStart(3, '0')}`,
|
|
pen: pen.name,
|
|
temperature: +(baseTemp + (healthStatus !== 'healthy' ? tempOffset : tempOffset)).toFixed(1),
|
|
heartRate: Math.floor(45 + (healthStatus !== 'healthy' ? Math.random() * 15 : (Math.random() - 0.5) * 10)),
|
|
respRate: Math.floor(10 + (healthStatus !== 'healthy' ? Math.random() * 8 : (Math.random() - 0.5) * 4)),
|
|
activity: Math.floor(4000 + Math.random() * 4000),
|
|
waterIntake: Math.floor(3 + Math.random() * 5),
|
|
feedIntake: +(8 + Math.random() * 4).toFixed(1),
|
|
healthStatus: healthStatus,
|
|
healthRecords: this.generateHealthRecords(healthStatus)
|
|
});
|
|
}
|
|
});
|
|
this.camels = allCamels;
|
|
},
|
|
|
|
generateHealthRecords(status) {
|
|
const records = [];
|
|
const dates = [0, 1, 2, 3, 4, 5, 6];
|
|
for (let i = 0; i < 5; i++) {
|
|
const date = dayjs().subtract(dates[i], 'day').format('MM/DD');
|
|
if (status === 'healthy') {
|
|
records.push({
|
|
date: date,
|
|
event: '日常巡检',
|
|
detail: '体温正常,精神状态良好,采食饮水正常'
|
|
});
|
|
} else if (status === 'warning') {
|
|
records.push({
|
|
date: date,
|
|
event: i === 0 ? '体温偏高' : '持续观察',
|
|
detail: i === 0 ? '体温38.9°C,建议隔离观察' : '体温略高,精神状态尚可'
|
|
});
|
|
} else {
|
|
records.push({
|
|
date: date,
|
|
event: i === 0 ? '异常告警' : '医疗干预',
|
|
detail: i === 0 ? '体温39.5°C,食欲不振,精神萎靡' : '已用药治疗,继续观察'
|
|
});
|
|
}
|
|
}
|
|
return records;
|
|
},
|
|
|
|
selectPen(penName) {
|
|
this.selectedPen = penName;
|
|
this.selectedCamel = null;
|
|
// 更新圈舍健康数据 (模拟)
|
|
this.penHealthData = {
|
|
avgTemp: +(37.2 + Math.random() * 0.8).toFixed(1),
|
|
airQuality: Math.floor(40 + Math.random() * 50),
|
|
humidity: Math.floor(40 + Math.random() * 30),
|
|
ventilation: Math.floor(2 + Math.random() * 3)
|
|
};
|
|
},
|
|
|
|
getPenCount(penName) {
|
|
const pen = this.pens.find(p => p.name === penName);
|
|
return pen ? pen.count : 0;
|
|
},
|
|
|
|
selectCamel(camel) {
|
|
this.selectedCamel = camel;
|
|
},
|
|
|
|
getHealthStatusText(status) {
|
|
const map = {
|
|
healthy: '健康',
|
|
warning: '亚健康',
|
|
critical: '异常'
|
|
};
|
|
return map[status] || '未知';
|
|
},
|
|
|
|
getTempStatus(temp) {
|
|
if (temp < 36.5) return 'warning';
|
|
if (temp > 38.5) return 'danger';
|
|
return 'normal';
|
|
},
|
|
|
|
getTempStatusText(temp) {
|
|
if (temp < 36.5) return '偏低';
|
|
if (temp > 38.5) return '偏高';
|
|
return '正常';
|
|
},
|
|
|
|
getAirStatus(airQuality) {
|
|
if (airQuality > 100) return 'danger';
|
|
if (airQuality > 70) return 'warning';
|
|
return 'normal';
|
|
},
|
|
|
|
getAirStatusText(airQuality) {
|
|
if (airQuality > 100) return '差';
|
|
if (airQuality > 70) return '一般';
|
|
return '良好';
|
|
},
|
|
|
|
getHumidityStatus(humidity) {
|
|
if (humidity > 75) return 'warning';
|
|
if (humidity < 40) return 'warning';
|
|
return 'normal';
|
|
},
|
|
|
|
getHumidityStatusText(humidity) {
|
|
if (humidity > 75) return '偏高';
|
|
if (humidity < 40) return '偏低';
|
|
return '适宜';
|
|
},
|
|
|
|
getTempWarning(temp) {
|
|
if (temp < 36.5 || temp > 38.5) return 'warning';
|
|
return '';
|
|
},
|
|
|
|
initHealthChart() {
|
|
const chartDom = this.$refs.healthChart;
|
|
this.healthChartIns = echarts.init(chartDom);
|
|
this.updateHealthChart();
|
|
},
|
|
|
|
updateHealthChart() {
|
|
const currentData = this.trendData[this.activeMetric];
|
|
const unitMap = {
|
|
temperature: '°C',
|
|
activity: '步',
|
|
healthRate: '%'
|
|
};
|
|
const nameMap = {
|
|
temperature: '平均体温',
|
|
activity: '平均活动量',
|
|
healthRate: '健康率'
|
|
};
|
|
const yAxisMin = this.activeMetric === 'healthRate' ? 90 : null;
|
|
|
|
this.healthChartIns.setOption({
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
borderColor: '#3498db',
|
|
textStyle: { color: '#fff' }
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
data: this.timeLabels,
|
|
axisLabel: { color: '#fff' },
|
|
axisLine: { lineStyle: { color: '#5dade2' } }
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
name: `${nameMap[this.activeMetric]} (${unitMap[this.activeMetric]})`,
|
|
nameTextStyle: { color: '#fff' },
|
|
axisLabel: { color: '#fff' },
|
|
splitLine: { lineStyle: { color: 'rgba(93, 173, 226, 0.3)' } },
|
|
axisLine: { lineStyle: { color: '#5dade2' } },
|
|
min: yAxisMin
|
|
},
|
|
series: [{
|
|
data: currentData,
|
|
type: 'line',
|
|
smooth: true,
|
|
lineStyle: { width: 3, color: '#5dade2' },
|
|
areaStyle: { opacity: 0.2, color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
{ offset: 0, color: '#3498db' }, { offset: 1, color: 'rgba(52, 152, 219, 0.1)' }
|
|
]) },
|
|
symbol: 'circle',
|
|
symbolSize: 8,
|
|
itemStyle: { color: '#3498db' }
|
|
}],
|
|
grid: { containLabel: true, bottom: 20, top: 30, right: 20, left: 55 }
|
|
});
|
|
},
|
|
|
|
switchMetric(metric) {
|
|
this.activeMetric = metric;
|
|
this.updateHealthChart();
|
|
},
|
|
|
|
|
|
|
|
handleResize() {
|
|
if (this.healthChartIns) this.healthChartIns.resize();
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.health-dashboard {
|
|
width: 100%;
|
|
height: calc(100vh - 100px);
|
|
background: transparent;
|
|
padding: 20px 24px 30px;
|
|
box-sizing: border-box;
|
|
font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', sans-serif;
|
|
overflow-y: auto;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
|
|
.dashboard-main {
|
|
display: flex;
|
|
gap: 20px;
|
|
flex-wrap: wrap;
|
|
align-items: stretch;
|
|
|
|
.col-left, .col-right {
|
|
flex: 1.1;
|
|
min-width: 280px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
.col-center {
|
|
flex: 1.8;
|
|
min-width: 420px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
}
|
|
|
|
.card {
|
|
background: transparent;
|
|
backdrop-filter: blur(0);
|
|
border-radius: 16px;
|
|
border: 1.5px solid rgba(52, 152, 219, 0.7);
|
|
overflow: hidden;
|
|
transition: all 0.3s ease;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
&:hover {
|
|
border-color: #5dade2;
|
|
box-shadow: 0 4px 20px rgba(52, 152, 219, 0.25);
|
|
background: rgba(52, 152, 219, 0.05);
|
|
}
|
|
|
|
.card-header {
|
|
padding: 14px 20px;
|
|
border-bottom: 1px solid rgba(52, 152, 219, 0.4);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
flex-shrink: 0;
|
|
|
|
.title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
letter-spacing: 0.5px;
|
|
text-shadow: 0 0 5px rgba(52, 152, 219, 0.5);
|
|
}
|
|
.badge {
|
|
background: #e74c3c;
|
|
border-radius: 30px;
|
|
padding: 2px 10px;
|
|
font-size: 12px;
|
|
color: white;
|
|
}
|
|
.sub {
|
|
font-size: 11px;
|
|
color: rgba(255, 255, 255, 0.7);
|
|
}
|
|
.chart-tabs {
|
|
display: flex;
|
|
gap: 8px;
|
|
button {
|
|
background: rgba(52, 152, 219, 0.2);
|
|
border: 1px solid rgba(52, 152, 219, 0.4);
|
|
border-radius: 20px;
|
|
padding: 4px 12px;
|
|
font-size: 11px;
|
|
color: rgba(255, 255, 255, 0.8);
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
&:hover, &.active {
|
|
background: #3498db;
|
|
border-color: #3498db;
|
|
color: white;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 驼圈列表
|
|
.pen-list-card {
|
|
.pen-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 10px;
|
|
padding: 16px;
|
|
.pen-item {
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border: 1px solid rgba(52, 152, 219, 0.4);
|
|
border-radius: 12px;
|
|
padding: 12px 8px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
&:hover, &.active {
|
|
border-color: #5dade2;
|
|
background: rgba(52, 152, 219, 0.15);
|
|
transform: translateY(-2px);
|
|
}
|
|
.pen-name {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #fff;
|
|
margin-bottom: 4px;
|
|
}
|
|
.pen-count {
|
|
font-size: 11px;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 统计卡片
|
|
.stats-card {
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 16px;
|
|
padding: 20px;
|
|
.stat-item {
|
|
text-align: center;
|
|
.stat-value {
|
|
font-size: 28px;
|
|
font-weight: bold;
|
|
color: #5dade2;
|
|
.unit {
|
|
font-size: 12px;
|
|
margin-left: 2px;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
}
|
|
}
|
|
.stat-label {
|
|
font-size: 11px;
|
|
color: rgba(255, 255, 255, 0.7);
|
|
margin: 4px 0;
|
|
}
|
|
.stat-trend {
|
|
font-size: 10px;
|
|
&.up { color: #2ecc71; }
|
|
&.danger { color: #e74c3c; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 预警列表
|
|
.alert-card {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.alert-list {
|
|
padding: 8px 16px;
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
.alert-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
border-bottom: 1px solid rgba(52, 152, 219, 0.2);
|
|
padding: 12px 4px;
|
|
.alert-level {
|
|
font-size: 10px;
|
|
padding: 2px 8px;
|
|
border-radius: 20px;
|
|
min-width: 40px;
|
|
text-align: center;
|
|
&.high {
|
|
background: rgba(231, 76, 60, 0.2);
|
|
color: #e74c3c;
|
|
}
|
|
&.medium {
|
|
background: rgba(243, 156, 18, 0.2);
|
|
color: #f39c12;
|
|
}
|
|
&.low {
|
|
background: rgba(52, 152, 219, 0.2);
|
|
color: #3498db;
|
|
}
|
|
}
|
|
.alert-content {
|
|
flex: 1;
|
|
.alert-title {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: rgba(255, 255, 255, 0.9);
|
|
}
|
|
.alert-desc {
|
|
font-size: 11px;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
margin-top: 2px;
|
|
}
|
|
}
|
|
.alert-time {
|
|
font-size: 10px;
|
|
color: rgba(255, 255, 255, 0.4);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 圈舍详情
|
|
.pen-detail-card {
|
|
.pen-detail {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 16px;
|
|
padding: 20px;
|
|
.detail-item {
|
|
text-align: center;
|
|
.detail-label {
|
|
font-size: 11px;
|
|
color: rgba(255, 255, 255, 0.7);
|
|
margin-bottom: 6px;
|
|
}
|
|
.detail-value {
|
|
font-size: 22px;
|
|
font-weight: bold;
|
|
color: #5dade2;
|
|
.unit {
|
|
font-size: 11px;
|
|
margin-left: 2px;
|
|
color: rgba(255, 255, 255, 0.5);
|
|
}
|
|
}
|
|
.detail-status {
|
|
font-size: 10px;
|
|
margin-top: 4px;
|
|
&.normal { color: #2ecc71; }
|
|
&.warning { color: #f39c12; }
|
|
&.danger { color: #e74c3c; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 图表容器
|
|
.chart-container {
|
|
width: 100%;
|
|
height: 240px;
|
|
padding: 8px;
|
|
}
|
|
|
|
// 骆驼列表
|
|
.camel-list-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.camel-search {
|
|
padding: 12px 16px;
|
|
flex-shrink: 0;
|
|
.search-input {
|
|
width: 100%;
|
|
background: rgba(0, 0, 0, 0.4);
|
|
border: 1px solid rgba(52, 152, 219, 0.5);
|
|
border-radius: 20px;
|
|
padding: 8px 16px;
|
|
color: white;
|
|
font-size: 12px;
|
|
outline: none;
|
|
&:focus {
|
|
border-color: #5dade2;
|
|
}
|
|
&::placeholder {
|
|
color: rgba(255, 255, 255, 0.4);
|
|
}
|
|
}
|
|
}
|
|
.camel-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 12px;
|
|
padding: 16px;
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
max-height: 280px;
|
|
.camel-item {
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border: 1px solid rgba(52, 152, 219, 0.4);
|
|
border-radius: 12px;
|
|
padding: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
&:hover {
|
|
border-color: #5dade2;
|
|
background: rgba(52, 152, 219, 0.1);
|
|
}
|
|
&.warning {
|
|
border-color: rgba(243, 156, 18, 0.6);
|
|
}
|
|
.camel-avatar {
|
|
position: relative;
|
|
.camel-icon {
|
|
font-size: 28px;
|
|
}
|
|
.warning-dot {
|
|
position: absolute;
|
|
top: -2px;
|
|
right: -2px;
|
|
width: 10px;
|
|
height: 10px;
|
|
background: #f39c12;
|
|
border-radius: 50%;
|
|
animation: pulse 1.5s infinite;
|
|
}
|
|
}
|
|
.camel-info {
|
|
flex: 1;
|
|
.camel-id {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: #fff;
|
|
}
|
|
.camel-temp {
|
|
font-size: 10px;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
}
|
|
}
|
|
.camel-status {
|
|
font-size: 10px;
|
|
padding: 2px 6px;
|
|
border-radius: 10px;
|
|
&.healthy {
|
|
background: rgba(46, 204, 113, 0.2);
|
|
color: #2ecc71;
|
|
}
|
|
&.warning {
|
|
background: rgba(243, 156, 18, 0.2);
|
|
color: #f39c12;
|
|
}
|
|
&.critical {
|
|
background: rgba(231, 76, 60, 0.2);
|
|
color: #e74c3c;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 健康报告
|
|
.health-report-card {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.health-report {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.report-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 20px;
|
|
background: rgba(0, 0, 0, 0.2);
|
|
border-bottom: 1px solid rgba(52, 152, 219, 0.3);
|
|
flex-shrink: 0;
|
|
.camel-avatar-large {
|
|
font-size: 48px;
|
|
}
|
|
.camel-basic {
|
|
flex: 1;
|
|
.camel-name {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
color: #fff;
|
|
}
|
|
.camel-location {
|
|
font-size: 11px;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
margin-top: 4px;
|
|
}
|
|
}
|
|
.health-badge {
|
|
padding: 4px 12px;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
&.healthy {
|
|
background: rgba(46, 204, 113, 0.2);
|
|
color: #2ecc71;
|
|
}
|
|
&.warning {
|
|
background: rgba(243, 156, 18, 0.2);
|
|
color: #f39c12;
|
|
}
|
|
&.critical {
|
|
background: rgba(231, 76, 60, 0.2);
|
|
color: #e74c3c;
|
|
}
|
|
}
|
|
}
|
|
.report-metrics {
|
|
padding: 16px;
|
|
flex-shrink: 0;
|
|
.metric-row {
|
|
display: flex;
|
|
gap: 20px;
|
|
margin-bottom: 16px;
|
|
.metric {
|
|
flex: 1;
|
|
.metric-label {
|
|
font-size: 11px;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
margin-bottom: 4px;
|
|
}
|
|
.metric-value {
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
color: #5dade2;
|
|
&.warning {
|
|
color: #e74c3c;
|
|
}
|
|
.unit {
|
|
font-size: 11px;
|
|
margin-left: 2px;
|
|
color: rgba(255, 255, 255, 0.5);
|
|
}
|
|
}
|
|
.metric-range {
|
|
font-size: 10px;
|
|
color: rgba(255, 255, 255, 0.4);
|
|
margin-top: 4px;
|
|
}
|
|
.metric-progress {
|
|
margin-top: 8px;
|
|
background: rgba(52, 152, 219, 0.3);
|
|
border-radius: 10px;
|
|
height: 6px;
|
|
.progress-bar {
|
|
height: 6px;
|
|
background: #5dade2;
|
|
border-radius: 10px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.report-timeline {
|
|
padding: 16px;
|
|
border-top: 1px solid rgba(52, 152, 219, 0.3);
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
.timeline-title {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: #fff;
|
|
margin-bottom: 12px;
|
|
}
|
|
.timeline-list {
|
|
.timeline-item {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-bottom: 12px;
|
|
.timeline-date {
|
|
font-size: 10px;
|
|
color: rgba(255, 255, 255, 0.5);
|
|
min-width: 50px;
|
|
}
|
|
.timeline-content {
|
|
flex: 1;
|
|
.timeline-event {
|
|
font-size: 12px;
|
|
color: rgba(255, 255, 255, 0.9);
|
|
}
|
|
.timeline-detail {
|
|
font-size: 10px;
|
|
color: rgba(255, 255, 255, 0.5);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.no-selection {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
.placeholder-icon {
|
|
font-size: 64px;
|
|
opacity: 0.5;
|
|
display: block;
|
|
margin-bottom: 16px;
|
|
}
|
|
p {
|
|
color: rgba(255, 255, 255, 0.5);
|
|
font-size: 13px;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 健康建议
|
|
.advice-card {
|
|
.advice-list {
|
|
padding: 16px;
|
|
.advice-item {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
&:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
.advice-icon {
|
|
font-size: 20px;
|
|
}
|
|
.advice-content {
|
|
flex: 1;
|
|
.advice-title {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: #fff;
|
|
}
|
|
.advice-desc {
|
|
font-size: 11px;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
margin-top: 2px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; transform: scale(1); }
|
|
50% { opacity: 0.5; transform: scale(1.2); }
|
|
}
|
|
</style>
|