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