右旗智慧驼厂
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

<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>