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

1158 lines
34 KiB

  1. <template>
  2. <div class="health-dashboard">
  3. <!-- 主内容区域 -->
  4. <div class="dashboard-main">
  5. <!-- 左侧驼圈列表 -->
  6. <div class="col-left">
  7. <div class="card pen-list-card">
  8. <div class="card-header">
  9. <span class="title">🐫 驼圈列表</span>
  10. <span class="sub"> {{ pens.length }} 个圈舍</span>
  11. </div>
  12. <div class="pen-grid">
  13. <div
  14. v-for="pen in pens"
  15. :key="pen.name"
  16. class="pen-item"
  17. :class="{ active: selectedPen === pen.name }"
  18. @click="selectPen(pen.name)"
  19. >
  20. <div class="pen-name">{{ pen.name }}</div>
  21. <div class="pen-count">{{ pen.count }}</div>
  22. </div>
  23. </div>
  24. </div>
  25. <div class="card stats-card">
  26. <div class="card-header">
  27. <span class="title">📊 健康统计总览</span>
  28. </div>
  29. <div class="stats-grid">
  30. <div class="stat-item">
  31. <div class="stat-value">
  32. <animate-number :from="0" :to="healthStats.totalCamels" duration="3000" />
  33. <span class="unit"></span>
  34. </div>
  35. <div class="stat-label">总骆驼数</div>
  36. </div>
  37. <div class="stat-item">
  38. <div class="stat-value">
  39. <animate-number :from="0" :to="healthStats.healthyRate" duration="3000" />
  40. <span class="unit">%</span>
  41. </div>
  42. <div class="stat-label">健康率</div>
  43. <div class="stat-trend up"> 2.3%</div>
  44. </div>
  45. <div class="stat-item">
  46. <div class="stat-value">
  47. <animate-number :from="0" :to="healthStats.avgTemp" duration="3000" :decimals="1" />
  48. <span class="unit">°C</span>
  49. </div>
  50. <div class="stat-label">平均体温</div>
  51. </div>
  52. <div class="stat-item">
  53. <div class="stat-value">
  54. <animate-number :from="0" :to="healthStats.warningCount" duration="3000" />
  55. <span class="unit"></span>
  56. </div>
  57. <div class="stat-label">异常预警</div>
  58. <div class="stat-trend danger">需关注</div>
  59. </div>
  60. </div>
  61. </div>
  62. <div class="card alert-card">
  63. <div class="card-header">
  64. <span class="title"> 健康预警</span>
  65. <span class="badge">{{ healthAlerts.length }}</span>
  66. </div>
  67. <div class="alert-list">
  68. <div v-for="(alert, idx) in healthAlerts" :key="idx" class="alert-item">
  69. <span class="alert-level" :class="alert.level">{{ alert.levelText }}</span>
  70. <div class="alert-content">
  71. <div class="alert-title">{{ alert.title }}</div>
  72. <div class="alert-desc">{{ alert.desc }}</div>
  73. </div>
  74. <span class="alert-time">{{ alert.time }}</span>
  75. </div>
  76. </div>
  77. </div>
  78. </div>
  79. <!-- 中间驼圈详情 + 健康指标趋势 -->
  80. <div class="col-center">
  81. <div class="card pen-detail-card">
  82. <div class="card-header">
  83. <span class="title">📍 {{ selectedPen }} · 圈舍详情</span>
  84. <span class="sub">{{ getPenCount(selectedPen) }}峰骆驼</span>
  85. </div>
  86. <div class="pen-detail">
  87. <div class="detail-item">
  88. <div class="detail-label">平均体温</div>
  89. <div class="detail-value">{{ penHealthData.avgTemp }}<span class="unit">°C</span></div>
  90. <div class="detail-status" :class="getTempStatus(penHealthData.avgTemp)">{{ getTempStatusText(penHealthData.avgTemp) }}</div>
  91. </div>
  92. <div class="detail-item">
  93. <div class="detail-label">空气质量</div>
  94. <div class="detail-value">{{ penHealthData.airQuality }}<span class="unit">AQI</span></div>
  95. <div class="detail-status" :class="getAirStatus(penHealthData.airQuality)">{{ getAirStatusText(penHealthData.airQuality) }}</div>
  96. </div>
  97. <div class="detail-item">
  98. <div class="detail-label">湿度</div>
  99. <div class="detail-value">{{ penHealthData.humidity }}<span class="unit">%</span></div>
  100. <div class="detail-status" :class="getHumidityStatus(penHealthData.humidity)">{{ getHumidityStatusText(penHealthData.humidity) }}</div>
  101. </div>
  102. <div class="detail-item">
  103. <div class="detail-label">通风指数</div>
  104. <div class="detail-value">{{ penHealthData.ventilation }}<span class="unit"></span></div>
  105. <div class="detail-status normal">良好</div>
  106. </div>
  107. </div>
  108. </div>
  109. <!-- 健康指标趋势图 -->
  110. <div class="card chart-card">
  111. <div class="card-header">
  112. <span class="title">📈 健康指标趋势 (7)</span>
  113. <div class="chart-tabs">
  114. <button
  115. v-for="metric in chartMetrics"
  116. :key="metric.key"
  117. :class="{ active: activeMetric === metric.key }"
  118. @click="switchMetric(metric.key)"
  119. >
  120. {{ metric.name }}
  121. </button>
  122. </div>
  123. </div>
  124. <div ref="healthChart" class="chart-container"></div>
  125. </div>
  126. <!-- 个体骆驼健康数据 -->
  127. <div class="card camel-list-card">
  128. <div class="card-header">
  129. <span class="title">🐪 个体骆驼健康数据</span>
  130. <span class="sub">点击骆驼查看详细健康报告</span>
  131. </div>
  132. <div class="camel-search">
  133. <input
  134. type="text"
  135. v-model="camelSearch"
  136. placeholder="搜索骆驼编号..."
  137. class="search-input"
  138. />
  139. </div>
  140. <div class="camel-grid">
  141. <div
  142. v-for="camel in filteredCamels"
  143. :key="camel.id"
  144. class="camel-item"
  145. :class="{ warning: camel.healthStatus !== 'healthy' }"
  146. @click="selectCamel(camel)"
  147. >
  148. <div class="camel-avatar">
  149. <span class="camel-icon">🐪</span>
  150. <span v-if="camel.healthStatus !== 'healthy'" class="warning-dot"></span>
  151. </div>
  152. <div class="camel-info">
  153. <div class="camel-id">{{ camel.id }}</div>
  154. <div class="camel-temp">{{ camel.temperature }}°C</div>
  155. </div>
  156. <div class="camel-status" :class="camel.healthStatus">
  157. {{ getHealthStatusText(camel.healthStatus) }}
  158. </div>
  159. </div>
  160. </div>
  161. </div>
  162. </div>
  163. <!-- 右侧个体骆驼详细健康报告 -->
  164. <div class="col-right">
  165. <div class="card health-report-card">
  166. <div class="card-header">
  167. <span class="title">📋 个体健康报告</span>
  168. <span v-if="selectedCamel" class="sub">{{ selectedCamel.id }}</span>
  169. </div>
  170. <div v-if="selectedCamel" class="health-report">
  171. <div class="report-header">
  172. <div class="camel-avatar-large">🐪</div>
  173. <div class="camel-basic">
  174. <div class="camel-name">{{ selectedCamel.id }}</div>
  175. <div class="camel-location">圈舍: {{ selectedCamel.pen }}</div>
  176. </div>
  177. <div class="health-badge" :class="selectedCamel.healthStatus">
  178. {{ getHealthStatusText(selectedCamel.healthStatus) }}
  179. </div>
  180. </div>
  181. <div class="report-metrics">
  182. <div class="metric-row">
  183. <div class="metric">
  184. <div class="metric-label">体温</div>
  185. <div class="metric-value" :class="getTempWarning(selectedCamel.temperature)">
  186. {{ selectedCamel.temperature }}<span class="unit">°C</span>
  187. </div>
  188. <div class="metric-range">正常范围: 36.5-38.5°C</div>
  189. </div>
  190. <div class="metric">
  191. <div class="metric-label">心率</div>
  192. <div class="metric-value">{{ selectedCamel.heartRate }}<span class="unit">bpm</span></div>
  193. <div class="metric-range">正常范围: 40-60 bpm</div>
  194. </div>
  195. <div class="metric">
  196. <div class="metric-label">呼吸频率</div>
  197. <div class="metric-value">{{ selectedCamel.respRate }}<span class="unit">/</span></div>
  198. <div class="metric-range">正常范围: 8-16/</div>
  199. </div>
  200. </div>
  201. <div class="metric-row">
  202. <div class="metric">
  203. <div class="metric-label">活动量</div>
  204. <div class="metric-value">{{ selectedCamel.activity }}<span class="unit"></span></div>
  205. <div class="metric-progress">
  206. <div class="progress-bar" :style="{ width: (selectedCamel.activity / 8000 * 100) + '%' }"></div>
  207. </div>
  208. </div>
  209. <div class="metric">
  210. <div class="metric-label">饮水次数</div>
  211. <div class="metric-value">{{ selectedCamel.waterIntake }}<span class="unit">/</span></div>
  212. </div>
  213. <div class="metric">
  214. <div class="metric-label">采食量</div>
  215. <div class="metric-value">{{ selectedCamel.feedIntake }}<span class="unit">kg</span></div>
  216. </div>
  217. </div>
  218. </div>
  219. <div class="report-timeline">
  220. <div class="timeline-title">📅 近期健康记录</div>
  221. <div class="timeline-list">
  222. <div v-for="record in selectedCamel.healthRecords" :key="record.date" class="timeline-item">
  223. <div class="timeline-date">{{ record.date }}</div>
  224. <div class="timeline-content">
  225. <div class="timeline-event">{{ record.event }}</div>
  226. <div class="timeline-detail">{{ record.detail }}</div>
  227. </div>
  228. </div>
  229. </div>
  230. </div>
  231. </div>
  232. <div v-else class="no-selection">
  233. <span class="placeholder-icon">🐪</span>
  234. <p>点击左侧骆驼查看详细健康报告</p>
  235. </div>
  236. </div>
  237. <!-- 健康建议 -->
  238. <div class="card advice-card">
  239. <div class="card-header">
  240. <span class="title">💡 健康管理建议</span>
  241. </div>
  242. <div class="advice-list">
  243. <div class="advice-item" v-for="(advice, idx) in healthAdvice" :key="idx">
  244. <span class="advice-icon">{{ advice.icon }}</span>
  245. <div class="advice-content">
  246. <div class="advice-title">{{ advice.title }}</div>
  247. <div class="advice-desc">{{ advice.desc }}</div>
  248. </div>
  249. </div>
  250. </div>
  251. </div>
  252. </div>
  253. </div>
  254. </div>
  255. </template>
  256. <script>
  257. import * as echarts from 'echarts';
  258. import dayjs from 'dayjs';
  259. import 'dayjs/locale/zh-cn';
  260. export default {
  261. name: 'HealthDashboard',
  262. data() {
  263. return {
  264. // 驼圈数据 (根据Excel)
  265. pens: [
  266. { name: 'A1圈', count: 35 }, { name: 'A2圈', count: 35 },
  267. { name: 'B1圈', count: 35 }, { name: 'B2圈', count: 35 },
  268. { name: 'C1圈', count: 35 }, { name: 'C2圈', count: 35 },
  269. { name: 'D1圈', count: 35 }, { name: 'D2圈', count: 35 },
  270. { name: 'E1圈', count: 35 }, { name: 'E2圈', count: 35 },
  271. { name: 'F1圈', count: 35 }, { name: 'F2圈', count: 35 },
  272. { name: 'G1圈', count: 35 }, { name: 'G2圈', count: 35 },
  273. { name: 'H1圈', count: 35 }, { name: 'H2圈', count: 35 },
  274. { name: 'I1圈', count: 35 }, { name: 'I2圈', count: 35 },
  275. { name: 'K1圈', count: 35 }, { name: 'K2圈', count: 35 }
  276. ],
  277. selectedPen: 'A1圈',
  278. // 健康统计数据
  279. healthStats: {
  280. totalCamels: 700,
  281. healthyRate: 94.2,
  282. avgTemp: 37.6,
  283. warningCount: 42
  284. },
  285. // 健康预警
  286. healthAlerts: [
  287. { level: 'high', levelText: '紧急', title: 'A1圈骆驼体温异常', desc: '编号A1-023体温39.2°C', time: '09:32' },
  288. { level: 'medium', levelText: '警告', title: 'B2圈空气质量下降', desc: '氨气浓度超标', time: '09:15' },
  289. { level: 'low', levelText: '提示', title: 'C1圈活动量偏低', desc: '3峰骆驼活动量低于正常值', time: '08:47' },
  290. { level: 'medium', levelText: '警告', title: 'E2圈饮水异常', desc: '2峰骆驼饮水次数减少', time: '07:22' }
  291. ],
  292. // 圈舍健康数据
  293. penHealthData: {
  294. avgTemp: 37.5,
  295. airQuality: 68,
  296. humidity: 58,
  297. ventilation: 3
  298. },
  299. // 图表配置
  300. chartMetrics: [
  301. { key: 'temperature', name: '体温' },
  302. { key: 'activity', name: '活动量' },
  303. { key: 'healthRate', name: '健康率' }
  304. ],
  305. activeMetric: 'temperature',
  306. // 骆驼数据 (每个圈舍35峰骆驼)
  307. camels: [],
  308. camelSearch: '',
  309. selectedCamel: null,
  310. // 趋势数据
  311. trendData: {
  312. temperature: [37.2, 37.3, 37.4, 37.5, 37.6, 37.5, 37.4],
  313. activity: [5200, 5400, 5600, 5800, 5900, 6100, 6000],
  314. healthRate: [92.5, 93.1, 93.6, 93.8, 94.0, 94.1, 94.2]
  315. },
  316. // 健康建议
  317. healthAdvice: [
  318. { icon: '🌡️', title: '体温监控', desc: 'A1圈有2峰骆驼体温偏高,建议隔离观察' },
  319. { icon: '💨', title: '通风改善', desc: 'B2圈空气质量下降,建议开启通风设备' },
  320. { icon: '💧', title: '饮水管理', desc: 'E2圈饮水异常,检查饮水设备是否故障' },
  321. { icon: '🏃', title: '活动促进', desc: 'C1圈活动量偏低,建议增加驱赶活动' }
  322. ],
  323. // ECharts实例
  324. healthChartIns: null,
  325. // 时间标签
  326. timeLabels: []
  327. };
  328. },
  329. computed: {
  330. filteredCamels() {
  331. if (!this.camelSearch) {
  332. return this.camels.filter(c => c.pen === this.selectedPen);
  333. }
  334. return this.camels.filter(c =>
  335. c.pen === this.selectedPen &&
  336. c.id.toLowerCase().includes(this.camelSearch.toLowerCase())
  337. );
  338. }
  339. },
  340. mounted() {
  341. this.getCurrentDateTime();
  342. this.initTimeLabels();
  343. this.initCamelData();
  344. this.initHealthChart();
  345. window.addEventListener('resize', this.handleResize);
  346. },
  347. beforeDestroy() {
  348. if (this.healthChartIns) this.healthChartIns.dispose();
  349. window.removeEventListener('resize', this.handleResize);
  350. },
  351. methods: {
  352. getCurrentDateTime() {
  353. const dateDom = document.querySelector('#cloudDate');
  354. const timeDom = document.querySelector('#cloudTime');
  355. const update = () => {
  356. const now = dayjs().locale('zh-cn');
  357. if (dateDom) dateDom.innerHTML = now.format('MM月DD日 dddd');
  358. if (timeDom) timeDom.innerHTML = now.format('HH:mm:ss');
  359. };
  360. update();
  361. setInterval(update, 1000);
  362. },
  363. initTimeLabels() {
  364. for (let i = 6; i >= 0; i--) {
  365. this.timeLabels.push(dayjs().subtract(i, 'day').format('MM/DD'));
  366. }
  367. },
  368. initCamelData() {
  369. // 为每个圈舍生成35峰骆驼数据
  370. const allCamels = [];
  371. const statuses = ['healthy', 'warning', 'critical'];
  372. const statusWeights = [0.85, 0.12, 0.03];
  373. this.pens.forEach(pen => {
  374. for (let i = 1; i <= pen.count; i++) {
  375. const random = Math.random();
  376. let healthStatus = 'healthy';
  377. if (random < statusWeights[0]) healthStatus = 'healthy';
  378. else if (random < statusWeights[0] + statusWeights[1]) healthStatus = 'warning';
  379. else healthStatus = 'critical';
  380. const baseTemp = 37.5;
  381. const tempOffset = healthStatus === 'healthy' ? (Math.random() - 0.5) * 0.8 :
  382. healthStatus === 'warning' ? 0.8 + Math.random() * 0.5 :
  383. 1.3 + Math.random() * 0.7;
  384. allCamels.push({
  385. id: `${pen.name}-${String(i).padStart(3, '0')}`,
  386. pen: pen.name,
  387. temperature: +(baseTemp + (healthStatus !== 'healthy' ? tempOffset : tempOffset)).toFixed(1),
  388. heartRate: Math.floor(45 + (healthStatus !== 'healthy' ? Math.random() * 15 : (Math.random() - 0.5) * 10)),
  389. respRate: Math.floor(10 + (healthStatus !== 'healthy' ? Math.random() * 8 : (Math.random() - 0.5) * 4)),
  390. activity: Math.floor(4000 + Math.random() * 4000),
  391. waterIntake: Math.floor(3 + Math.random() * 5),
  392. feedIntake: +(8 + Math.random() * 4).toFixed(1),
  393. healthStatus: healthStatus,
  394. healthRecords: this.generateHealthRecords(healthStatus)
  395. });
  396. }
  397. });
  398. this.camels = allCamels;
  399. },
  400. generateHealthRecords(status) {
  401. const records = [];
  402. const dates = [0, 1, 2, 3, 4, 5, 6];
  403. for (let i = 0; i < 5; i++) {
  404. const date = dayjs().subtract(dates[i], 'day').format('MM/DD');
  405. if (status === 'healthy') {
  406. records.push({
  407. date: date,
  408. event: '日常巡检',
  409. detail: '体温正常,精神状态良好,采食饮水正常'
  410. });
  411. } else if (status === 'warning') {
  412. records.push({
  413. date: date,
  414. event: i === 0 ? '体温偏高' : '持续观察',
  415. detail: i === 0 ? '体温38.9°C,建议隔离观察' : '体温略高,精神状态尚可'
  416. });
  417. } else {
  418. records.push({
  419. date: date,
  420. event: i === 0 ? '异常告警' : '医疗干预',
  421. detail: i === 0 ? '体温39.5°C,食欲不振,精神萎靡' : '已用药治疗,继续观察'
  422. });
  423. }
  424. }
  425. return records;
  426. },
  427. selectPen(penName) {
  428. this.selectedPen = penName;
  429. this.selectedCamel = null;
  430. // 更新圈舍健康数据 (模拟)
  431. this.penHealthData = {
  432. avgTemp: +(37.2 + Math.random() * 0.8).toFixed(1),
  433. airQuality: Math.floor(40 + Math.random() * 50),
  434. humidity: Math.floor(40 + Math.random() * 30),
  435. ventilation: Math.floor(2 + Math.random() * 3)
  436. };
  437. },
  438. getPenCount(penName) {
  439. const pen = this.pens.find(p => p.name === penName);
  440. return pen ? pen.count : 0;
  441. },
  442. selectCamel(camel) {
  443. this.selectedCamel = camel;
  444. },
  445. getHealthStatusText(status) {
  446. const map = {
  447. healthy: '健康',
  448. warning: '亚健康',
  449. critical: '异常'
  450. };
  451. return map[status] || '未知';
  452. },
  453. getTempStatus(temp) {
  454. if (temp < 36.5) return 'warning';
  455. if (temp > 38.5) return 'danger';
  456. return 'normal';
  457. },
  458. getTempStatusText(temp) {
  459. if (temp < 36.5) return '偏低';
  460. if (temp > 38.5) return '偏高';
  461. return '正常';
  462. },
  463. getAirStatus(airQuality) {
  464. if (airQuality > 100) return 'danger';
  465. if (airQuality > 70) return 'warning';
  466. return 'normal';
  467. },
  468. getAirStatusText(airQuality) {
  469. if (airQuality > 100) return '差';
  470. if (airQuality > 70) return '一般';
  471. return '良好';
  472. },
  473. getHumidityStatus(humidity) {
  474. if (humidity > 75) return 'warning';
  475. if (humidity < 40) return 'warning';
  476. return 'normal';
  477. },
  478. getHumidityStatusText(humidity) {
  479. if (humidity > 75) return '偏高';
  480. if (humidity < 40) return '偏低';
  481. return '适宜';
  482. },
  483. getTempWarning(temp) {
  484. if (temp < 36.5 || temp > 38.5) return 'warning';
  485. return '';
  486. },
  487. initHealthChart() {
  488. const chartDom = this.$refs.healthChart;
  489. this.healthChartIns = echarts.init(chartDom);
  490. this.updateHealthChart();
  491. },
  492. updateHealthChart() {
  493. const currentData = this.trendData[this.activeMetric];
  494. const unitMap = {
  495. temperature: '°C',
  496. activity: '步',
  497. healthRate: '%'
  498. };
  499. const nameMap = {
  500. temperature: '平均体温',
  501. activity: '平均活动量',
  502. healthRate: '健康率'
  503. };
  504. const yAxisMin = this.activeMetric === 'healthRate' ? 90 : null;
  505. this.healthChartIns.setOption({
  506. tooltip: {
  507. trigger: 'axis',
  508. backgroundColor: 'rgba(0,0,0,0.7)',
  509. borderColor: '#3498db',
  510. textStyle: { color: '#fff' }
  511. },
  512. xAxis: {
  513. type: 'category',
  514. data: this.timeLabels,
  515. axisLabel: { color: '#fff' },
  516. axisLine: { lineStyle: { color: '#5dade2' } }
  517. },
  518. yAxis: {
  519. type: 'value',
  520. name: `${nameMap[this.activeMetric]} (${unitMap[this.activeMetric]})`,
  521. nameTextStyle: { color: '#fff' },
  522. axisLabel: { color: '#fff' },
  523. splitLine: { lineStyle: { color: 'rgba(93, 173, 226, 0.3)' } },
  524. axisLine: { lineStyle: { color: '#5dade2' } },
  525. min: yAxisMin
  526. },
  527. series: [{
  528. data: currentData,
  529. type: 'line',
  530. smooth: true,
  531. lineStyle: { width: 3, color: '#5dade2' },
  532. areaStyle: { opacity: 0.2, color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  533. { offset: 0, color: '#3498db' }, { offset: 1, color: 'rgba(52, 152, 219, 0.1)' }
  534. ]) },
  535. symbol: 'circle',
  536. symbolSize: 8,
  537. itemStyle: { color: '#3498db' }
  538. }],
  539. grid: { containLabel: true, bottom: 20, top: 30, right: 20, left: 55 }
  540. });
  541. },
  542. switchMetric(metric) {
  543. this.activeMetric = metric;
  544. this.updateHealthChart();
  545. },
  546. handleResize() {
  547. if (this.healthChartIns) this.healthChartIns.resize();
  548. }
  549. }
  550. };
  551. </script>
  552. <style scoped lang="scss">
  553. .health-dashboard {
  554. width: 100%;
  555. height: calc(100vh - 100px);
  556. background: transparent;
  557. padding: 20px 24px 30px;
  558. box-sizing: border-box;
  559. font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', sans-serif;
  560. overflow-y: auto;
  561. overflow-x: auto;
  562. }
  563. .dashboard-main {
  564. display: flex;
  565. gap: 20px;
  566. flex-wrap: wrap;
  567. align-items: stretch;
  568. .col-left, .col-right {
  569. flex: 1.1;
  570. min-width: 280px;
  571. display: flex;
  572. flex-direction: column;
  573. gap: 20px;
  574. }
  575. .col-center {
  576. flex: 1.8;
  577. min-width: 420px;
  578. display: flex;
  579. flex-direction: column;
  580. gap: 20px;
  581. }
  582. }
  583. .card {
  584. background: transparent;
  585. backdrop-filter: blur(0);
  586. border-radius: 16px;
  587. border: 1.5px solid rgba(52, 152, 219, 0.7);
  588. overflow: hidden;
  589. transition: all 0.3s ease;
  590. display: flex;
  591. flex-direction: column;
  592. &:hover {
  593. border-color: #5dade2;
  594. box-shadow: 0 4px 20px rgba(52, 152, 219, 0.25);
  595. background: rgba(52, 152, 219, 0.05);
  596. }
  597. .card-header {
  598. padding: 14px 20px;
  599. border-bottom: 1px solid rgba(52, 152, 219, 0.4);
  600. display: flex;
  601. justify-content: space-between;
  602. align-items: center;
  603. flex-wrap: wrap;
  604. gap: 10px;
  605. flex-shrink: 0;
  606. .title {
  607. font-size: 16px;
  608. font-weight: 600;
  609. color: #fff;
  610. letter-spacing: 0.5px;
  611. text-shadow: 0 0 5px rgba(52, 152, 219, 0.5);
  612. }
  613. .badge {
  614. background: #e74c3c;
  615. border-radius: 30px;
  616. padding: 2px 10px;
  617. font-size: 12px;
  618. color: white;
  619. }
  620. .sub {
  621. font-size: 11px;
  622. color: rgba(255, 255, 255, 0.7);
  623. }
  624. .chart-tabs {
  625. display: flex;
  626. gap: 8px;
  627. button {
  628. background: rgba(52, 152, 219, 0.2);
  629. border: 1px solid rgba(52, 152, 219, 0.4);
  630. border-radius: 20px;
  631. padding: 4px 12px;
  632. font-size: 11px;
  633. color: rgba(255, 255, 255, 0.8);
  634. cursor: pointer;
  635. transition: all 0.2s;
  636. &:hover, &.active {
  637. background: #3498db;
  638. border-color: #3498db;
  639. color: white;
  640. }
  641. }
  642. }
  643. }
  644. }
  645. // 驼圈列表
  646. .pen-list-card {
  647. .pen-grid {
  648. display: grid;
  649. grid-template-columns: repeat(4, 1fr);
  650. gap: 10px;
  651. padding: 16px;
  652. .pen-item {
  653. background: rgba(0, 0, 0, 0.3);
  654. border: 1px solid rgba(52, 152, 219, 0.4);
  655. border-radius: 12px;
  656. padding: 12px 8px;
  657. text-align: center;
  658. cursor: pointer;
  659. transition: all 0.2s;
  660. &:hover, &.active {
  661. border-color: #5dade2;
  662. background: rgba(52, 152, 219, 0.15);
  663. transform: translateY(-2px);
  664. }
  665. .pen-name {
  666. font-size: 14px;
  667. font-weight: 500;
  668. color: #fff;
  669. margin-bottom: 4px;
  670. }
  671. .pen-count {
  672. font-size: 11px;
  673. color: rgba(255, 255, 255, 0.6);
  674. }
  675. }
  676. }
  677. }
  678. // 统计卡片
  679. .stats-card {
  680. .stats-grid {
  681. display: grid;
  682. grid-template-columns: repeat(2, 1fr);
  683. gap: 16px;
  684. padding: 20px;
  685. .stat-item {
  686. text-align: center;
  687. .stat-value {
  688. font-size: 28px;
  689. font-weight: bold;
  690. color: #5dade2;
  691. .unit {
  692. font-size: 12px;
  693. margin-left: 2px;
  694. color: rgba(255, 255, 255, 0.6);
  695. }
  696. }
  697. .stat-label {
  698. font-size: 11px;
  699. color: rgba(255, 255, 255, 0.7);
  700. margin: 4px 0;
  701. }
  702. .stat-trend {
  703. font-size: 10px;
  704. &.up { color: #2ecc71; }
  705. &.danger { color: #e74c3c; }
  706. }
  707. }
  708. }
  709. }
  710. // 预警列表
  711. .alert-card {
  712. flex: 1;
  713. display: flex;
  714. flex-direction: column;
  715. .alert-list {
  716. padding: 8px 16px;
  717. flex: 1;
  718. overflow-y: auto;
  719. .alert-item {
  720. display: flex;
  721. align-items: flex-start;
  722. gap: 12px;
  723. border-bottom: 1px solid rgba(52, 152, 219, 0.2);
  724. padding: 12px 4px;
  725. .alert-level {
  726. font-size: 10px;
  727. padding: 2px 8px;
  728. border-radius: 20px;
  729. min-width: 40px;
  730. text-align: center;
  731. &.high {
  732. background: rgba(231, 76, 60, 0.2);
  733. color: #e74c3c;
  734. }
  735. &.medium {
  736. background: rgba(243, 156, 18, 0.2);
  737. color: #f39c12;
  738. }
  739. &.low {
  740. background: rgba(52, 152, 219, 0.2);
  741. color: #3498db;
  742. }
  743. }
  744. .alert-content {
  745. flex: 1;
  746. .alert-title {
  747. font-size: 13px;
  748. font-weight: 500;
  749. color: rgba(255, 255, 255, 0.9);
  750. }
  751. .alert-desc {
  752. font-size: 11px;
  753. color: rgba(255, 255, 255, 0.6);
  754. margin-top: 2px;
  755. }
  756. }
  757. .alert-time {
  758. font-size: 10px;
  759. color: rgba(255, 255, 255, 0.4);
  760. }
  761. }
  762. }
  763. }
  764. // 圈舍详情
  765. .pen-detail-card {
  766. .pen-detail {
  767. display: grid;
  768. grid-template-columns: repeat(4, 1fr);
  769. gap: 16px;
  770. padding: 20px;
  771. .detail-item {
  772. text-align: center;
  773. .detail-label {
  774. font-size: 11px;
  775. color: rgba(255, 255, 255, 0.7);
  776. margin-bottom: 6px;
  777. }
  778. .detail-value {
  779. font-size: 22px;
  780. font-weight: bold;
  781. color: #5dade2;
  782. .unit {
  783. font-size: 11px;
  784. margin-left: 2px;
  785. color: rgba(255, 255, 255, 0.5);
  786. }
  787. }
  788. .detail-status {
  789. font-size: 10px;
  790. margin-top: 4px;
  791. &.normal { color: #2ecc71; }
  792. &.warning { color: #f39c12; }
  793. &.danger { color: #e74c3c; }
  794. }
  795. }
  796. }
  797. }
  798. // 图表容器
  799. .chart-container {
  800. width: 100%;
  801. height: 240px;
  802. padding: 8px;
  803. }
  804. // 骆驼列表
  805. .camel-list-card {
  806. display: flex;
  807. flex-direction: column;
  808. .camel-search {
  809. padding: 12px 16px;
  810. flex-shrink: 0;
  811. .search-input {
  812. width: 100%;
  813. background: rgba(0, 0, 0, 0.4);
  814. border: 1px solid rgba(52, 152, 219, 0.5);
  815. border-radius: 20px;
  816. padding: 8px 16px;
  817. color: white;
  818. font-size: 12px;
  819. outline: none;
  820. &:focus {
  821. border-color: #5dade2;
  822. }
  823. &::placeholder {
  824. color: rgba(255, 255, 255, 0.4);
  825. }
  826. }
  827. }
  828. .camel-grid {
  829. display: grid;
  830. grid-template-columns: repeat(3, 1fr);
  831. gap: 12px;
  832. padding: 16px;
  833. flex: 1;
  834. overflow-y: auto;
  835. max-height: 280px;
  836. .camel-item {
  837. background: rgba(0, 0, 0, 0.3);
  838. border: 1px solid rgba(52, 152, 219, 0.4);
  839. border-radius: 12px;
  840. padding: 10px;
  841. display: flex;
  842. align-items: center;
  843. gap: 10px;
  844. cursor: pointer;
  845. transition: all 0.2s;
  846. &:hover {
  847. border-color: #5dade2;
  848. background: rgba(52, 152, 219, 0.1);
  849. }
  850. &.warning {
  851. border-color: rgba(243, 156, 18, 0.6);
  852. }
  853. .camel-avatar {
  854. position: relative;
  855. .camel-icon {
  856. font-size: 28px;
  857. }
  858. .warning-dot {
  859. position: absolute;
  860. top: -2px;
  861. right: -2px;
  862. width: 10px;
  863. height: 10px;
  864. background: #f39c12;
  865. border-radius: 50%;
  866. animation: pulse 1.5s infinite;
  867. }
  868. }
  869. .camel-info {
  870. flex: 1;
  871. .camel-id {
  872. font-size: 12px;
  873. font-weight: 500;
  874. color: #fff;
  875. }
  876. .camel-temp {
  877. font-size: 10px;
  878. color: rgba(255, 255, 255, 0.6);
  879. }
  880. }
  881. .camel-status {
  882. font-size: 10px;
  883. padding: 2px 6px;
  884. border-radius: 10px;
  885. &.healthy {
  886. background: rgba(46, 204, 113, 0.2);
  887. color: #2ecc71;
  888. }
  889. &.warning {
  890. background: rgba(243, 156, 18, 0.2);
  891. color: #f39c12;
  892. }
  893. &.critical {
  894. background: rgba(231, 76, 60, 0.2);
  895. color: #e74c3c;
  896. }
  897. }
  898. }
  899. }
  900. }
  901. // 健康报告
  902. .health-report-card {
  903. flex: 1;
  904. display: flex;
  905. flex-direction: column;
  906. .health-report {
  907. flex: 1;
  908. display: flex;
  909. flex-direction: column;
  910. .report-header {
  911. display: flex;
  912. align-items: center;
  913. gap: 16px;
  914. padding: 20px;
  915. background: rgba(0, 0, 0, 0.2);
  916. border-bottom: 1px solid rgba(52, 152, 219, 0.3);
  917. flex-shrink: 0;
  918. .camel-avatar-large {
  919. font-size: 48px;
  920. }
  921. .camel-basic {
  922. flex: 1;
  923. .camel-name {
  924. font-size: 18px;
  925. font-weight: bold;
  926. color: #fff;
  927. }
  928. .camel-location {
  929. font-size: 11px;
  930. color: rgba(255, 255, 255, 0.6);
  931. margin-top: 4px;
  932. }
  933. }
  934. .health-badge {
  935. padding: 4px 12px;
  936. border-radius: 20px;
  937. font-size: 12px;
  938. font-weight: 500;
  939. &.healthy {
  940. background: rgba(46, 204, 113, 0.2);
  941. color: #2ecc71;
  942. }
  943. &.warning {
  944. background: rgba(243, 156, 18, 0.2);
  945. color: #f39c12;
  946. }
  947. &.critical {
  948. background: rgba(231, 76, 60, 0.2);
  949. color: #e74c3c;
  950. }
  951. }
  952. }
  953. .report-metrics {
  954. padding: 16px;
  955. flex-shrink: 0;
  956. .metric-row {
  957. display: flex;
  958. gap: 20px;
  959. margin-bottom: 16px;
  960. .metric {
  961. flex: 1;
  962. .metric-label {
  963. font-size: 11px;
  964. color: rgba(255, 255, 255, 0.6);
  965. margin-bottom: 4px;
  966. }
  967. .metric-value {
  968. font-size: 20px;
  969. font-weight: bold;
  970. color: #5dade2;
  971. &.warning {
  972. color: #e74c3c;
  973. }
  974. .unit {
  975. font-size: 11px;
  976. margin-left: 2px;
  977. color: rgba(255, 255, 255, 0.5);
  978. }
  979. }
  980. .metric-range {
  981. font-size: 10px;
  982. color: rgba(255, 255, 255, 0.4);
  983. margin-top: 4px;
  984. }
  985. .metric-progress {
  986. margin-top: 8px;
  987. background: rgba(52, 152, 219, 0.3);
  988. border-radius: 10px;
  989. height: 6px;
  990. .progress-bar {
  991. height: 6px;
  992. background: #5dade2;
  993. border-radius: 10px;
  994. }
  995. }
  996. }
  997. }
  998. }
  999. .report-timeline {
  1000. padding: 16px;
  1001. border-top: 1px solid rgba(52, 152, 219, 0.3);
  1002. flex: 1;
  1003. overflow-y: auto;
  1004. .timeline-title {
  1005. font-size: 13px;
  1006. font-weight: 500;
  1007. color: #fff;
  1008. margin-bottom: 12px;
  1009. }
  1010. .timeline-list {
  1011. .timeline-item {
  1012. display: flex;
  1013. gap: 12px;
  1014. margin-bottom: 12px;
  1015. .timeline-date {
  1016. font-size: 10px;
  1017. color: rgba(255, 255, 255, 0.5);
  1018. min-width: 50px;
  1019. }
  1020. .timeline-content {
  1021. flex: 1;
  1022. .timeline-event {
  1023. font-size: 12px;
  1024. color: rgba(255, 255, 255, 0.9);
  1025. }
  1026. .timeline-detail {
  1027. font-size: 10px;
  1028. color: rgba(255, 255, 255, 0.5);
  1029. }
  1030. }
  1031. }
  1032. }
  1033. }
  1034. }
  1035. .no-selection {
  1036. text-align: center;
  1037. padding: 60px 20px;
  1038. flex: 1;
  1039. display: flex;
  1040. flex-direction: column;
  1041. justify-content: center;
  1042. align-items: center;
  1043. .placeholder-icon {
  1044. font-size: 64px;
  1045. opacity: 0.5;
  1046. display: block;
  1047. margin-bottom: 16px;
  1048. }
  1049. p {
  1050. color: rgba(255, 255, 255, 0.5);
  1051. font-size: 13px;
  1052. }
  1053. }
  1054. }
  1055. // 健康建议
  1056. .advice-card {
  1057. .advice-list {
  1058. padding: 16px;
  1059. .advice-item {
  1060. display: flex;
  1061. gap: 12px;
  1062. margin-bottom: 16px;
  1063. &:last-child {
  1064. margin-bottom: 0;
  1065. }
  1066. .advice-icon {
  1067. font-size: 20px;
  1068. }
  1069. .advice-content {
  1070. flex: 1;
  1071. .advice-title {
  1072. font-size: 13px;
  1073. font-weight: 500;
  1074. color: #fff;
  1075. }
  1076. .advice-desc {
  1077. font-size: 11px;
  1078. color: rgba(255, 255, 255, 0.6);
  1079. margin-top: 2px;
  1080. }
  1081. }
  1082. }
  1083. }
  1084. }
  1085. @keyframes pulse {
  1086. 0%, 100% { opacity: 1; transform: scale(1); }
  1087. 50% { opacity: 0.5; transform: scale(1.2); }
  1088. }
  1089. </style>