|
|
@@ -0,0 +1,433 @@
|
|
|
+<!-- MonitorPanel.vue 完整版(支持销毁) -->
|
|
|
+<template>
|
|
|
+ <!-- 外层动画包裹层 -->
|
|
|
+
|
|
|
+ <div class="panel-wrapper">
|
|
|
+ <!-- 'monitor-panel-zzg': sensorData.positionName == '总支管#', -->
|
|
|
+ <div class="monitor-panel" :class="{ alarm: hasAlarm }" ref="panelRef">
|
|
|
+ <!-- <div style="position: absolute; top: -80px; right: 20px; font-size: 50px; width: 100%; text-align: center">总支管</div> -->
|
|
|
+ <div>
|
|
|
+ <div class="title"><div class="line"></div>{{ monitorData.positionName }}旋进漩涡流量计<div class="line"></div></div>
|
|
|
+ <div class="panel-grid">
|
|
|
+ <div class="panel-item">
|
|
|
+ <span class="item-label">▸瞬时标况流量 </span>
|
|
|
+ <div>
|
|
|
+ <span class="item-value" :class="getStatusClass('flowStdInstant')">
|
|
|
+ {{ monitorData.flowStdInstant ?? 'XXXX' }}
|
|
|
+ </span>
|
|
|
+ <span>Nm3/h</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="panel-item">
|
|
|
+ <span class="item-label">▸ 压力</span>
|
|
|
+ <div>
|
|
|
+ <span class="item-value" :class="getStatusClass('injectionPressure')">
|
|
|
+ {{ monitorData.injectionPressure ?? 'XXXX' }}
|
|
|
+ </span>
|
|
|
+ <span>MPa</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="panel-item">
|
|
|
+ <span class="item-label">▸ 温度 </span>
|
|
|
+ <div>
|
|
|
+ <span class="item-value" :class="getStatusClass('injectionTemperature')">
|
|
|
+ {{ monitorData.injectionTemperature ?? 'XXXX' }}
|
|
|
+ </span>
|
|
|
+ <span>℃</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="panel-item">
|
|
|
+ <span class="item-label">▸ 累计注气量</span>
|
|
|
+ <div>
|
|
|
+ <span class="item-value" :class="getStatusClass('flowStdAccum')">
|
|
|
+ {{ monitorData.flowStdAccum ?? 'XXXX' }}
|
|
|
+ </span>
|
|
|
+ <span>Nm3</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <div class="title"><div class="line"></div>{{ monitorData.positionName }}阀门<div class="line"></div></div>
|
|
|
+ <div class="panel-grid">
|
|
|
+ <div class="panel-item">
|
|
|
+ <span class="item-label">▸ 阀门开度 </span>
|
|
|
+ <div>
|
|
|
+ <span class="item-value" :class="getStatusClass('valveOpening')">
|
|
|
+ {{ monitorData.valveOpening ?? 'XXXX' }}
|
|
|
+ </span>
|
|
|
+ <span>°</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div :class="{ 'panel-arrow': monitorData.positionName !== '总支管#', 'panel-line': monitorData.positionName == '总支管#' }"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+ import { ref, onMounted, computed, onUnmounted, Ref, watch } from 'vue';
|
|
|
+ import { CSS3DObject } from 'three/examples/jsm/renderers/CSS3DRenderer.js';
|
|
|
+ import type { Scene, Camera } from 'three';
|
|
|
+
|
|
|
+ export interface AlarmThreshold {
|
|
|
+ flowStdInstant?: number;
|
|
|
+ flowStdAccum?: number;
|
|
|
+ injectionTemperature?: number;
|
|
|
+ injectionPressure?: number;
|
|
|
+ valveOpening?: number;
|
|
|
+ paiqiStatus?: number;
|
|
|
+ }
|
|
|
+
|
|
|
+ export interface SensorData {
|
|
|
+ positionName?: string;
|
|
|
+ flowStdInstant?: number;
|
|
|
+ flowStdAccum?: number;
|
|
|
+ injectionTemperature?: number;
|
|
|
+ injectionPressure?: number;
|
|
|
+ valveOpening?: number;
|
|
|
+ paiqiStatus?: number;
|
|
|
+ }
|
|
|
+
|
|
|
+ const props = defineProps({
|
|
|
+ sensorData: {
|
|
|
+ type: Object as () => Ref<SensorData>,
|
|
|
+ default: () => ({}),
|
|
|
+ },
|
|
|
+ threshold: {
|
|
|
+ type: Object as () => AlarmThreshold,
|
|
|
+ default: () => ({}),
|
|
|
+ },
|
|
|
+ position: {
|
|
|
+ type: Array as () => [number, number, number],
|
|
|
+ default: () => [0, 5, -10],
|
|
|
+ },
|
|
|
+ scale: {
|
|
|
+ type: Number,
|
|
|
+ default: 0.5,
|
|
|
+ },
|
|
|
+ threeScene: {
|
|
|
+ type: Object as () => Scene,
|
|
|
+ default: null,
|
|
|
+ },
|
|
|
+ instanceId: {
|
|
|
+ type: String,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ const panelRef = ref<HTMLElement | null>(null);
|
|
|
+ const cssObject = ref<CSS3DObject | null>(null);
|
|
|
+ const monitorData = ref<SensorData>({});
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => props.sensorData.value,
|
|
|
+ (newValue, oldValue) => {
|
|
|
+ if (newValue) monitorData.value = newValue;
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+ );
|
|
|
+
|
|
|
+ // 默认阈值
|
|
|
+ const defaultThreshold: Required<AlarmThreshold> = {
|
|
|
+ flowStdInstant: 100,
|
|
|
+ flowStdAccum: 100,
|
|
|
+ injectionTemperature: 100,
|
|
|
+ injectionPressure: 10,
|
|
|
+ valveOpening: 50,
|
|
|
+ paiqiStatus: 30,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 合并阈值
|
|
|
+ const currentThreshold = computed(() => ({
|
|
|
+ ...defaultThreshold,
|
|
|
+ ...props.threshold,
|
|
|
+ }));
|
|
|
+
|
|
|
+ // ✅ 正确:访问 .value
|
|
|
+ const getStatusClass = (key: keyof AlarmThreshold) => {
|
|
|
+ // 1. 先获取当前数据对象
|
|
|
+ const currentData = props.sensorData.value;
|
|
|
+
|
|
|
+ // 2. 安全访问属性
|
|
|
+ const value = currentData ? currentData[key] : undefined;
|
|
|
+
|
|
|
+ if (value == null) return 'normal';
|
|
|
+
|
|
|
+ const threshold = currentThreshold.value[key]!;
|
|
|
+ if (value > threshold) {
|
|
|
+ return 'alarm';
|
|
|
+ }
|
|
|
+ return 'normal';
|
|
|
+ };
|
|
|
+
|
|
|
+ // ✅ 正确:hasAlarm 也要修改
|
|
|
+ const hasAlarm = computed(() => {
|
|
|
+ const currentData = props.sensorData.value; // 👈 获取值
|
|
|
+ if (!currentData) return false;
|
|
|
+
|
|
|
+ return Object.keys(currentThreshold.value).some((key) => {
|
|
|
+ const value = currentData[key as keyof AlarmThreshold];
|
|
|
+ return value != null && value > currentThreshold.value[key as keyof AlarmThreshold]!;
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 面朝相机更新
|
|
|
+ const update = (camera: Camera) => {
|
|
|
+ if (cssObject.value) {
|
|
|
+ // cssObject.value.lookAt(camera.position);
|
|
|
+ // 修复翻转
|
|
|
+ // cssObject.value.rotation.z = Math.PI;
|
|
|
+ // cssObject.value.rotation.x = Math.PI;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ if (!panelRef.value) return;
|
|
|
+ cssObject.value = new CSS3DObject(panelRef.value);
|
|
|
+ cssObject.value.scale.set(props.scale, props.scale, props.scale);
|
|
|
+ // cssObject.value.scale.set(0, 0, 0);
|
|
|
+ cssObject.value.position.set(props.position[0], props.position[1] + 1.56, props.position[2]);
|
|
|
+ // cssObject.value.center.set(0, 0);
|
|
|
+ cssObject.value.rotation.y -= Math.PI / 2;
|
|
|
+
|
|
|
+ if (props.threeScene) {
|
|
|
+ props.threeScene.add(cssObject.value);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 销毁方法
|
|
|
+ const destroy = () => {
|
|
|
+ if (cssObject.value && props.threeScene) {
|
|
|
+ props.threeScene.remove(cssObject.value);
|
|
|
+ (cssObject.value as any).element = null;
|
|
|
+ cssObject.value = null;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ onUnmounted(() => {
|
|
|
+ destroy();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 暴露方法
|
|
|
+ defineExpose({
|
|
|
+ id: props.instanceId,
|
|
|
+ cssObject: cssObject,
|
|
|
+ update,
|
|
|
+ destroy,
|
|
|
+ });
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="less">
|
|
|
+ /* ====================================== */
|
|
|
+ /* 🔥 核心:从中间向两边扩散展开动画 */
|
|
|
+ /* ====================================== */
|
|
|
+ .panel-wrapper {
|
|
|
+ animation: panelExpand 1.7s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 扩散动画:中间 → 左右展开 */
|
|
|
+ @keyframes panelExpand {
|
|
|
+ 0% {
|
|
|
+ opacity: 0;
|
|
|
+ transform: scaleX(0); /* 中间闭合 */
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ opacity: 1;
|
|
|
+ transform: scaleX(1); /* 展开到完整宽度 */
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ====================================== */
|
|
|
+ /* 主面板(科技风 + 状态颜色) */
|
|
|
+ /* ====================================== */
|
|
|
+ .monitor-panel {
|
|
|
+ width: 1000px;
|
|
|
+ background: linear-gradient(145deg, rgba(10, 42, 60, 0.55), rgba(15, 40, 80, 0.9));
|
|
|
+ border: 5px solid #40d9ff;
|
|
|
+
|
|
|
+ border-radius: 16px;
|
|
|
+ padding: 20px 24px 40px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ position: absolute;
|
|
|
+ font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
|
|
|
+ color: #fff;
|
|
|
+ box-shadow:
|
|
|
+ 0 12px 40px rgba(64, 169, 255, 0.25),
|
|
|
+ inset 0 0 0 1px rgba(64, 169, 255, 0.15);
|
|
|
+ backdrop-filter: blur(12px);
|
|
|
+ transition: all 0.4s ease;
|
|
|
+ overflow: visible;
|
|
|
+ transform-origin: center center; /* 扩散中心点:中间 */
|
|
|
+ }
|
|
|
+ .monitor-panel-zzg {
|
|
|
+ background: linear-gradient(145deg, rgba(10, 50, 60, 0.55), rgba(15, 56, 80, 0.9));
|
|
|
+ border: 5px solid #9b9b9b;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 报警状态整体变红 */
|
|
|
+ .monitor-panel .alarm {
|
|
|
+ border-color: #ff4d4f;
|
|
|
+
|
|
|
+ box-shadow:
|
|
|
+ 0 20px 40px rgba(255, 77, 79, 0.35),
|
|
|
+ inset 0 0 6px 2px rgba(255, 77, 79, 0.2);
|
|
|
+ animation: panelPulse 1.5s infinite alternate;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes panelPulse {
|
|
|
+ 0% {
|
|
|
+ box-shadow:
|
|
|
+ 0 20px 40px rgba(255, 77, 79, 0.35),
|
|
|
+ inset 0 0 6px 2px rgba(255, 77, 79, 0.2);
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ box-shadow:
|
|
|
+ 0 20px 50px rgba(255, 77, 79, 0.5),
|
|
|
+ inset 0 0 6px 2px rgba(255, 77, 79, 0.3);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 标题 */
|
|
|
+ .panel-title {
|
|
|
+ text-align: center;
|
|
|
+ font-size: 40px;
|
|
|
+ font-weight: 700;
|
|
|
+ margin-bottom: 24px;
|
|
|
+ letter-spacing: 4px;
|
|
|
+ background: linear-gradient(90deg, #40a9ff, #69c0ff);
|
|
|
+ -webkit-background-clip: text;
|
|
|
+ -webkit-text-fill-color: transparent;
|
|
|
+ text-shadow: 0 0 16px rgba(64, 169, 255, 0.4);
|
|
|
+ position: absolute;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 网格布局 */
|
|
|
+ .panel-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 16px;
|
|
|
+ }
|
|
|
+ .title {
|
|
|
+ width: 100%;
|
|
|
+ font-size: 35px;
|
|
|
+ text-align: center;
|
|
|
+ padding: 20px 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ .line {
|
|
|
+ flex: 1;
|
|
|
+ height: 1.2px;
|
|
|
+ background-color: #36cffb;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .item-full {
|
|
|
+ grid-column: 1 / -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 监控项 */
|
|
|
+ .panel-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ background: rgba(30, 70, 120, 0.4);
|
|
|
+ border: 3px solid rgba(64, 169, 255, 0.3);
|
|
|
+ padding: 12px 10px;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-size: 28px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ cursor: default;
|
|
|
+ }
|
|
|
+
|
|
|
+ .panel-item:hover {
|
|
|
+ background: rgba(40, 90, 150, 0.5);
|
|
|
+ border-color: rgba(64, 169, 255, 0.6);
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 12px rgba(64, 169, 255, 0.2);
|
|
|
+ }
|
|
|
+
|
|
|
+ .item-label {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #e6f3ff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ai-label {
|
|
|
+ color: #36cffb;
|
|
|
+ text-shadow: 0 0 8px rgba(54, 207, 251, 0.3);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 数值正常颜色 */
|
|
|
+ .item-value {
|
|
|
+ font-weight: 700;
|
|
|
+ font-size: 27px;
|
|
|
+ color: #40a9ff;
|
|
|
+ letter-spacing: 2px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ padding-right: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 报警闪烁 */
|
|
|
+ .item-value .alarm {
|
|
|
+ color: #ff4d4f !important;
|
|
|
+ text-shadow: 0 0 12px rgba(255, 77, 79, 0.6);
|
|
|
+ animation: valueBlink 0.8s infinite alternate;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes valueBlink {
|
|
|
+ 0% {
|
|
|
+ opacity: 0.6;
|
|
|
+ transform: scale(1);
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ opacity: 1;
|
|
|
+ transform: scale(1.05);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 底部定位箭头 */
|
|
|
+ .panel-arrow {
|
|
|
+ position: absolute;
|
|
|
+ bottom: -18px;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ border-left: 18px solid transparent;
|
|
|
+ border-right: 18px solid transparent;
|
|
|
+ border-top: 18px solid #1eddff;
|
|
|
+ filter: drop-shadow(0 4px 8px rgba(64, 169, 255, 0.3));
|
|
|
+ transition: all 0.4s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .panel-line {
|
|
|
+ position: absolute;
|
|
|
+ bottom: -180px;
|
|
|
+ left: -180px;
|
|
|
+ // transform: translateX(-50%);
|
|
|
+ width: 180px;
|
|
|
+ height: 8px;
|
|
|
+ background: #7f8f9c;
|
|
|
+ border-left: 2px solid #00aaff; /* 斜线颜色粗细 */
|
|
|
+ border-bottom: 2px solid #00aaff;
|
|
|
+ transform: skewY(-45deg); /* 角度 */
|
|
|
+ transform-origin: left bottom; /* 从左下角开始 */
|
|
|
+ // border-left: 18px solid transparent;
|
|
|
+ // border-right: 18px solid transparent;
|
|
|
+ // border-top: 18px solid #40a9ff;
|
|
|
+ // filter: drop-shadow(0 4px 8px rgba(64, 169, 255, 0.3));
|
|
|
+ // transition: all 0.4s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .monitor-panel .alarm .panel-arrow {
|
|
|
+ border-top-color: #ff4d4f;
|
|
|
+ filter: drop-shadow(0 8px 16px rgba(255, 77, 79, 0.4));
|
|
|
+ }
|
|
|
+</style>
|