|
|
@@ -0,0 +1,357 @@
|
|
|
+<template>
|
|
|
+ <div class="dcs_tanks">
|
|
|
+ <!-- 顶部固定头部 -->
|
|
|
+ <div class="fixed-header" ref="fixedHeader">
|
|
|
+ <HeaderComponent title="碳酸钠-流程控制" backTo="/controlPage/flowSelect" />
|
|
|
+ <PageNav :items="navItems" :currentCode="flowCode" @navClick="onNavClick"/>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 主内容区 -->
|
|
|
+ <div class="main-container">
|
|
|
+ <div class="main-content">
|
|
|
+ <div class="content-area">
|
|
|
+ <div v-if="isLoading" class="loading">正在加载设备数据...</div>
|
|
|
+ <div v-if="hasError" class="error">
|
|
|
+ <p>加载数据失败: {{ errorMessage }}</p>
|
|
|
+ <button @click="loadInitialData">重试</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else class="tanks_container">
|
|
|
+ <div v-for="(equipments, tankName) in deviceConfigGroup" :key="tankName"
|
|
|
+ class="tank_section" :id="`tank-${tankName}`">
|
|
|
+ <h2 class="tank_title">{{ tankName }}</h2>
|
|
|
+ <div class="equipment_grid">
|
|
|
+ <component v-for="equipment in equipments" :key="equipment.code"
|
|
|
+ :is="getComponentByType(equipment.equipmentType)"
|
|
|
+ :title="equipment.title + '-' + equipment.equipmentName"
|
|
|
+ :code="equipment.code"
|
|
|
+ :dataArr="getValueByCode(tankName, equipment.code)"
|
|
|
+ class="equipment_item" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="isEmptyData" class="no_data">未获取到设备数据</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右侧导航 -->
|
|
|
+ <TankNavigation :tanks="allTanks"
|
|
|
+ :current-index="currentNavIndex"
|
|
|
+ :header-height="headerHeight"
|
|
|
+ @tankClick="scrollToTank"
|
|
|
+ @scrollPositionChange="handleScrollPositionChange" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed, onMounted, onBeforeUnmount, watchEffect } from 'vue';
|
|
|
+import { useRoute, useRouter } from 'vue-router';
|
|
|
+import HeaderComponent from '@/components/DCS/HeaderComponent.vue';
|
|
|
+import PageNav from '@/components/GeneralComponents/control/PageNavComponent.vue';
|
|
|
+import TankNavigation from '@/components/GeneralComponents/control/TankNavigationComponent.vue';
|
|
|
+import SensorControl from '@/components/GeneralComponents/control/SensorControl2Component.vue';
|
|
|
+import PumpControlComponent from '@/components/GeneralComponents/control/PumpControl2Component.vue';
|
|
|
+import ValveControlComponent from '@/components/GeneralComponents/control/ValveControl2Component.vue';
|
|
|
+import { navItems_Na2SO4 } from '@/config';
|
|
|
+import { stompClient } from '@/utils/ws/stompClient';
|
|
|
+import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment';
|
|
|
+
|
|
|
+const route = useRoute();
|
|
|
+const router = useRouter();
|
|
|
+
|
|
|
+const navItems = navItems_Na2SO4; // 暂时前端常量
|
|
|
+const flowCode = computed(() => route.params.flowCode);
|
|
|
+const title = computed(() => navItems.find(i => i.code === flowCode.value)?.label || '');
|
|
|
+
|
|
|
+const isLoading = ref(true);
|
|
|
+const hasError = ref(false);
|
|
|
+const errorMessage = ref('');
|
|
|
+const isEmptyData = ref(false);
|
|
|
+
|
|
|
+const fixedHeader = ref(null);
|
|
|
+const headerHeight = ref(0);
|
|
|
+const allTanks = ref([]);
|
|
|
+const currentNavIndex = ref(-1);
|
|
|
+
|
|
|
+const deviceConfigGroup = ref({});
|
|
|
+const deviceDataGroup = ref({});
|
|
|
+
|
|
|
+function getComponentByType(type) {
|
|
|
+ if ([1,5].includes(Number(type))) return ValveControlComponent;
|
|
|
+ if (Number(type) === 2) return PumpControlComponent;
|
|
|
+ if (Number(type) === 4) return SensorControl;
|
|
|
+ return 'div';
|
|
|
+}
|
|
|
+
|
|
|
+function getValueByCode(tankName, code) {
|
|
|
+ const dataArr = deviceDataGroup.value[tankName] || [];
|
|
|
+ const target = dataArr.find(item => item.code === code);
|
|
|
+ return target?.value || [];
|
|
|
+}
|
|
|
+
|
|
|
+async function loadInitialData() {
|
|
|
+ isLoading.value = true;
|
|
|
+ hasError.value = false;
|
|
|
+ errorMessage.value = '';
|
|
|
+ try {
|
|
|
+ const res = await getPageEquipmentGroupByTankByFlowCode(flowCode.value);
|
|
|
+ deviceConfigGroup.value = Object.fromEntries(
|
|
|
+ Object.entries(res.data || {}).filter(([_, e]) => e?.length)
|
|
|
+ );
|
|
|
+ isEmptyData.value = Object.keys(deviceConfigGroup.value).length === 0;
|
|
|
+
|
|
|
+ // WebSocket 订阅
|
|
|
+ stompClient.unsubscribeFromPage(flowCode.value);
|
|
|
+ stompClient.subscribeToPage(flowCode.value, data => {
|
|
|
+ if (data) {
|
|
|
+ for (const key in data) deviceDataGroup.value[key] = data[key] || [];
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } catch (err) {
|
|
|
+ hasError.value = true;
|
|
|
+ errorMessage.value = err.message || '加载失败';
|
|
|
+ } finally {
|
|
|
+ isLoading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+watchEffect(() => {
|
|
|
+ allTanks.value = Object.keys(deviceConfigGroup.value).map(name => ({
|
|
|
+ uniqueId: `tank-${name}`,
|
|
|
+ tankName: name,
|
|
|
+ shortName: name.length > 18 ? name.slice(0,16)+'...' : name
|
|
|
+ }));
|
|
|
+});
|
|
|
+
|
|
|
+function calculateHeaderHeight() { headerHeight.value = fixedHeader.value?.offsetHeight || 0; }
|
|
|
+
|
|
|
+function scrollToTank(tank,index) {
|
|
|
+ currentNavIndex.value = index;
|
|
|
+ const el = document.getElementById(`tank-${tank.tankName}`);
|
|
|
+ if(el) window.scrollTo({top: el.offsetTop - headerHeight.value - 20, behavior:'smooth'});
|
|
|
+}
|
|
|
+
|
|
|
+function handleScrollPositionChange(index){ currentNavIndex.value = index; }
|
|
|
+
|
|
|
+// 点击导航栏跳转页面
|
|
|
+function onNavClick(item) {
|
|
|
+ router.push(item.path)
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ calculateHeaderHeight();
|
|
|
+ window.addEventListener('resize', calculateHeaderHeight);
|
|
|
+ loadInitialData();
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ stompClient.unsubscribeFromPage(flowCode.value);
|
|
|
+ window.removeEventListener('resize', calculateHeaderHeight);
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.dcs_tanks {
|
|
|
+ width: 100%;
|
|
|
+ min-height: 100vh;
|
|
|
+ background-color: #141414;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+// 固定在顶部的头部区域
|
|
|
+.fixed-header {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ z-index: 1000;
|
|
|
+ background-color: #141414;
|
|
|
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+// 主容器 - 添加顶部内边距,避免被固定头部遮挡
|
|
|
+.main-container {
|
|
|
+ padding: 140px 32px 24px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+// 主内容区布局
|
|
|
+.main-content {
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+ width: 100%;
|
|
|
+ max-width: 1600px;
|
|
|
+ margin: 0 auto;
|
|
|
+}
|
|
|
+
|
|
|
+.content-area {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+// 罐体容器
|
|
|
+.tanks_container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 30px;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+// 每个罐体区域
|
|
|
+.tank_section {
|
|
|
+ background: #1a1a1a;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 20px 24px;
|
|
|
+ width: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 20px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ scroll-margin-top: 160px;
|
|
|
+ /* 用于滚动定位时预留空间 */
|
|
|
+}
|
|
|
+
|
|
|
+.tank_title {
|
|
|
+ font-size: 20px;
|
|
|
+ color: #fff;
|
|
|
+ padding-bottom: 12px;
|
|
|
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
+ text-align: center;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+// 设备网格布局
|
|
|
+.equipment_grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
+ /* 默认4列布局 */
|
|
|
+ gap: 24px;
|
|
|
+ /* 增大间距避免拥挤 */
|
|
|
+ padding: 12px 0;
|
|
|
+}
|
|
|
+
|
|
|
+// 统一设备项样式
|
|
|
+.equipment_item {
|
|
|
+ min-height: 200px;
|
|
|
+ /* 固定最小高度,确保排列整齐 */
|
|
|
+ padding: 16px;
|
|
|
+ background-color: rgba(30, 30, 30, 0.9);
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid rgba(114, 224, 255, 0.15);
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+/* 确保子组件正确填充容器 */
|
|
|
+:deep(.equipment_item) {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+
|
|
|
+ .title {
|
|
|
+ margin-bottom: 16px;
|
|
|
+ padding-bottom: 8px;
|
|
|
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 无数据提示样式 */
|
|
|
+.no_data {
|
|
|
+ color: white;
|
|
|
+ margin-top: 20px;
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background-color: rgba(255, 255, 255, 0.1);
|
|
|
+ width: 100%;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+/* 状态提示样式 */
|
|
|
+.loading,
|
|
|
+.error {
|
|
|
+ color: white;
|
|
|
+ margin-top: 20px;
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background-color: rgba(255, 255, 255, 0.1);
|
|
|
+ width: 100%;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.error {
|
|
|
+ color: #ff4444;
|
|
|
+
|
|
|
+ button {
|
|
|
+ margin-top: 10px;
|
|
|
+ padding: 8px 16px;
|
|
|
+ background-color: #00C851;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background-color 0.2s;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: #007E33;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式布局 */
|
|
|
+@media (max-width: 1400px) {
|
|
|
+ .equipment_grid {
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 20px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 1200px) {
|
|
|
+ .main-container {
|
|
|
+ padding: 140px 20px 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .main-content {
|
|
|
+ max-width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .equipment_grid {
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 18px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 992px) {
|
|
|
+ .main-content {
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .equipment_grid {
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ gap: 16px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .main-container {
|
|
|
+ padding: 120px 16px 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .equipment_grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ gap: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tank_section {
|
|
|
+ padding: 16px 18px;
|
|
|
+ gap: 16px;
|
|
|
+ scroll-margin-top: 140px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .equipment_item {
|
|
|
+ min-height: 180px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|