MonitorPanel.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. <!-- MonitorPanel.vue 完整版(支持销毁) -->
  2. <template>
  3. <!-- 外层动画包裹层 -->
  4. <div class="panel-wrapper" v-if="monitorData">
  5. <div class="monitor-panel" ref="panelRef" :class="getStatusClass()">
  6. <div class="panel-title">{{ monitorData.positionName || 'XXXXX位置' }}</div>
  7. <div class="panel-grid">
  8. <div class="panel-item">
  9. <span class="item-label">▸ 微震测声</span>
  10. <span class="item-value" :class="getStatusClass('microseism')">
  11. {{ monitorData.microseism != null ? (monitorData.microseism == 0 ? '无震动' : '有震动') : '-' }}
  12. </span>
  13. </div>
  14. <div class="panel-item">
  15. <span class="item-label">▸ 光纤测温</span>
  16. <span class="item-value" :class="getStatusClass('fiberTemp')">
  17. {{ monitorData.fiberTemp ?? '-' }}
  18. </span>
  19. </div>
  20. <div class="panel-item">
  21. <span class="item-label">▸ 温度</span>
  22. <span class="item-value" :class="getStatusClass('temperature')">
  23. {{ monitorData.temperature ?? '-' }}
  24. </span>
  25. </div>
  26. <div class="panel-item">
  27. <span class="item-label">▸ 火焰</span>
  28. <span class="item-value" :class="getStatusClass('flame')">
  29. {{ monitorData.flame != null ? (monitorData.flame == 0 ? '无火' : '有火') : '-' }}
  30. </span>
  31. </div>
  32. <!-- <div class="panel-item item-full">
  33. <span class="item-label ai-label">▸ AI视频火焰识别</span>
  34. <span class="item-value" :class="getStatusClass('aiFlame')">
  35. {{ sensorData.aiFlame ?? '-' }}
  36. </span>
  37. </div> -->
  38. <div class="panel-item">
  39. <span class="item-label">▸ HCl</span>
  40. <span class="item-value" :class="getStatusClass('hcl')">
  41. {{ monitorData.hcl ?? '-' }}
  42. </span>
  43. </div>
  44. <div class="panel-item">
  45. <span class="item-label">▸ CO</span>
  46. <span class="item-value" :class="getStatusClass('co')">
  47. {{ monitorData.co ?? '-' }}
  48. </span>
  49. </div>
  50. <div class="panel-item">
  51. <span class="item-label">▸ 烟雾</span>
  52. <span class="item-value" :class="getStatusClass('smoke')">
  53. {{ monitorData.smoke != null ? (monitorData.smoke == 0 ? '无烟' : '有烟') : '-' }}
  54. </span>
  55. </div>
  56. <div class="panel-item">
  57. <span class="item-label">▸ 多参数</span>
  58. <span class="item-value" :class="getStatusClass('multiParam')">
  59. {{ monitorData.multiParam ?? '-' }}
  60. </span>
  61. </div>
  62. </div>
  63. <div class="panel-arrow"></div>
  64. </div>
  65. </div>
  66. </template>
  67. <script setup lang="ts">
  68. import { ref, onMounted, computed, onUnmounted } from 'vue';
  69. import { CSS3DObject } from 'three/examples/jsm/renderers/CSS3DRenderer.js';
  70. import type { Scene, Camera } from 'three';
  71. export interface AlarmThreshold {
  72. microseism?: number;
  73. hcl?: number;
  74. fiberTemp?: number;
  75. co?: number;
  76. temperature?: number;
  77. smoke?: number;
  78. flame?: number;
  79. multiParam?: number;
  80. aiFlame?: number;
  81. }
  82. export interface SensorData {
  83. positionName?: string;
  84. microseism?: number;
  85. hcl?: number;
  86. fiberTemp?: number;
  87. co?: number;
  88. temperature?: number;
  89. smoke?: number;
  90. flame?: number;
  91. multiParam?: number;
  92. aiFlame?: number;
  93. alarmLevel: number;
  94. }
  95. const props = defineProps({
  96. sensorData: {
  97. type: Object as () => SensorData,
  98. default: () => ({}),
  99. },
  100. threshold: {
  101. type: Object as () => AlarmThreshold,
  102. default: () => ({}),
  103. },
  104. position: {
  105. type: Array as () => [number, number, number],
  106. default: () => [0, 5, -10],
  107. },
  108. scale: {
  109. type: Number,
  110. default: 0.5,
  111. },
  112. threeScene: {
  113. type: Object as () => Scene,
  114. default: null,
  115. },
  116. instanceId: {
  117. type: String,
  118. default: '',
  119. },
  120. });
  121. const panelRef = ref<HTMLElement | null>(null);
  122. const cssObject = ref<CSS3DObject | null>(null);
  123. const monitorData = ref<SensorData>(props.sensorData);
  124. const threshold = ref<AlarmThreshold>(props.threshold);
  125. // 默认阈值
  126. const defaultThreshold: Required<AlarmThreshold> = {
  127. microseism: 100,
  128. hcl: 100,
  129. fiberTemp: 100,
  130. co: 10,
  131. temperature: 50,
  132. smoke: 30,
  133. flame: 1,
  134. multiParam: 100,
  135. aiFlame: 1,
  136. };
  137. // 合并阈值
  138. const currentThreshold = computed(() => ({
  139. ...defaultThreshold,
  140. ...props.threshold,
  141. }));
  142. // 判断状态:normal/warn/alarm
  143. const getStatusClass = (key?: keyof AlarmThreshold) => {
  144. debugger;
  145. if (key) {
  146. const value = props.sensorData[key];
  147. if (value == null) return 'normal';
  148. const threshold = currentThreshold.value[key]!;
  149. if (threshold >= 102) return `alarm alarm_${threshold}`;
  150. } else {
  151. // const threshold = monitorData.value.alarmLevel;
  152. const threshold = monitorData.value.alarmLevel;
  153. if (threshold >= 102) return `alarm alarm_${threshold}`;
  154. }
  155. return 'normal';
  156. };
  157. // 全局报警状态(任意项异常则面板变红)
  158. const hasAlarm = computed(() => {
  159. return Object.keys(currentThreshold.value).some((key) => {
  160. const value = props.sensorData[key as keyof AlarmThreshold];
  161. return value != null && value > currentThreshold.value[key as keyof AlarmThreshold]!;
  162. });
  163. });
  164. // 面朝相机更新
  165. const update = (camera: Camera) => {
  166. if (cssObject.value) {
  167. cssObject.value.lookAt(camera.position);
  168. // 修复翻转
  169. cssObject.value.rotation.z = Math.PI;
  170. cssObject.value.rotation.x = Math.PI;
  171. }
  172. };
  173. const updateMonitorData = (data: { sensorData: SensorData; threshold: AlarmThreshold }) => {
  174. monitorData.value = data.sensorData;
  175. threshold.value = data.threshold;
  176. };
  177. onMounted(() => {
  178. if (!panelRef.value) return;
  179. cssObject.value = new CSS3DObject(panelRef.value);
  180. cssObject.value.position.set(props.position[0], props.position[1] + 2, props.position[2] + 3.4);
  181. cssObject.value.rotation.z += Math.PI;
  182. cssObject.value.rotation.x += Math.PI;
  183. cssObject.value.scale.setScalar(props.scale);
  184. cssObject.value.lookAt(props.position[0], props.position[1] + 30, props.position[2] - 10);
  185. if (props.threeScene) {
  186. props.threeScene.add(cssObject.value);
  187. }
  188. });
  189. // 销毁方法
  190. const destroy = () => {
  191. if (cssObject.value && props.threeScene) {
  192. props.threeScene.remove(cssObject.value);
  193. (cssObject.value as any).element = null;
  194. cssObject.value = null;
  195. }
  196. };
  197. onUnmounted(() => {
  198. destroy();
  199. });
  200. // 暴露方法
  201. defineExpose({
  202. id: props.instanceId,
  203. cssObject: cssObject,
  204. update,
  205. destroy,
  206. updateMonitorData,
  207. });
  208. </script>
  209. <style scoped>
  210. /* ====================================== */
  211. /* 🔥 核心:从中间向两边扩散展开动画 */
  212. /* ====================================== */
  213. .panel-wrapper {
  214. animation: panelExpand 1.7s cubic-bezier(0.22, 1, 0.36, 1) forwards;
  215. }
  216. /* 扩散动画:中间 → 左右展开 */
  217. @keyframes panelExpand {
  218. 0% {
  219. opacity: 0;
  220. transform: scaleX(0); /* 中间闭合 */
  221. }
  222. 100% {
  223. opacity: 1;
  224. transform: scaleX(1); /* 展开到完整宽度 */
  225. }
  226. }
  227. /* ====================================== */
  228. /* 主面板(科技风 + 状态颜色) */
  229. /* ====================================== */
  230. .monitor-panel {
  231. width: 800px;
  232. background: linear-gradient(0deg, rgba(1, 38, 65, 0.8), rgba(0, 55, 92, 0.55));
  233. /* background: rgba(0, 47, 85, 0.7); */
  234. border: 5px solid #40a9ff;
  235. border-radius: 16px;
  236. padding: 40px 30px;
  237. box-sizing: border-box;
  238. position: absolute;
  239. font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
  240. color: #fff;
  241. box-shadow:
  242. 0 12px 40px rgba(64, 169, 255, 0.25),
  243. inset 0 0 0 1px rgba(64, 169, 255, 0.15);
  244. backdrop-filter: blur(12px);
  245. transition: all 0.4s ease;
  246. overflow: visible;
  247. transform-origin: center center; /* 扩散中心点:中间 */
  248. }
  249. /* 报警状态整体变红 */
  250. .monitor-panel.alarm {
  251. border-color: #ff4d4f;
  252. /* box-shadow: inset 0 0 60px rgb(206, 0, 0.7); 内发光效果
  253. animation: panelPulse 0.8s infinite alternate; */
  254. }
  255. .monitor-panel.alarm_102 {
  256. /* color: #ffdb3d; */
  257. box-shadow: inset 0 0 60px rgb(206, 202, 0); /* 内发光效果 */
  258. animation: panelPulse_102 0.8s infinite alternate;
  259. }
  260. @keyframes panelPulse_102 {
  261. 0% {
  262. border-color: #ffed4d55;
  263. box-shadow: inset 0 0 60px rgba(206, 175, 0, 0.5); /* 内发光效果 */
  264. }
  265. 100% {
  266. border-color: #bb9f0088;
  267. box-shadow: inset 0 0 60px rgb(206, 175, 0); /* 内发光效果 */
  268. }
  269. }
  270. .monitor-panel.alarm_103 {
  271. /* color: #ff5e00; */
  272. box-shadow: inset 0 0 60px rgb(206, 82, 0); /* 内发光效果 */
  273. animation: panelPulse_103 0.8s infinite alternate;
  274. }
  275. @keyframes panelPulse_103 {
  276. 0% {
  277. border-color: #ffb24d55;
  278. box-shadow: inset 0 0 60px rgba(206, 100, 0, 0.5); /* 内发光效果 */
  279. }
  280. 100% {
  281. border-color: #bb450088;
  282. box-shadow: inset 0 0 60px rgb(206, 82, 0); /* 内发光效果 */
  283. }
  284. }
  285. .monitor-panel.alarm_104 {
  286. box-shadow: inset 0 0 60px rgb(206, 0, 0.7); /* 内发光效果 */
  287. animation: panelPulse_104 0.8s infinite alternate;
  288. }
  289. @keyframes panelPulse_104 {
  290. 0% {
  291. border-color: #ff4d4f55;
  292. box-shadow: inset 0 0 60px rgb(206, 0, 0, 0.5); /* 内发光效果 */
  293. }
  294. 100% {
  295. border-color: #bb000388;
  296. box-shadow: inset 0 0 60px rgb(206, 0, 0.9); /* 内发光效果 */
  297. }
  298. }
  299. /* 标题 */
  300. .panel-title {
  301. text-align: center;
  302. font-size: 35px;
  303. font-weight: 700;
  304. margin-bottom: 18px;
  305. letter-spacing: 4px;
  306. width: 100%;
  307. padding: 5px 0;
  308. background: linear-gradient(90deg, #4195d211, #0068bd55, #4195d211);
  309. /* -webkit-background-clip: text; */
  310. /* -webkit-text-fill-color: transparent; */
  311. text-shadow: 0 0 16px rgba(64, 169, 255, 0.4);
  312. }
  313. /* 网格布局 */
  314. .panel-grid {
  315. display: grid;
  316. grid-template-columns: 1fr 1fr;
  317. gap: 16px;
  318. }
  319. .item-full {
  320. grid-column: 1 / -1;
  321. }
  322. /* 监控项 */
  323. .panel-item {
  324. display: flex;
  325. justify-content: space-between;
  326. align-items: center;
  327. background: rgba(30, 70, 120, 0.2);
  328. border: 3px solid rgba(64, 169, 255, 0.2);
  329. padding: 12px 20px;
  330. border-radius: 12px;
  331. font-size: 25px;
  332. transition: all 0.3s ease;
  333. cursor: default;
  334. }
  335. .panel-item:hover {
  336. background: rgba(40, 90, 150, 0.5);
  337. border-color: rgba(64, 169, 255, 0.6);
  338. transform: translateY(-2px);
  339. box-shadow: 0 4px 12px rgba(64, 169, 255, 0.2);
  340. }
  341. .item-label {
  342. display: flex;
  343. align-items: center;
  344. gap: 10px;
  345. font-weight: 500;
  346. color: #e6f3ff;
  347. }
  348. .ai-label {
  349. color: #00d9ff;
  350. text-shadow: 0 0 8px rgba(54, 207, 251, 0.3);
  351. }
  352. /* 数值正常颜色 */
  353. .item-value {
  354. font-weight: 700;
  355. font-size: 27px;
  356. color: #6abcff;
  357. letter-spacing: 2px;
  358. transition: all 0.3s ease;
  359. }
  360. /* 报警闪烁 */
  361. .item-value.alarm {
  362. /* color: #ff4d4f !important; */
  363. text-shadow: 0 0 12px rgba(255, 77, 79, 0.6);
  364. animation: valueBlink 0.8s infinite alternate;
  365. }
  366. .alarm_102 {
  367. color: #ffdb3d;
  368. }
  369. .alarm_103 {
  370. color: #ff5e00;
  371. }
  372. .alarm_104 {
  373. color: #ff2424;
  374. }
  375. @keyframes valueBlink {
  376. 0% {
  377. opacity: 0.6;
  378. transform: scale(1);
  379. }
  380. 100% {
  381. opacity: 1;
  382. transform: scale(1.05);
  383. }
  384. }
  385. /* 底部定位箭头 */
  386. .panel-arrow {
  387. position: absolute;
  388. bottom: -18px;
  389. left: 50%;
  390. transform: translateX(-50%);
  391. width: 0;
  392. height: 0;
  393. border-left: 18px solid transparent;
  394. border-right: 18px solid transparent;
  395. border-top: 18px solid #40a9ff;
  396. filter: drop-shadow(0 4px 8px rgba(64, 169, 255, 0.3));
  397. transition: all 0.4s ease;
  398. }
  399. .monitor-panel.alarm .panel-arrow {
  400. border-top-color: #cf0003;
  401. filter: drop-shadow(0 4px 8px rgba(255, 77, 79, 0.4));
  402. }
  403. </style>