Bläddra i källkod

refactor(controlPage):碳酸钠流程控制页面调整

HMY 10 månader sedan
förälder
incheckning
ce4f41f5ce

+ 331 - 146
ui/src/views/controlPage/flowSelect/DissolvedCrystal/index.vue

@@ -1,277 +1,462 @@
 <template>
     <div class="dcs_tanks">
-        <HeaderComponent :title="title" backTo="/controlPage/flowSelect" />
-        <PageNav :items="navItems_Na2SO4" :currentCode="flowCode" />
-        <!-- 加载状态提示 -->
-        <div v-if="isLoading" class="loading">
-            正在加载设备数据...
-        </div>
-        <!-- 错误状态提示 -->
-        <div v-if="hasError" class="error">
-            <p>加载数据失败: {{ errorMessage }}</p>
-            <button @click="loadInitialData">重试</button>
+        <!-- 固定在顶部的头部区域 -->
+        <div class="fixed-header" ref="fixedHeader">
+            <HeaderComponent title="硫酸钠反应流程" backTo="/controlPage/flowSelect" />
+            <PageNav :items="navItems_Na2SO4" :currentCode="flowCode" />
         </div>
 
-        <div v-else class="tanks_container">
-            <!-- 每个罐体占一行 -->
-            <div v-for="(equipments, tankName) in deviceConfigGroup" :key="tankName" class="tank_row">
-                <h2 class="tank_title">{{ tankName }}</h2>
-                <div class="equipment_list">
-                    <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)" />
+        <!-- 主内容区(包含导航),添加顶部间距避免被固定头部遮挡 -->
+        <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">
+                                <!-- 添加其他设备组件 -->
+                                <PumpControlComponent title="原矿进料" :dataArr="[5, 100, 100]"
+                                    v-if="tankName === '1#反应釜-C3001'" id="equipment-special-pump"
+                                    class="equipment_item" />
+                                <WorkModeControl v-if="tankName === '兑卤器低位槽-V3001'" class="equipment_item" />
+                                <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)"
+                                    :id="`equipment-${equipment.code}`" class="equipment_item" />
+                            </div>
+                        </div>
+
+                        <!-- 无数据提示 -->
+                        <div v-if="isEmptyData" class="no_data">
+                            未获取到设备数据
+                        </div>
+                    </div>
                 </div>
-            </div>
 
-            <!-- 无数据提示 -->
-            <div v-if="isEmptyData" class="no-data">
-                未获取到设备数据
+                <!-- 右侧导航栏 -->
+                <TankNavigation :tanks="allTanks" :current-index="currentNavIndex" :header-height="headerHeight"
+                    @tankClick="scrollToTank" @scrollPositionChange="handleScrollPositionChange" />
             </div>
         </div>
     </div>
 </template>
 
 <script setup>
-import HeaderComponent from '@/components/DCS/HeaderComponent.vue'
-import PageNav from '@/components/GeneralComponents/control/PageNavComponent.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 { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment'
-import { useEquipmentLayout } from '@/hooks/useEquipmentLayout'
-import { navItems_Na2SO4 } from '@/config'
-import { stompClient } from '@/utils/ws/stompClient'
-import { updateZTPageConfig } from '@/api/dcs/configurePage'
-
-const route = useRoute()
+import { ref, onMounted, onBeforeUnmount, watch, watchEffect } from 'vue';
+import { useRoute } from 'vue-router';
+// 导入组件
+import HeaderComponent from '@/components/DCS/HeaderComponent.vue';
+import PageNav from '@/components/GeneralComponents/control/PageNavComponent.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 WorkModeControl from '@/components/GeneralComponents/control/WorkModeControl2Component.vue';
+import TankNavigation from '@/components/GeneralComponents/control/TankNavigationComponent.vue';
+
+// 导入其他依赖
+import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment';
+import { useEquipmentLayout } from '@/hooks/useEquipmentLayout';
+import { navItems_Na2SO4 } from '@/config';
+import { stompClient } from '@/utils/ws/stompClient';
+import { updateZTPageConfig } from '@/api/dcs/configurePage';
+
+const route = useRoute();
 
 // 状态变量
-const isLoading = ref(true)// 加载状态
-const hasError = ref(false)// 错误状态
-const errorMessage = ref('')// 错误信息
-const isEmptyData = ref(false)// 是否无数据
+const isLoading = ref(true);
+const hasError = ref(false);
+const errorMessage = ref('');
+const isEmptyData = ref(false);
+const fixedHeader = ref(null);
+const headerHeight = ref(0);
+
+const flowCode = ref(route.name);
+const title = navItems_Na2SO4.find(i => i.code === flowCode.value)?.label || '';
+
+// 导航相关变量
+const allTanks = ref([]);  // 所有罐体列表
+const currentNavIndex = ref(-1);  // 当前激活的导航项索引
 
-const flowCode = route.name
+// 设备配置和数据
+const deviceConfigGroup = ref({});// 设备配置分组
+const deviceDataGroup = ref({});// 设备数据分组
+const pageParams = ref({});
 
-const title = navItems_Na2SO4.find(i => i.code === flowCode)?.label || ''
+const { generatePageParams } = useEquipmentLayout();
 
 // 监控flowCode变化
 watch(() => route.name, (newVal) => {
-    console.log('路由变化:', newVal)
-    flowCode = newVal
-    loadInitialData()
-})
-const deviceConfigGroup = ref({})// 设备配置分组 - 用于渲染设备结构
-const deviceDataGroup = ref({})// 设备数据分组 - 用于提供设备数据
-const pageParams = ref({})
-
-const { generatePageParams } = useEquipmentLayout()
+    console.log('路由变化:', newVal);
+    flowCode.value = newVal;
+    loadInitialData();
+});
+
+// 生成罐体导航列表(直接取deviceConfigGroup的tankName)
+watchEffect(() => {
+    // 收集所有罐体名称
+    const tanks = [];
+
+    // 直接使用deviceConfigGroup的键作为罐体名称
+    Object.keys(deviceConfigGroup.value).forEach(tankName => {
+        tanks.push({
+            uniqueId: `tank-${tankName}`,
+            tankName,
+            shortName: shortenTankName(tankName),
+        });
+    });
+
+    allTanks.value = tanks;
+    currentNavIndex.value = -1;
+});
+
+// 计算固定头部高度
+const calculateHeaderHeight = () => {
+    if (fixedHeader.value) {
+        headerHeight.value = fixedHeader.value.offsetHeight;
+    }
+};
 
 async function loadInitialData() {
     try {
-        isLoading.value = true
-        hasError.value = false
-        errorMessage.value = ''
+        isLoading.value = true;
+        hasError.value = false;
+        errorMessage.value = '';
 
-        const res = await getPageEquipmentGroupByTankByFlowCode(flowCode)
+        const res = await getPageEquipmentGroupByTankByFlowCode(flowCode.value);
 
         deviceConfigGroup.value = Object.fromEntries(
             Object.entries(res.data || {}).filter(([_, equipments]) => equipments?.length > 0)
-        )
-        // console.log('设备配置分组:', deviceConfigGroup.value)
-        isEmptyData.value = Object.keys(deviceConfigGroup.value).length === 0
+        );
+        isEmptyData.value = Object.keys(deviceConfigGroup.value).length === 0;
 
-        pageParams.value = generatePageParams(deviceConfigGroup.value)
-        await updatePageConfig()
+        pageParams.value = generatePageParams(deviceConfigGroup.value);
+        // console.log('页面参数:', pageParams.value);
+        await updatePageConfig();
     } catch (err) {
-        hasError.value = true
-        errorMessage.value = err.message || '加载设备分组失败'
-        console.error('加载页面设备分组失败', err)
+        hasError.value = true;
+        errorMessage.value = err.message || '加载设备分组失败';
+        console.error('加载页面设备分组失败', err);
     } finally {
-        isLoading.value = false
+        isLoading.value = false;
     }
 }
 
 async function updatePageConfig() {
     try {
-        // console.log('更新页面配置:', flowCode, pageParams.value)
-        await updateZTPageConfig(flowCode, pageParams.value)
+        await updateZTPageConfig(flowCode.value, pageParams.value);
 
         // 先取消可能存在的订阅,避免重复订阅
-        stompClient.unsubscribeFromPage(flowCode)
+        stompClient.unsubscribeFromPage(flowCode.value);
 
-        stompClient.subscribeToPage(flowCode, data => {
-            // console.log('收到页面实时数据推送:', data)
+        stompClient.subscribeToPage(flowCode.value, data => {
             if (data && Object.keys(data).length > 0) {
                 try {
                     // 按key逐个更新,避免整体替换导致页面闪烁
                     for (const key in data) {
-                        deviceDataGroup.value[key] = data[key] || []
+                        deviceDataGroup.value[key] = data[key] || [];
                     }
                 } catch (updateErr) {
-                    console.error('更新设备数据失败:', updateErr)
+                    console.error('更新设备数据失败:', updateErr);
                 }
             }
-        })
+        });
     } catch (err) {
-        console.error('页面配置失败:', err)
+        console.error('页面配置失败:', err);
         // 配置失败也可以继续显示已有数据
     }
 }
+
 /**
  * 根据设备类型编码判断设备类型并返回对应组件
- * @param {number|string} equipmentType - 设备类型编码(1、5为阀门,2为泵,4为传感器)
- * @returns {Component} 对应的设备组件
  */
 function getComponentByType(equipmentType) {
-    // 处理空值或无效类型情况
     if (equipmentType === undefined || equipmentType === null) {
-        console.error('设备类型编码不能为空')
-        return 'div'
+        console.error('设备类型编码不能为空');
+        return 'div';
     }
 
-    // 将输入转换为数字类型(兼容字符串形式的数字,如"1")
-    const type = Number(equipmentType)
+    const type = Number(equipmentType);
 
-    // 校验是否为有效数字
     if (isNaN(type)) {
-        console.error('无效的设备类型编码:', equipmentType)
-        return 'div'
+        console.error('无效的设备类型编码:', equipmentType);
+        return 'div';
     }
 
-    // 根据类型编码返回对应组件
     if (type === 1 || type === 5) {
-        // console.log(`设备类型编码${type}判定为阀门`)
-        return ValveControlComponent
+        return ValveControlComponent;
     } else if (type === 2) {
-        // console.log(`设备类型编码${type}判定为泵`)
-        return PumpControlComponent
+        return PumpControlComponent;
     } else if (type === 4) {
-        // console.log(`设备类型编码${type}判定为传感器`)
-        return SensorControl
+        return SensorControl;
     } else {
-        // 未知类型
-        console.warn(`设备类型编码${type}无法识别,使用默认div`)
-        return 'div'
+        console.warn(`设备类型编码${type}无法识别,使用默认div`);
+        return 'div';
     }
 }
 
 function getValueByCode(tankName, code) {
-    const dataArr = deviceDataGroup.value[tankName] || []
-    const target = dataArr.find(item => item.code === code)
-    return target ? target.value : []
+    const dataArr = deviceDataGroup.value[tankName] || [];
+    const target = dataArr.find(item => item.code === code);
+    return target ? target.value : [];
+}
+
+// 导航功能:滚动到指定罐体
+function scrollToTank(tank, index) {
+    currentNavIndex.value = index;
+    const elementId = `tank-${tank.tankName}`;
+
+    const element = document.getElementById(elementId);
+    if (element) {
+        // 计算滚动位置时考虑固定头部的高度
+        window.scrollTo({
+            top: element.offsetTop - headerHeight.value - 20,  // 顶部留一些间距
+            behavior: 'smooth'
+        });
+    }
+}
+
+// 处理滚动位置变化事件
+function handleScrollPositionChange(index) {
+    currentNavIndex.value = index;
+}
+
+// 缩短罐体名称用于导航显示
+function shortenTankName(fullName) {
+    if (fullName.length > 18) {
+        return fullName.substring(0, 16) + '...';
+    }
+    return fullName;
 }
 
 onMounted(() => {
-    console.log('组件挂载')
-    loadInitialData()
-})
+    console.log('组件挂载');
+    // 计算头部高度
+    calculateHeaderHeight();
+    // 监听窗口大小变化,重新计算头部高度
+    window.addEventListener('resize', calculateHeaderHeight);
+    loadInitialData();
+});
 
 onBeforeUnmount(() => {
-    console.log('组件卸载,取消订阅')
-    stompClient.unsubscribeFromPage(flowCode)
-})
+    console.log('组件卸载,取消订阅');
+    stompClient.unsubscribeFromPage(flowCode.value);
+    window.removeEventListener('resize', calculateHeaderHeight);
+});
 </script>
 
 <style scoped lang="scss">
 .dcs_tanks {
     width: 100%;
     min-height: 100vh;
-    padding: 24px 32px;
     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;
-    flex-direction: column;
-    align-items: center;
+    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%;
-    max-width: 1400px;
-    margin-top: 20px;
 }
 
-// 每个罐体占一行
-.tank_row {
+// 每个罐体区域
+.tank_section {
     background: #1a1a1a;
     border-radius: 12px;
-    padding: 16px 20px;
+    padding: 20px 24px;
     width: 100%;
     display: flex;
     flex-direction: column;
-    gap: 16px;
+    gap: 20px;
+    box-sizing: border-box;
+    scroll-margin-top: 160px;
+    /* 用于滚动定位时预留空间 */
 }
 
 .tank_title {
     font-size: 20px;
     color: #fff;
-    padding-bottom: 8px;
+    padding-bottom: 12px;
     border-bottom: 1px solid rgba(255, 255, 255, 0.1);
     text-align: center;
+    margin: 0;
 }
 
-// 设备列表改为横向排列,可滚动
-.equipment_list {
-    display: flex;
-    gap: 16px;
-    overflow-x: auto;
-    padding: 8px 0;
-    min-height: 120px;
-    /* 确保有足够高度显示设备 */
+// 设备网格布局
+.equipment_grid {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    /* 默认4列布局 */
+    gap: 24px;
+    /* 增大间距避免拥挤 */
+    padding: 12px 0;
 }
 
-/* 滚动条美化 */
-.equipment_list::-webkit-scrollbar {
-    height: 6px;
+// 统一设备项样式
+.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;
 }
 
-.equipment_list::-webkit-scrollbar-track {
-    background: rgba(255, 255, 255, 0.05);
-    border-radius: 3px;
+/* 确保子组件正确填充容器 */
+: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;
+    }
 }
 
-.equipment_list::-webkit-scrollbar-thumb {
-    background: rgba(255, 255, 255, 0.2);
-    border-radius: 3px;
+/* 无数据提示样式 */
+.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,
-.no-data {
+.error {
     color: white;
     margin-top: 20px;
     padding: 20px;
     border-radius: 8px;
     background-color: rgba(255, 255, 255, 0.1);
     width: 100%;
-    max-width: 1400px;
     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;
+        }
+    }
 }
 
-.error button {
-    margin-top: 10px;
-    padding: 8px 16px;
-    background-color: #00C851;
-    color: white;
-    border: none;
-    border-radius: 4px;
-    cursor: pointer;
-    transition: background-color 0.2s;
+/* 响应式布局 */
+@media (max-width: 1400px) {
+    .equipment_grid {
+        grid-template-columns: repeat(3, 1fr);
+        gap: 20px;
+    }
 }
 
-.error button:hover {
-    background-color: #007E33;
+@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>

+ 331 - 148
ui/src/views/controlPage/flowSelect/G1AccessorySeparator/index.vue

@@ -1,279 +1,462 @@
 <template>
     <div class="dcs_tanks">
-        <HeaderComponent :title="title" backTo="/controlPage/flowSelect" />
-        <PageNav :items="navItems_Na2SO4" :currentCode="flowCode" />
-        <!-- 加载状态提示 -->
-        <div v-if="isLoading" class="loading">
-            正在加载设备数据...
-        </div>
-        <!-- 错误状态提示 -->
-        <div v-if="hasError" class="error">
-            <p>加载数据失败: {{ errorMessage }}</p>
-            <button @click="loadInitialData">重试</button>
+        <!-- 固定在顶部的头部区域 -->
+        <div class="fixed-header" ref="fixedHeader">
+            <HeaderComponent title="硫酸钠反应流程" backTo="/controlPage/flowSelect" />
+            <PageNav :items="navItems_Na2SO4" :currentCode="flowCode" />
         </div>
 
-        <div v-else class="tanks_container">
-            <!-- 每个罐体占一行 -->
-            <div v-for="(equipments, tankName) in deviceConfigGroup" :key="tankName" class="tank_row">
-                <h2 class="tank_title">{{ tankName }}</h2>
-                <div class="equipment_list">
-                    <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)" />
+        <!-- 主内容区(包含导航),添加顶部间距避免被固定头部遮挡 -->
+        <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">
+                                <!-- 添加其他设备组件 -->
+                                <PumpControlComponent title="原矿进料" :dataArr="[5, 100, 100]"
+                                    v-if="tankName === '1#反应釜-C3001'" id="equipment-special-pump"
+                                    class="equipment_item" />
+                                <WorkModeControl v-if="tankName === '兑卤器低位槽-V3001'" class="equipment_item" />
+                                <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)"
+                                    :id="`equipment-${equipment.code}`" class="equipment_item" />
+                            </div>
+                        </div>
+
+                        <!-- 无数据提示 -->
+                        <div v-if="isEmptyData" class="no_data">
+                            未获取到设备数据
+                        </div>
+                    </div>
                 </div>
-            </div>
 
-            <!-- 无数据提示 -->
-            <div v-if="isEmptyData" class="no-data">
-                未获取到设备数据
+                <!-- 右侧导航栏 -->
+                <TankNavigation :tanks="allTanks" :current-index="currentNavIndex" :header-height="headerHeight"
+                    @tankClick="scrollToTank" @scrollPositionChange="handleScrollPositionChange" />
             </div>
         </div>
     </div>
 </template>
 
 <script setup>
-import HeaderComponent from '@/components/DCS/HeaderComponent.vue'
-import PageNav from '@/components/GeneralComponents/control/PageNavComponent.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 { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment'
-import { useEquipmentLayout } from '@/hooks/useEquipmentLayout'
-import { navItems_Na2SO4 } from '@/config'
-import { stompClient } from '@/utils/ws/stompClient'
-import { updateZTPageConfig } from '@/api/dcs/configurePage'
-
-const route = useRoute()
+import { ref, onMounted, onBeforeUnmount, watch, watchEffect } from 'vue';
+import { useRoute } from 'vue-router';
+// 导入组件
+import HeaderComponent from '@/components/DCS/HeaderComponent.vue';
+import PageNav from '@/components/GeneralComponents/control/PageNavComponent.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 WorkModeControl from '@/components/GeneralComponents/control/WorkModeControl2Component.vue';
+import TankNavigation from '@/components/GeneralComponents/control/TankNavigationComponent.vue';
+
+// 导入其他依赖
+import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment';
+import { useEquipmentLayout } from '@/hooks/useEquipmentLayout';
+import { navItems_Na2SO4 } from '@/config';
+import { stompClient } from '@/utils/ws/stompClient';
+import { updateZTPageConfig } from '@/api/dcs/configurePage';
+
+const route = useRoute();
 
 // 状态变量
-const isLoading = ref(true)// 加载状态
-const hasError = ref(false)// 错误状态
-const errorMessage = ref('')// 错误信息
-const isEmptyData = ref(false)// 是否无数据
+const isLoading = ref(true);
+const hasError = ref(false);
+const errorMessage = ref('');
+const isEmptyData = ref(false);
+const fixedHeader = ref(null);
+const headerHeight = ref(0);
+
+const flowCode = ref(route.name);
+const title = navItems_Na2SO4.find(i => i.code === flowCode.value)?.label || '';
+
+// 导航相关变量
+const allTanks = ref([]);  // 所有罐体列表
+const currentNavIndex = ref(-1);  // 当前激活的导航项索引
 
-const flowCode = route.name
+// 设备配置和数据
+const deviceConfigGroup = ref({});// 设备配置分组
+const deviceDataGroup = ref({});// 设备数据分组
+const pageParams = ref({});
 
-const title = navItems_Na2SO4.find(i => i.code === flowCode)?.label || ''
+const { generatePageParams } = useEquipmentLayout();
 
 // 监控flowCode变化
 watch(() => route.name, (newVal) => {
-    console.log('路由变化:', newVal)
-    flowCode = newVal
-    loadInitialData()
-})
-const deviceConfigGroup = ref({})// 设备配置分组 - 用于渲染设备结构
-const deviceDataGroup = ref({})// 设备数据分组 - 用于提供设备数据
-const pageParams = ref({})
-
-const { generatePageParams } = useEquipmentLayout()
+    console.log('路由变化:', newVal);
+    flowCode.value = newVal;
+    loadInitialData();
+});
+
+// 生成罐体导航列表(直接取deviceConfigGroup的tankName)
+watchEffect(() => {
+    // 收集所有罐体名称
+    const tanks = [];
+
+    // 直接使用deviceConfigGroup的键作为罐体名称
+    Object.keys(deviceConfigGroup.value).forEach(tankName => {
+        tanks.push({
+            uniqueId: `tank-${tankName}`,
+            tankName,
+            shortName: shortenTankName(tankName),
+        });
+    });
+
+    allTanks.value = tanks;
+    currentNavIndex.value = -1;
+});
+
+// 计算固定头部高度
+const calculateHeaderHeight = () => {
+    if (fixedHeader.value) {
+        headerHeight.value = fixedHeader.value.offsetHeight;
+    }
+};
 
 async function loadInitialData() {
     try {
-        isLoading.value = true
-        hasError.value = false
-        errorMessage.value = ''
+        isLoading.value = true;
+        hasError.value = false;
+        errorMessage.value = '';
 
-        const res = await getPageEquipmentGroupByTankByFlowCode(flowCode)
+        const res = await getPageEquipmentGroupByTankByFlowCode(flowCode.value);
 
         deviceConfigGroup.value = Object.fromEntries(
             Object.entries(res.data || {}).filter(([_, equipments]) => equipments?.length > 0)
-        )
-        // console.log('设备配置分组:', deviceConfigGroup.value)
-        isEmptyData.value = Object.keys(deviceConfigGroup.value).length === 0
+        );
+        isEmptyData.value = Object.keys(deviceConfigGroup.value).length === 0;
 
-        pageParams.value = generatePageParams(deviceConfigGroup.value)
-        await updatePageConfig()
+        pageParams.value = generatePageParams(deviceConfigGroup.value);
+        // console.log('页面参数:', pageParams.value);
+        await updatePageConfig();
     } catch (err) {
-        hasError.value = true
-        errorMessage.value = err.message || '加载设备分组失败'
-        console.error('加载页面设备分组失败', err)
+        hasError.value = true;
+        errorMessage.value = err.message || '加载设备分组失败';
+        console.error('加载页面设备分组失败', err);
     } finally {
-        isLoading.value = false
+        isLoading.value = false;
     }
 }
 
 async function updatePageConfig() {
     try {
-        // console.log('更新页面配置:', flowCode, pageParams.value)
-        await updateZTPageConfig(flowCode, pageParams.value)
+        await updateZTPageConfig(flowCode.value, pageParams.value);
 
         // 先取消可能存在的订阅,避免重复订阅
-        stompClient.unsubscribeFromPage(flowCode)
+        stompClient.unsubscribeFromPage(flowCode.value);
 
-        stompClient.subscribeToPage(flowCode, data => {
-            // console.log('收到页面实时数据推送:', data)
+        stompClient.subscribeToPage(flowCode.value, data => {
             if (data && Object.keys(data).length > 0) {
                 try {
                     // 按key逐个更新,避免整体替换导致页面闪烁
                     for (const key in data) {
-                        deviceDataGroup.value[key] = data[key] || []
+                        deviceDataGroup.value[key] = data[key] || [];
                     }
                 } catch (updateErr) {
-                    console.error('更新设备数据失败:', updateErr)
+                    console.error('更新设备数据失败:', updateErr);
                 }
             }
-        })
+        });
     } catch (err) {
-        console.error('页面配置失败:', err)
+        console.error('页面配置失败:', err);
         // 配置失败也可以继续显示已有数据
     }
 }
+
 /**
  * 根据设备类型编码判断设备类型并返回对应组件
- * @param {number|string} equipmentType - 设备类型编码(1、5为阀门,2为泵,4为传感器)
- * @returns {Component} 对应的设备组件
  */
 function getComponentByType(equipmentType) {
-    // 处理空值或无效类型情况
     if (equipmentType === undefined || equipmentType === null) {
-        console.error('设备类型编码不能为空')
-        return 'div'
+        console.error('设备类型编码不能为空');
+        return 'div';
     }
 
-    // 将输入转换为数字类型(兼容字符串形式的数字,如"1")
-    const type = Number(equipmentType)
+    const type = Number(equipmentType);
 
-    // 校验是否为有效数字
     if (isNaN(type)) {
-        console.error('无效的设备类型编码:', equipmentType)
-        return 'div'
+        console.error('无效的设备类型编码:', equipmentType);
+        return 'div';
     }
 
-    // 根据类型编码返回对应组件
     if (type === 1 || type === 5) {
-        // console.log(`设备类型编码${type}判定为阀门`)
-        return ValveControlComponent
+        return ValveControlComponent;
     } else if (type === 2) {
-        // console.log(`设备类型编码${type}判定为泵`)
-        return PumpControlComponent
+        return PumpControlComponent;
     } else if (type === 4) {
-        // console.log(`设备类型编码${type}判定为传感器`)
-        return SensorControl
+        return SensorControl;
     } else {
-        // 未知类型
-        console.warn(`设备类型编码${type}无法识别,使用默认div`)
-        return 'div'
+        console.warn(`设备类型编码${type}无法识别,使用默认div`);
+        return 'div';
     }
 }
 
 function getValueByCode(tankName, code) {
-    const dataArr = deviceDataGroup.value[tankName] || []
-    const target = dataArr.find(item => item.code === code)
-    return target ? target.value : []
+    const dataArr = deviceDataGroup.value[tankName] || [];
+    const target = dataArr.find(item => item.code === code);
+    return target ? target.value : [];
+}
+
+// 导航功能:滚动到指定罐体
+function scrollToTank(tank, index) {
+    currentNavIndex.value = index;
+    const elementId = `tank-${tank.tankName}`;
+
+    const element = document.getElementById(elementId);
+    if (element) {
+        // 计算滚动位置时考虑固定头部的高度
+        window.scrollTo({
+            top: element.offsetTop - headerHeight.value - 20,  // 顶部留一些间距
+            behavior: 'smooth'
+        });
+    }
+}
+
+// 处理滚动位置变化事件
+function handleScrollPositionChange(index) {
+    currentNavIndex.value = index;
+}
+
+// 缩短罐体名称用于导航显示
+function shortenTankName(fullName) {
+    if (fullName.length > 18) {
+        return fullName.substring(0, 16) + '...';
+    }
+    return fullName;
 }
 
 onMounted(() => {
-    console.log('组件挂载')
-    loadInitialData()
-})
+    console.log('组件挂载');
+    // 计算头部高度
+    calculateHeaderHeight();
+    // 监听窗口大小变化,重新计算头部高度
+    window.addEventListener('resize', calculateHeaderHeight);
+    loadInitialData();
+});
 
 onBeforeUnmount(() => {
-    console.log('组件卸载,取消订阅')
-    stompClient.unsubscribeFromPage(flowCode)
-})
+    console.log('组件卸载,取消订阅');
+    stompClient.unsubscribeFromPage(flowCode.value);
+    window.removeEventListener('resize', calculateHeaderHeight);
+});
 </script>
 
 <style scoped lang="scss">
 .dcs_tanks {
     width: 100%;
     min-height: 100vh;
-    padding: 24px 32px;
     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;
-    flex-direction: column;
-    align-items: center;
+    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%;
-    max-width: 1400px;
-    margin-top: 20px;
 }
 
-// 每个罐体占一行
-.tank_row {
+// 每个罐体区域
+.tank_section {
     background: #1a1a1a;
     border-radius: 12px;
-    padding: 16px 20px;
+    padding: 20px 24px;
     width: 100%;
     display: flex;
     flex-direction: column;
-    gap: 16px;
+    gap: 20px;
+    box-sizing: border-box;
+    scroll-margin-top: 160px;
+    /* 用于滚动定位时预留空间 */
 }
 
 .tank_title {
     font-size: 20px;
     color: #fff;
-    padding-bottom: 8px;
+    padding-bottom: 12px;
     border-bottom: 1px solid rgba(255, 255, 255, 0.1);
     text-align: center;
+    margin: 0;
 }
 
-// 设备列表改为横向排列,可滚动
-.equipment_list {
-    display: flex;
-    gap: 16px;
-    overflow-x: auto;
-    padding: 8px 0;
-    min-height: 120px;
-    align-items: center;
-
-    /* 确保有足够高度显示设备 */
+// 设备网格布局
+.equipment_grid {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    /* 默认4列布局 */
+    gap: 24px;
+    /* 增大间距避免拥挤 */
+    padding: 12px 0;
 }
 
-/* 滚动条美化 */
-.equipment_list::-webkit-scrollbar {
-    height: 6px;
+// 统一设备项样式
+.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;
 }
 
-.equipment_list::-webkit-scrollbar-track {
-    background: rgba(255, 255, 255, 0.05);
-    border-radius: 3px;
+/* 确保子组件正确填充容器 */
+: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;
+    }
 }
 
-.equipment_list::-webkit-scrollbar-thumb {
-    background: rgba(255, 255, 255, 0.2);
-    border-radius: 3px;
+/* 无数据提示样式 */
+.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,
-.no-data {
+.error {
     color: white;
     margin-top: 20px;
     padding: 20px;
     border-radius: 8px;
     background-color: rgba(255, 255, 255, 0.1);
     width: 100%;
-    max-width: 1400px;
     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;
+        }
+    }
 }
 
-.error button {
-    margin-top: 10px;
-    padding: 8px 16px;
-    background-color: #00C851;
-    color: white;
-    border: none;
-    border-radius: 4px;
-    cursor: pointer;
-    transition: background-color 0.2s;
+/* 响应式布局 */
+@media (max-width: 1400px) {
+    .equipment_grid {
+        grid-template-columns: repeat(3, 1fr);
+        gap: 20px;
+    }
 }
 
-.error button:hover {
-    background-color: #007E33;
+@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>

+ 331 - 148
ui/src/views/controlPage/flowSelect/G1Decomposition/index.vue

@@ -1,279 +1,462 @@
 <template>
     <div class="dcs_tanks">
-        <HeaderComponent :title="title" backTo="/controlPage/flowSelect" />
-        <PageNav :items="navItems_Na2SO4" :currentCode="flowCode" />
-        <!-- 加载状态提示 -->
-        <div v-if="isLoading" class="loading">
-            正在加载设备数据...
-        </div>
-        <!-- 错误状态提示 -->
-        <div v-if="hasError" class="error">
-            <p>加载数据失败: {{ errorMessage }}</p>
-            <button @click="loadInitialData">重试</button>
+        <!-- 固定在顶部的头部区域 -->
+        <div class="fixed-header" ref="fixedHeader">
+            <HeaderComponent title="硫酸钠反应流程" backTo="/controlPage/flowSelect" />
+            <PageNav :items="navItems_Na2SO4" :currentCode="flowCode" />
         </div>
 
-        <div v-else class="tanks_container">
-            <!-- 每个罐体占一行 -->
-            <div v-for="(equipments, tankName) in deviceConfigGroup" :key="tankName" class="tank_row">
-                <h2 class="tank_title">{{ tankName }}</h2>
-                <div class="equipment_list">
-                    <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)" />
+        <!-- 主内容区(包含导航),添加顶部间距避免被固定头部遮挡 -->
+        <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">
+                                <!-- 添加其他设备组件 -->
+                                <PumpControlComponent title="原矿进料" :dataArr="[5, 100, 100]"
+                                    v-if="tankName === '1#反应釜-C3001'" id="equipment-special-pump"
+                                    class="equipment_item" />
+                                <WorkModeControl v-if="tankName === '兑卤器低位槽-V3001'" class="equipment_item" />
+                                <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)"
+                                    :id="`equipment-${equipment.code}`" class="equipment_item" />
+                            </div>
+                        </div>
+
+                        <!-- 无数据提示 -->
+                        <div v-if="isEmptyData" class="no_data">
+                            未获取到设备数据
+                        </div>
+                    </div>
                 </div>
-            </div>
 
-            <!-- 无数据提示 -->
-            <div v-if="isEmptyData" class="no-data">
-                未获取到设备数据
+                <!-- 右侧导航栏 -->
+                <TankNavigation :tanks="allTanks" :current-index="currentNavIndex" :header-height="headerHeight"
+                    @tankClick="scrollToTank" @scrollPositionChange="handleScrollPositionChange" />
             </div>
         </div>
     </div>
 </template>
 
 <script setup>
-import HeaderComponent from '@/components/DCS/HeaderComponent.vue'
-import PageNav from '@/components/GeneralComponents/control/PageNavComponent.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 { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment'
-import { useEquipmentLayout } from '@/hooks/useEquipmentLayout'
-import { navItems_Na2SO4 } from '@/config'
-import { stompClient } from '@/utils/ws/stompClient'
-import { updateZTPageConfig } from '@/api/dcs/configurePage'
-
-const route = useRoute()
+import { ref, onMounted, onBeforeUnmount, watch, watchEffect } from 'vue';
+import { useRoute } from 'vue-router';
+// 导入组件
+import HeaderComponent from '@/components/DCS/HeaderComponent.vue';
+import PageNav from '@/components/GeneralComponents/control/PageNavComponent.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 WorkModeControl from '@/components/GeneralComponents/control/WorkModeControl2Component.vue';
+import TankNavigation from '@/components/GeneralComponents/control/TankNavigationComponent.vue';
+
+// 导入其他依赖
+import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment';
+import { useEquipmentLayout } from '@/hooks/useEquipmentLayout';
+import { navItems_Na2SO4 } from '@/config';
+import { stompClient } from '@/utils/ws/stompClient';
+import { updateZTPageConfig } from '@/api/dcs/configurePage';
+
+const route = useRoute();
 
 // 状态变量
-const isLoading = ref(true)// 加载状态
-const hasError = ref(false)// 错误状态
-const errorMessage = ref('')// 错误信息
-const isEmptyData = ref(false)// 是否无数据
+const isLoading = ref(true);
+const hasError = ref(false);
+const errorMessage = ref('');
+const isEmptyData = ref(false);
+const fixedHeader = ref(null);
+const headerHeight = ref(0);
+
+const flowCode = ref(route.name);
+const title = navItems_Na2SO4.find(i => i.code === flowCode.value)?.label || '';
+
+// 导航相关变量
+const allTanks = ref([]);  // 所有罐体列表
+const currentNavIndex = ref(-1);  // 当前激活的导航项索引
 
-const flowCode = route.name
+// 设备配置和数据
+const deviceConfigGroup = ref({});// 设备配置分组
+const deviceDataGroup = ref({});// 设备数据分组
+const pageParams = ref({});
 
-const title = navItems_Na2SO4.find(i => i.code === flowCode)?.label || ''
+const { generatePageParams } = useEquipmentLayout();
 
 // 监控flowCode变化
 watch(() => route.name, (newVal) => {
-    console.log('路由变化:', newVal)
-    flowCode = newVal
-    loadInitialData()
-})
-const deviceConfigGroup = ref({})// 设备配置分组 - 用于渲染设备结构
-const deviceDataGroup = ref({})// 设备数据分组 - 用于提供设备数据
-const pageParams = ref({})
-
-const { generatePageParams } = useEquipmentLayout()
+    console.log('路由变化:', newVal);
+    flowCode.value = newVal;
+    loadInitialData();
+});
+
+// 生成罐体导航列表(直接取deviceConfigGroup的tankName)
+watchEffect(() => {
+    // 收集所有罐体名称
+    const tanks = [];
+
+    // 直接使用deviceConfigGroup的键作为罐体名称
+    Object.keys(deviceConfigGroup.value).forEach(tankName => {
+        tanks.push({
+            uniqueId: `tank-${tankName}`,
+            tankName,
+            shortName: shortenTankName(tankName),
+        });
+    });
+
+    allTanks.value = tanks;
+    currentNavIndex.value = -1;
+});
+
+// 计算固定头部高度
+const calculateHeaderHeight = () => {
+    if (fixedHeader.value) {
+        headerHeight.value = fixedHeader.value.offsetHeight;
+    }
+};
 
 async function loadInitialData() {
     try {
-        isLoading.value = true
-        hasError.value = false
-        errorMessage.value = ''
+        isLoading.value = true;
+        hasError.value = false;
+        errorMessage.value = '';
 
-        const res = await getPageEquipmentGroupByTankByFlowCode(flowCode)
+        const res = await getPageEquipmentGroupByTankByFlowCode(flowCode.value);
 
         deviceConfigGroup.value = Object.fromEntries(
             Object.entries(res.data || {}).filter(([_, equipments]) => equipments?.length > 0)
-        )
-        // console.log('设备配置分组:', deviceConfigGroup.value)
-        isEmptyData.value = Object.keys(deviceConfigGroup.value).length === 0
+        );
+        isEmptyData.value = Object.keys(deviceConfigGroup.value).length === 0;
 
-        pageParams.value = generatePageParams(deviceConfigGroup.value)
-        await updatePageConfig()
+        pageParams.value = generatePageParams(deviceConfigGroup.value);
+        // console.log('页面参数:', pageParams.value);
+        await updatePageConfig();
     } catch (err) {
-        hasError.value = true
-        errorMessage.value = err.message || '加载设备分组失败'
-        console.error('加载页面设备分组失败', err)
+        hasError.value = true;
+        errorMessage.value = err.message || '加载设备分组失败';
+        console.error('加载页面设备分组失败', err);
     } finally {
-        isLoading.value = false
+        isLoading.value = false;
     }
 }
 
 async function updatePageConfig() {
     try {
-        // console.log('更新页面配置:', flowCode, pageParams.value)
-        await updateZTPageConfig(flowCode, pageParams.value)
+        await updateZTPageConfig(flowCode.value, pageParams.value);
 
         // 先取消可能存在的订阅,避免重复订阅
-        stompClient.unsubscribeFromPage(flowCode)
+        stompClient.unsubscribeFromPage(flowCode.value);
 
-        stompClient.subscribeToPage(flowCode, data => {
-            // console.log('收到页面实时数据推送:', data)
+        stompClient.subscribeToPage(flowCode.value, data => {
             if (data && Object.keys(data).length > 0) {
                 try {
                     // 按key逐个更新,避免整体替换导致页面闪烁
                     for (const key in data) {
-                        deviceDataGroup.value[key] = data[key] || []
+                        deviceDataGroup.value[key] = data[key] || [];
                     }
                 } catch (updateErr) {
-                    console.error('更新设备数据失败:', updateErr)
+                    console.error('更新设备数据失败:', updateErr);
                 }
             }
-        })
+        });
     } catch (err) {
-        console.error('页面配置失败:', err)
+        console.error('页面配置失败:', err);
         // 配置失败也可以继续显示已有数据
     }
 }
+
 /**
  * 根据设备类型编码判断设备类型并返回对应组件
- * @param {number|string} equipmentType - 设备类型编码(1、5为阀门,2为泵,4为传感器)
- * @returns {Component} 对应的设备组件
  */
 function getComponentByType(equipmentType) {
-    // 处理空值或无效类型情况
     if (equipmentType === undefined || equipmentType === null) {
-        console.error('设备类型编码不能为空')
-        return 'div'
+        console.error('设备类型编码不能为空');
+        return 'div';
     }
 
-    // 将输入转换为数字类型(兼容字符串形式的数字,如"1")
-    const type = Number(equipmentType)
+    const type = Number(equipmentType);
 
-    // 校验是否为有效数字
     if (isNaN(type)) {
-        console.error('无效的设备类型编码:', equipmentType)
-        return 'div'
+        console.error('无效的设备类型编码:', equipmentType);
+        return 'div';
     }
 
-    // 根据类型编码返回对应组件
     if (type === 1 || type === 5) {
-        // console.log(`设备类型编码${type}判定为阀门`)
-        return ValveControlComponent
+        return ValveControlComponent;
     } else if (type === 2) {
-        // console.log(`设备类型编码${type}判定为泵`)
-        return PumpControlComponent
+        return PumpControlComponent;
     } else if (type === 4) {
-        // console.log(`设备类型编码${type}判定为传感器`)
-        return SensorControl
+        return SensorControl;
     } else {
-        // 未知类型
-        console.warn(`设备类型编码${type}无法识别,使用默认div`)
-        return 'div'
+        console.warn(`设备类型编码${type}无法识别,使用默认div`);
+        return 'div';
     }
 }
 
 function getValueByCode(tankName, code) {
-    const dataArr = deviceDataGroup.value[tankName] || []
-    const target = dataArr.find(item => item.code === code)
-    return target ? target.value : []
+    const dataArr = deviceDataGroup.value[tankName] || [];
+    const target = dataArr.find(item => item.code === code);
+    return target ? target.value : [];
+}
+
+// 导航功能:滚动到指定罐体
+function scrollToTank(tank, index) {
+    currentNavIndex.value = index;
+    const elementId = `tank-${tank.tankName}`;
+
+    const element = document.getElementById(elementId);
+    if (element) {
+        // 计算滚动位置时考虑固定头部的高度
+        window.scrollTo({
+            top: element.offsetTop - headerHeight.value - 20,  // 顶部留一些间距
+            behavior: 'smooth'
+        });
+    }
+}
+
+// 处理滚动位置变化事件
+function handleScrollPositionChange(index) {
+    currentNavIndex.value = index;
+}
+
+// 缩短罐体名称用于导航显示
+function shortenTankName(fullName) {
+    if (fullName.length > 18) {
+        return fullName.substring(0, 16) + '...';
+    }
+    return fullName;
 }
 
 onMounted(() => {
-    console.log('组件挂载')
-    loadInitialData()
-})
+    console.log('组件挂载');
+    // 计算头部高度
+    calculateHeaderHeight();
+    // 监听窗口大小变化,重新计算头部高度
+    window.addEventListener('resize', calculateHeaderHeight);
+    loadInitialData();
+});
 
 onBeforeUnmount(() => {
-    console.log('组件卸载,取消订阅')
-    stompClient.unsubscribeFromPage(flowCode)
-})
+    console.log('组件卸载,取消订阅');
+    stompClient.unsubscribeFromPage(flowCode.value);
+    window.removeEventListener('resize', calculateHeaderHeight);
+});
 </script>
 
 <style scoped lang="scss">
 .dcs_tanks {
     width: 100%;
     min-height: 100vh;
-    padding: 24px 32px;
     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;
-    flex-direction: column;
-    align-items: center;
+    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%;
-    max-width: 1400px;
-    margin-top: 20px;
 }
 
-// 每个罐体占一行
-.tank_row {
+// 每个罐体区域
+.tank_section {
     background: #1a1a1a;
     border-radius: 12px;
-    padding: 16px 20px;
+    padding: 20px 24px;
     width: 100%;
     display: flex;
     flex-direction: column;
-    gap: 16px;
+    gap: 20px;
+    box-sizing: border-box;
+    scroll-margin-top: 160px;
+    /* 用于滚动定位时预留空间 */
 }
 
 .tank_title {
     font-size: 20px;
     color: #fff;
-    padding-bottom: 8px;
+    padding-bottom: 12px;
     border-bottom: 1px solid rgba(255, 255, 255, 0.1);
     text-align: center;
+    margin: 0;
 }
 
-// 设备列表改为横向排列,可滚动
-.equipment_list {
-    display: flex;
-    gap: 16px;
-    overflow-x: auto;
-    padding: 8px 0;
-    min-height: 120px;
-    align-items: center;
-
-    /* 确保有足够高度显示设备 */
+// 设备网格布局
+.equipment_grid {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    /* 默认4列布局 */
+    gap: 24px;
+    /* 增大间距避免拥挤 */
+    padding: 12px 0;
 }
 
-/* 滚动条美化 */
-.equipment_list::-webkit-scrollbar {
-    height: 6px;
+// 统一设备项样式
+.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;
 }
 
-.equipment_list::-webkit-scrollbar-track {
-    background: rgba(255, 255, 255, 0.05);
-    border-radius: 3px;
+/* 确保子组件正确填充容器 */
+: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;
+    }
 }
 
-.equipment_list::-webkit-scrollbar-thumb {
-    background: rgba(255, 255, 255, 0.2);
-    border-radius: 3px;
+/* 无数据提示样式 */
+.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,
-.no-data {
+.error {
     color: white;
     margin-top: 20px;
     padding: 20px;
     border-radius: 8px;
     background-color: rgba(255, 255, 255, 0.1);
     width: 100%;
-    max-width: 1400px;
     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;
+        }
+    }
 }
 
-.error button {
-    margin-top: 10px;
-    padding: 8px 16px;
-    background-color: #00C851;
-    color: white;
-    border: none;
-    border-radius: 4px;
-    cursor: pointer;
-    transition: background-color 0.2s;
+/* 响应式布局 */
+@media (max-width: 1400px) {
+    .equipment_grid {
+        grid-template-columns: repeat(3, 1fr);
+        gap: 20px;
+    }
 }
 
-.error button:hover {
-    background-color: #007E33;
+@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>

+ 331 - 147
ui/src/views/controlPage/flowSelect/G3Decomposition/index.vue

@@ -1,278 +1,462 @@
 <template>
     <div class="dcs_tanks">
-        <HeaderComponent :title="title" backTo="/controlPage/flowSelect" />
-        <PageNav :items="navItems_Na2SO4" :currentCode="flowCode" />
-        <!-- 加载状态提示 -->
-        <div v-if="isLoading" class="loading">
-            正在加载设备数据...
-        </div>
-        <!-- 错误状态提示 -->
-        <div v-if="hasError" class="error">
-            <p>加载数据失败: {{ errorMessage }}</p>
-            <button @click="loadInitialData">重试</button>
+        <!-- 固定在顶部的头部区域 -->
+        <div class="fixed-header" ref="fixedHeader">
+            <HeaderComponent title="硫酸钠反应流程" backTo="/controlPage/flowSelect" />
+            <PageNav :items="navItems_Na2SO4" :currentCode="flowCode" />
         </div>
 
-        <div v-else class="tanks_container">
-            <!-- 每个罐体占一行 -->
-            <div v-for="(equipments, tankName) in deviceConfigGroup" :key="tankName" class="tank_row">
-                <h2 class="tank_title">{{ tankName }}</h2>
-                <div class="equipment_list">
-                    <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)" />
+        <!-- 主内容区(包含导航),添加顶部间距避免被固定头部遮挡 -->
+        <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">
+                                <!-- 添加其他设备组件 -->
+                                <PumpControlComponent title="原矿进料" :dataArr="[5, 100, 100]"
+                                    v-if="tankName === '1#反应釜-C3001'" id="equipment-special-pump"
+                                    class="equipment_item" />
+                                <WorkModeControl v-if="tankName === '兑卤器低位槽-V3001'" class="equipment_item" />
+                                <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)"
+                                    :id="`equipment-${equipment.code}`" class="equipment_item" />
+                            </div>
+                        </div>
+
+                        <!-- 无数据提示 -->
+                        <div v-if="isEmptyData" class="no_data">
+                            未获取到设备数据
+                        </div>
+                    </div>
                 </div>
-            </div>
 
-            <!-- 无数据提示 -->
-            <div v-if="isEmptyData" class="no-data">
-                未获取到设备数据
+                <!-- 右侧导航栏 -->
+                <TankNavigation :tanks="allTanks" :current-index="currentNavIndex" :header-height="headerHeight"
+                    @tankClick="scrollToTank" @scrollPositionChange="handleScrollPositionChange" />
             </div>
         </div>
     </div>
 </template>
 
 <script setup>
-import HeaderComponent from '@/components/DCS/HeaderComponent.vue'
-import PageNav from '@/components/GeneralComponents/control/PageNavComponent.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 { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment'
-import { useEquipmentLayout } from '@/hooks/useEquipmentLayout'
-import { navItems_Na2SO4 } from '@/config'
-import { stompClient } from '@/utils/ws/stompClient'
-import { updateZTPageConfig } from '@/api/dcs/configurePage'
-
-const route = useRoute()
+import { ref, onMounted, onBeforeUnmount, watch, watchEffect } from 'vue';
+import { useRoute } from 'vue-router';
+// 导入组件
+import HeaderComponent from '@/components/DCS/HeaderComponent.vue';
+import PageNav from '@/components/GeneralComponents/control/PageNavComponent.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 WorkModeControl from '@/components/GeneralComponents/control/WorkModeControl2Component.vue';
+import TankNavigation from '@/components/GeneralComponents/control/TankNavigationComponent.vue';
+
+// 导入其他依赖
+import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment';
+import { useEquipmentLayout } from '@/hooks/useEquipmentLayout';
+import { navItems_Na2SO4 } from '@/config';
+import { stompClient } from '@/utils/ws/stompClient';
+import { updateZTPageConfig } from '@/api/dcs/configurePage';
+
+const route = useRoute();
 
 // 状态变量
-const isLoading = ref(true)// 加载状态
-const hasError = ref(false)// 错误状态
-const errorMessage = ref('')// 错误信息
-const isEmptyData = ref(false)// 是否无数据
+const isLoading = ref(true);
+const hasError = ref(false);
+const errorMessage = ref('');
+const isEmptyData = ref(false);
+const fixedHeader = ref(null);
+const headerHeight = ref(0);
+
+const flowCode = ref(route.name);
+const title = navItems_Na2SO4.find(i => i.code === flowCode.value)?.label || '';
+
+// 导航相关变量
+const allTanks = ref([]);  // 所有罐体列表
+const currentNavIndex = ref(-1);  // 当前激活的导航项索引
 
-const flowCode = route.name
+// 设备配置和数据
+const deviceConfigGroup = ref({});// 设备配置分组
+const deviceDataGroup = ref({});// 设备数据分组
+const pageParams = ref({});
 
-const title = navItems_Na2SO4.find(i => i.code === flowCode)?.label || ''
+const { generatePageParams } = useEquipmentLayout();
 
 // 监控flowCode变化
 watch(() => route.name, (newVal) => {
-    console.log('路由变化:', newVal)
-    flowCode = newVal
-    loadInitialData()
-})
-const deviceConfigGroup = ref({})// 设备配置分组 - 用于渲染设备结构
-const deviceDataGroup = ref({})// 设备数据分组 - 用于提供设备数据
-const pageParams = ref({})
-
-const { generatePageParams } = useEquipmentLayout()
+    console.log('路由变化:', newVal);
+    flowCode.value = newVal;
+    loadInitialData();
+});
+
+// 生成罐体导航列表(直接取deviceConfigGroup的tankName)
+watchEffect(() => {
+    // 收集所有罐体名称
+    const tanks = [];
+
+    // 直接使用deviceConfigGroup的键作为罐体名称
+    Object.keys(deviceConfigGroup.value).forEach(tankName => {
+        tanks.push({
+            uniqueId: `tank-${tankName}`,
+            tankName,
+            shortName: shortenTankName(tankName),
+        });
+    });
+
+    allTanks.value = tanks;
+    currentNavIndex.value = -1;
+});
+
+// 计算固定头部高度
+const calculateHeaderHeight = () => {
+    if (fixedHeader.value) {
+        headerHeight.value = fixedHeader.value.offsetHeight;
+    }
+};
 
 async function loadInitialData() {
     try {
-        isLoading.value = true
-        hasError.value = false
-        errorMessage.value = ''
+        isLoading.value = true;
+        hasError.value = false;
+        errorMessage.value = '';
 
-        const res = await getPageEquipmentGroupByTankByFlowCode(flowCode)
+        const res = await getPageEquipmentGroupByTankByFlowCode(flowCode.value);
 
         deviceConfigGroup.value = Object.fromEntries(
             Object.entries(res.data || {}).filter(([_, equipments]) => equipments?.length > 0)
-        )
-        // console.log('设备配置分组:', deviceConfigGroup.value)
-        isEmptyData.value = Object.keys(deviceConfigGroup.value).length === 0
+        );
+        isEmptyData.value = Object.keys(deviceConfigGroup.value).length === 0;
 
-        pageParams.value = generatePageParams(deviceConfigGroup.value)
-        await updatePageConfig()
+        pageParams.value = generatePageParams(deviceConfigGroup.value);
+        // console.log('页面参数:', pageParams.value);
+        await updatePageConfig();
     } catch (err) {
-        hasError.value = true
-        errorMessage.value = err.message || '加载设备分组失败'
-        console.error('加载页面设备分组失败', err)
+        hasError.value = true;
+        errorMessage.value = err.message || '加载设备分组失败';
+        console.error('加载页面设备分组失败', err);
     } finally {
-        isLoading.value = false
+        isLoading.value = false;
     }
 }
 
 async function updatePageConfig() {
     try {
-        // console.log('更新页面配置:', flowCode, pageParams.value)
-        await updateZTPageConfig(flowCode, pageParams.value)
+        await updateZTPageConfig(flowCode.value, pageParams.value);
 
         // 先取消可能存在的订阅,避免重复订阅
-        stompClient.unsubscribeFromPage(flowCode)
+        stompClient.unsubscribeFromPage(flowCode.value);
 
-        stompClient.subscribeToPage(flowCode, data => {
-            // console.log('收到页面实时数据推送:', data)
+        stompClient.subscribeToPage(flowCode.value, data => {
             if (data && Object.keys(data).length > 0) {
                 try {
                     // 按key逐个更新,避免整体替换导致页面闪烁
                     for (const key in data) {
-                        deviceDataGroup.value[key] = data[key] || []
+                        deviceDataGroup.value[key] = data[key] || [];
                     }
                 } catch (updateErr) {
-                    console.error('更新设备数据失败:', updateErr)
+                    console.error('更新设备数据失败:', updateErr);
                 }
             }
-        })
+        });
     } catch (err) {
-        console.error('页面配置失败:', err)
+        console.error('页面配置失败:', err);
         // 配置失败也可以继续显示已有数据
     }
 }
+
 /**
  * 根据设备类型编码判断设备类型并返回对应组件
- * @param {number|string} equipmentType - 设备类型编码(1、5为阀门,2为泵,4为传感器)
- * @returns {Component} 对应的设备组件
  */
 function getComponentByType(equipmentType) {
-    // 处理空值或无效类型情况
     if (equipmentType === undefined || equipmentType === null) {
-        console.error('设备类型编码不能为空')
-        return 'div'
+        console.error('设备类型编码不能为空');
+        return 'div';
     }
 
-    // 将输入转换为数字类型(兼容字符串形式的数字,如"1")
-    const type = Number(equipmentType)
+    const type = Number(equipmentType);
 
-    // 校验是否为有效数字
     if (isNaN(type)) {
-        console.error('无效的设备类型编码:', equipmentType)
-        return 'div'
+        console.error('无效的设备类型编码:', equipmentType);
+        return 'div';
     }
 
-    // 根据类型编码返回对应组件
     if (type === 1 || type === 5) {
-        // console.log(`设备类型编码${type}判定为阀门`)
-        return ValveControlComponent
+        return ValveControlComponent;
     } else if (type === 2) {
-        // console.log(`设备类型编码${type}判定为泵`)
-        return PumpControlComponent
+        return PumpControlComponent;
     } else if (type === 4) {
-        // console.log(`设备类型编码${type}判定为传感器`)
-        return SensorControl
+        return SensorControl;
     } else {
-        // 未知类型
-        console.warn(`设备类型编码${type}无法识别,使用默认div`)
-        return 'div'
+        console.warn(`设备类型编码${type}无法识别,使用默认div`);
+        return 'div';
     }
 }
 
 function getValueByCode(tankName, code) {
-    const dataArr = deviceDataGroup.value[tankName] || []
-    const target = dataArr.find(item => item.code === code)
-    return target ? target.value : []
+    const dataArr = deviceDataGroup.value[tankName] || [];
+    const target = dataArr.find(item => item.code === code);
+    return target ? target.value : [];
+}
+
+// 导航功能:滚动到指定罐体
+function scrollToTank(tank, index) {
+    currentNavIndex.value = index;
+    const elementId = `tank-${tank.tankName}`;
+
+    const element = document.getElementById(elementId);
+    if (element) {
+        // 计算滚动位置时考虑固定头部的高度
+        window.scrollTo({
+            top: element.offsetTop - headerHeight.value - 20,  // 顶部留一些间距
+            behavior: 'smooth'
+        });
+    }
+}
+
+// 处理滚动位置变化事件
+function handleScrollPositionChange(index) {
+    currentNavIndex.value = index;
+}
+
+// 缩短罐体名称用于导航显示
+function shortenTankName(fullName) {
+    if (fullName.length > 18) {
+        return fullName.substring(0, 16) + '...';
+    }
+    return fullName;
 }
 
 onMounted(() => {
-    console.log('组件挂载')
-    loadInitialData()
-})
+    console.log('组件挂载');
+    // 计算头部高度
+    calculateHeaderHeight();
+    // 监听窗口大小变化,重新计算头部高度
+    window.addEventListener('resize', calculateHeaderHeight);
+    loadInitialData();
+});
 
 onBeforeUnmount(() => {
-    console.log('组件卸载,取消订阅')
-    stompClient.unsubscribeFromPage(flowCode)
-})
+    console.log('组件卸载,取消订阅');
+    stompClient.unsubscribeFromPage(flowCode.value);
+    window.removeEventListener('resize', calculateHeaderHeight);
+});
 </script>
 
 <style scoped lang="scss">
 .dcs_tanks {
     width: 100%;
     min-height: 100vh;
-    padding: 24px 32px;
     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;
-    flex-direction: column;
-    align-items: center;
+    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%;
-    max-width: 1400px;
-    margin-top: 20px;
 }
 
-// 每个罐体占一行
-.tank_row {
+// 每个罐体区域
+.tank_section {
     background: #1a1a1a;
     border-radius: 12px;
-    padding: 16px 20px;
+    padding: 20px 24px;
     width: 100%;
     display: flex;
     flex-direction: column;
-    gap: 16px;
+    gap: 20px;
+    box-sizing: border-box;
+    scroll-margin-top: 160px;
+    /* 用于滚动定位时预留空间 */
 }
 
 .tank_title {
     font-size: 20px;
     color: #fff;
-    padding-bottom: 8px;
+    padding-bottom: 12px;
     border-bottom: 1px solid rgba(255, 255, 255, 0.1);
     text-align: center;
+    margin: 0;
 }
 
-// 设备列表改为横向排列,可滚动
-.equipment_list {
-    display: flex;
-    gap: 16px;
-    overflow-x: auto;
-    padding: 8px 0;
-    align-items: center;
-    min-height: 120px;
-    /* 确保有足够高度显示设备 */
+// 设备网格布局
+.equipment_grid {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    /* 默认4列布局 */
+    gap: 24px;
+    /* 增大间距避免拥挤 */
+    padding: 12px 0;
 }
 
-/* 滚动条美化 */
-.equipment_list::-webkit-scrollbar {
-    height: 6px;
+// 统一设备项样式
+.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;
 }
 
-.equipment_list::-webkit-scrollbar-track {
-    background: rgba(255, 255, 255, 0.05);
-    border-radius: 3px;
+/* 确保子组件正确填充容器 */
+: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;
+    }
 }
 
-.equipment_list::-webkit-scrollbar-thumb {
-    background: rgba(255, 255, 255, 0.2);
-    border-radius: 3px;
+/* 无数据提示样式 */
+.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,
-.no-data {
+.error {
     color: white;
     margin-top: 20px;
     padding: 20px;
     border-radius: 8px;
     background-color: rgba(255, 255, 255, 0.1);
     width: 100%;
-    max-width: 1400px;
     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;
+        }
+    }
 }
 
-.error button {
-    margin-top: 10px;
-    padding: 8px 16px;
-    background-color: #00C851;
-    color: white;
-    border: none;
-    border-radius: 4px;
-    cursor: pointer;
-    transition: background-color 0.2s;
+/* 响应式布局 */
+@media (max-width: 1400px) {
+    .equipment_grid {
+        grid-template-columns: repeat(3, 1fr);
+        gap: 20px;
+    }
 }
 
-.error button:hover {
-    background-color: #007E33;
+@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>

+ 331 - 147
ui/src/views/controlPage/flowSelect/flatBeltFiltration/index.vue

@@ -1,278 +1,462 @@
 <template>
     <div class="dcs_tanks">
-        <HeaderComponent :title="title" backTo="/controlPage/flowSelect" />
-        <PageNav :items="navItems_Na2SO4" :currentCode="flowCode" />
-        <!-- 加载状态提示 -->
-        <div v-if="isLoading" class="loading">
-            正在加载设备数据...
-        </div>
-        <!-- 错误状态提示 -->
-        <div v-if="hasError" class="error">
-            <p>加载数据失败: {{ errorMessage }}</p>
-            <button @click="loadInitialData">重试</button>
+        <!-- 固定在顶部的头部区域 -->
+        <div class="fixed-header" ref="fixedHeader">
+            <HeaderComponent title="硫酸钠反应流程" backTo="/controlPage/flowSelect" />
+            <PageNav :items="navItems_Na2SO4" :currentCode="flowCode" />
         </div>
 
-        <div v-else class="tanks_container">
-            <!-- 每个罐体占一行 -->
-            <div v-for="(equipments, tankName) in deviceConfigGroup" :key="tankName" class="tank_row">
-                <h2 class="tank_title">{{ tankName }}</h2>
-                <div class="equipment_list">
-                    <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)" />
+        <!-- 主内容区(包含导航),添加顶部间距避免被固定头部遮挡 -->
+        <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">
+                                <!-- 添加其他设备组件 -->
+                                <PumpControlComponent title="原矿进料" :dataArr="[5, 100, 100]"
+                                    v-if="tankName === '1#反应釜-C3001'" id="equipment-special-pump"
+                                    class="equipment_item" />
+                                <WorkModeControl v-if="tankName === '兑卤器低位槽-V3001'" class="equipment_item" />
+                                <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)"
+                                    :id="`equipment-${equipment.code}`" class="equipment_item" />
+                            </div>
+                        </div>
+
+                        <!-- 无数据提示 -->
+                        <div v-if="isEmptyData" class="no_data">
+                            未获取到设备数据
+                        </div>
+                    </div>
                 </div>
-            </div>
 
-            <!-- 无数据提示 -->
-            <div v-if="isEmptyData" class="no-data">
-                未获取到设备数据
+                <!-- 右侧导航栏 -->
+                <TankNavigation :tanks="allTanks" :current-index="currentNavIndex" :header-height="headerHeight"
+                    @tankClick="scrollToTank" @scrollPositionChange="handleScrollPositionChange" />
             </div>
         </div>
     </div>
 </template>
 
 <script setup>
-import HeaderComponent from '@/components/DCS/HeaderComponent.vue'
-import PageNav from '@/components/GeneralComponents/control/PageNavComponent.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 { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment'
-import { useEquipmentLayout } from '@/hooks/useEquipmentLayout'
-import { navItems_Na2SO4 } from '@/config'
-import { stompClient } from '@/utils/ws/stompClient'
-import { updateZTPageConfig } from '@/api/dcs/configurePage'
-
-const route = useRoute()
+import { ref, onMounted, onBeforeUnmount, watch, watchEffect } from 'vue';
+import { useRoute } from 'vue-router';
+// 导入组件
+import HeaderComponent from '@/components/DCS/HeaderComponent.vue';
+import PageNav from '@/components/GeneralComponents/control/PageNavComponent.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 WorkModeControl from '@/components/GeneralComponents/control/WorkModeControl2Component.vue';
+import TankNavigation from '@/components/GeneralComponents/control/TankNavigationComponent.vue';
+
+// 导入其他依赖
+import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment';
+import { useEquipmentLayout } from '@/hooks/useEquipmentLayout';
+import { navItems_Na2SO4 } from '@/config';
+import { stompClient } from '@/utils/ws/stompClient';
+import { updateZTPageConfig } from '@/api/dcs/configurePage';
+
+const route = useRoute();
 
 // 状态变量
-const isLoading = ref(true)// 加载状态
-const hasError = ref(false)// 错误状态
-const errorMessage = ref('')// 错误信息
-const isEmptyData = ref(false)// 是否无数据
+const isLoading = ref(true);
+const hasError = ref(false);
+const errorMessage = ref('');
+const isEmptyData = ref(false);
+const fixedHeader = ref(null);
+const headerHeight = ref(0);
+
+const flowCode = ref(route.name);
+const title = navItems_Na2SO4.find(i => i.code === flowCode.value)?.label || '';
+
+// 导航相关变量
+const allTanks = ref([]);  // 所有罐体列表
+const currentNavIndex = ref(-1);  // 当前激活的导航项索引
 
-const flowCode = route.name
+// 设备配置和数据
+const deviceConfigGroup = ref({});// 设备配置分组
+const deviceDataGroup = ref({});// 设备数据分组
+const pageParams = ref({});
 
-const title = navItems_Na2SO4.find(i => i.code === flowCode)?.label || ''
+const { generatePageParams } = useEquipmentLayout();
 
 // 监控flowCode变化
 watch(() => route.name, (newVal) => {
-    console.log('路由变化:', newVal)
-    flowCode = newVal
-    loadInitialData()
-})
-const deviceConfigGroup = ref({})// 设备配置分组 - 用于渲染设备结构
-const deviceDataGroup = ref({})// 设备数据分组 - 用于提供设备数据
-const pageParams = ref({})
-
-const { generatePageParams } = useEquipmentLayout()
+    console.log('路由变化:', newVal);
+    flowCode.value = newVal;
+    loadInitialData();
+});
+
+// 生成罐体导航列表(直接取deviceConfigGroup的tankName)
+watchEffect(() => {
+    // 收集所有罐体名称
+    const tanks = [];
+
+    // 直接使用deviceConfigGroup的键作为罐体名称
+    Object.keys(deviceConfigGroup.value).forEach(tankName => {
+        tanks.push({
+            uniqueId: `tank-${tankName}`,
+            tankName,
+            shortName: shortenTankName(tankName),
+        });
+    });
+
+    allTanks.value = tanks;
+    currentNavIndex.value = -1;
+});
+
+// 计算固定头部高度
+const calculateHeaderHeight = () => {
+    if (fixedHeader.value) {
+        headerHeight.value = fixedHeader.value.offsetHeight;
+    }
+};
 
 async function loadInitialData() {
     try {
-        isLoading.value = true
-        hasError.value = false
-        errorMessage.value = ''
+        isLoading.value = true;
+        hasError.value = false;
+        errorMessage.value = '';
 
-        const res = await getPageEquipmentGroupByTankByFlowCode(flowCode)
+        const res = await getPageEquipmentGroupByTankByFlowCode(flowCode.value);
 
         deviceConfigGroup.value = Object.fromEntries(
             Object.entries(res.data || {}).filter(([_, equipments]) => equipments?.length > 0)
-        )
-        // console.log('设备配置分组:', deviceConfigGroup.value)
-        isEmptyData.value = Object.keys(deviceConfigGroup.value).length === 0
+        );
+        isEmptyData.value = Object.keys(deviceConfigGroup.value).length === 0;
 
-        pageParams.value = generatePageParams(deviceConfigGroup.value)
-        await updatePageConfig()
+        pageParams.value = generatePageParams(deviceConfigGroup.value);
+        // console.log('页面参数:', pageParams.value);
+        await updatePageConfig();
     } catch (err) {
-        hasError.value = true
-        errorMessage.value = err.message || '加载设备分组失败'
-        console.error('加载页面设备分组失败', err)
+        hasError.value = true;
+        errorMessage.value = err.message || '加载设备分组失败';
+        console.error('加载页面设备分组失败', err);
     } finally {
-        isLoading.value = false
+        isLoading.value = false;
     }
 }
 
 async function updatePageConfig() {
     try {
-        // console.log('更新页面配置:', flowCode, pageParams.value)
-        await updateZTPageConfig(flowCode, pageParams.value)
+        await updateZTPageConfig(flowCode.value, pageParams.value);
 
         // 先取消可能存在的订阅,避免重复订阅
-        stompClient.unsubscribeFromPage(flowCode)
+        stompClient.unsubscribeFromPage(flowCode.value);
 
-        stompClient.subscribeToPage(flowCode, data => {
-            // console.log('收到页面实时数据推送:', data)
+        stompClient.subscribeToPage(flowCode.value, data => {
             if (data && Object.keys(data).length > 0) {
                 try {
                     // 按key逐个更新,避免整体替换导致页面闪烁
                     for (const key in data) {
-                        deviceDataGroup.value[key] = data[key] || []
+                        deviceDataGroup.value[key] = data[key] || [];
                     }
                 } catch (updateErr) {
-                    console.error('更新设备数据失败:', updateErr)
+                    console.error('更新设备数据失败:', updateErr);
                 }
             }
-        })
+        });
     } catch (err) {
-        console.error('页面配置失败:', err)
+        console.error('页面配置失败:', err);
         // 配置失败也可以继续显示已有数据
     }
 }
+
 /**
  * 根据设备类型编码判断设备类型并返回对应组件
- * @param {number|string} equipmentType - 设备类型编码(1、5为阀门,2为泵,4为传感器)
- * @returns {Component} 对应的设备组件
  */
 function getComponentByType(equipmentType) {
-    // 处理空值或无效类型情况
     if (equipmentType === undefined || equipmentType === null) {
-        console.error('设备类型编码不能为空')
-        return 'div'
+        console.error('设备类型编码不能为空');
+        return 'div';
     }
 
-    // 将输入转换为数字类型(兼容字符串形式的数字,如"1")
-    const type = Number(equipmentType)
+    const type = Number(equipmentType);
 
-    // 校验是否为有效数字
     if (isNaN(type)) {
-        console.error('无效的设备类型编码:', equipmentType)
-        return 'div'
+        console.error('无效的设备类型编码:', equipmentType);
+        return 'div';
     }
 
-    // 根据类型编码返回对应组件
     if (type === 1 || type === 5) {
-        // console.log(`设备类型编码${type}判定为阀门`)
-        return ValveControlComponent
+        return ValveControlComponent;
     } else if (type === 2) {
-        // console.log(`设备类型编码${type}判定为泵`)
-        return PumpControlComponent
+        return PumpControlComponent;
     } else if (type === 4) {
-        // console.log(`设备类型编码${type}判定为传感器`)
-        return SensorControl
+        return SensorControl;
     } else {
-        // 未知类型
-        console.warn(`设备类型编码${type}无法识别,使用默认div`)
-        return 'div'
+        console.warn(`设备类型编码${type}无法识别,使用默认div`);
+        return 'div';
     }
 }
 
 function getValueByCode(tankName, code) {
-    const dataArr = deviceDataGroup.value[tankName] || []
-    const target = dataArr.find(item => item.code === code)
-    return target ? target.value : []
+    const dataArr = deviceDataGroup.value[tankName] || [];
+    const target = dataArr.find(item => item.code === code);
+    return target ? target.value : [];
+}
+
+// 导航功能:滚动到指定罐体
+function scrollToTank(tank, index) {
+    currentNavIndex.value = index;
+    const elementId = `tank-${tank.tankName}`;
+
+    const element = document.getElementById(elementId);
+    if (element) {
+        // 计算滚动位置时考虑固定头部的高度
+        window.scrollTo({
+            top: element.offsetTop - headerHeight.value - 20,  // 顶部留一些间距
+            behavior: 'smooth'
+        });
+    }
+}
+
+// 处理滚动位置变化事件
+function handleScrollPositionChange(index) {
+    currentNavIndex.value = index;
+}
+
+// 缩短罐体名称用于导航显示
+function shortenTankName(fullName) {
+    if (fullName.length > 18) {
+        return fullName.substring(0, 16) + '...';
+    }
+    return fullName;
 }
 
 onMounted(() => {
-    console.log('组件挂载')
-    loadInitialData()
-})
+    console.log('组件挂载');
+    // 计算头部高度
+    calculateHeaderHeight();
+    // 监听窗口大小变化,重新计算头部高度
+    window.addEventListener('resize', calculateHeaderHeight);
+    loadInitialData();
+});
 
 onBeforeUnmount(() => {
-    console.log('组件卸载,取消订阅')
-    stompClient.unsubscribeFromPage(flowCode)
-})
+    console.log('组件卸载,取消订阅');
+    stompClient.unsubscribeFromPage(flowCode.value);
+    window.removeEventListener('resize', calculateHeaderHeight);
+});
 </script>
 
 <style scoped lang="scss">
 .dcs_tanks {
     width: 100%;
     min-height: 100vh;
-    padding: 24px 32px;
     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;
-    flex-direction: column;
-    align-items: center;
+    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%;
-    max-width: 1400px;
-    margin-top: 20px;
 }
 
-// 每个罐体占一行
-.tank_row {
+// 每个罐体区域
+.tank_section {
     background: #1a1a1a;
     border-radius: 12px;
-    padding: 16px 20px;
+    padding: 20px 24px;
     width: 100%;
     display: flex;
     flex-direction: column;
-    gap: 16px;
+    gap: 20px;
+    box-sizing: border-box;
+    scroll-margin-top: 160px;
+    /* 用于滚动定位时预留空间 */
 }
 
 .tank_title {
     font-size: 20px;
     color: #fff;
-    padding-bottom: 8px;
+    padding-bottom: 12px;
     border-bottom: 1px solid rgba(255, 255, 255, 0.1);
     text-align: center;
+    margin: 0;
 }
 
-// 设备列表改为横向排列,可滚动
-.equipment_list {
-    display: flex;
-    gap: 16px;
-    overflow-x: auto;
-    padding: 8px 0;
-    min-height: 120px;
-    align-items: center;
-    /* 确保有足够高度显示设备 */
+// 设备网格布局
+.equipment_grid {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    /* 默认4列布局 */
+    gap: 24px;
+    /* 增大间距避免拥挤 */
+    padding: 12px 0;
 }
 
-/* 滚动条美化 */
-.equipment_list::-webkit-scrollbar {
-    height: 6px;
+// 统一设备项样式
+.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;
 }
 
-.equipment_list::-webkit-scrollbar-track {
-    background: rgba(255, 255, 255, 0.05);
-    border-radius: 3px;
+/* 确保子组件正确填充容器 */
+: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;
+    }
 }
 
-.equipment_list::-webkit-scrollbar-thumb {
-    background: rgba(255, 255, 255, 0.2);
-    border-radius: 3px;
+/* 无数据提示样式 */
+.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,
-.no-data {
+.error {
     color: white;
     margin-top: 20px;
     padding: 20px;
     border-radius: 8px;
     background-color: rgba(255, 255, 255, 0.1);
     width: 100%;
-    max-width: 1400px;
     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;
+        }
+    }
 }
 
-.error button {
-    margin-top: 10px;
-    padding: 8px 16px;
-    background-color: #00C851;
-    color: white;
-    border: none;
-    border-radius: 4px;
-    cursor: pointer;
-    transition: background-color 0.2s;
+/* 响应式布局 */
+@media (max-width: 1400px) {
+    .equipment_grid {
+        grid-template-columns: repeat(3, 1fr);
+        gap: 20px;
+    }
 }
 
-.error button:hover {
-    background-color: #007E33;
+@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>

+ 367 - 197
ui/src/views/controlPage/flowSelect/reaction/index.vue

@@ -1,292 +1,462 @@
 <template>
     <div class="dcs_tanks">
-        <HeaderComponent :title="title" backTo="/controlPage/flowSelect" />
-        <PageNav :items="navItems_Na2SO4" :currentCode="flowCode" />
-        <!-- 加载状态提示 -->
-        <div v-if="isLoading" class="loading">
-            正在加载设备数据...
-        </div>
-        <!-- 错误状态提示 -->
-        <div v-if="hasError" class="error">
-            <p>加载数据失败: {{ errorMessage }}</p>
-            <button @click="loadInitialData">重试</button>
+        <!-- 固定在顶部的头部区域 -->
+        <div class="fixed-header" ref="fixedHeader">
+            <HeaderComponent title="硫酸钠反应流程" backTo="/controlPage/flowSelect" />
+            <PageNav :items="navItems_Na2SO4" :currentCode="flowCode" />
         </div>
 
-        <div v-else class="tanks_container">
-            <!-- 每个罐体占一行 -->
-            <div v-for="(equipments, tankName) in deviceConfigGroup" :key="tankName" class="tank_row">
-                <h2 class="tank_title">{{ tankName }}</h2>
-                <div class="equipment_list">
-                    <WorkModeControl v-if="tankName === '兑卤器低位槽-V3001'" />
-                    <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)" />
-                    <!-- 添加其他设备组件 -->
-                    <PumpControlComponent title="原矿进料" :dataArr="[5, 100, 100]" v-if="tankName === '1#反应釜-C3001'" />
+        <!-- 主内容区(包含导航),添加顶部间距避免被固定头部遮挡 -->
+        <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">
+                                <!-- 添加其他设备组件 -->
+                                <PumpControlComponent title="原矿进料" :dataArr="[5, 100, 100]"
+                                    v-if="tankName === '1#反应釜-C3001'" id="equipment-special-pump"
+                                    class="equipment_item" />
+                                <WorkModeControl v-if="tankName === '兑卤器低位槽-V3001'" class="equipment_item" />
+                                <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)"
+                                    :id="`equipment-${equipment.code}`" class="equipment_item" />
+                            </div>
+                        </div>
+
+                        <!-- 无数据提示 -->
+                        <div v-if="isEmptyData" class="no_data">
+                            未获取到设备数据
+                        </div>
+                    </div>
                 </div>
-            </div>
 
-            <!-- 无数据提示 -->
-            <div v-if="isEmptyData" class="no_data">
-                未获取到设备数据
+                <!-- 右侧导航栏 -->
+                <TankNavigation :tanks="allTanks" :current-index="currentNavIndex" :header-height="headerHeight"
+                    @tankClick="scrollToTank" @scrollPositionChange="handleScrollPositionChange" />
             </div>
         </div>
     </div>
 </template>
 
 <script setup>
-import HeaderComponent from '@/components/DCS/HeaderComponent.vue'
-import PageNav from '@/components/GeneralComponents/control/PageNavComponent.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 WorkModeControl from '@/components/GeneralComponents/control/WorkModeControl2Component.vue'
-import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment'
-import { useEquipmentLayout } from '@/hooks/useEquipmentLayout'
-import { navItems_Na2SO4 } from '@/config'
-import { stompClient } from '@/utils/ws/stompClient'
-import { updateZTPageConfig } from '@/api/dcs/configurePage'
-
-const route = useRoute()
+import { ref, onMounted, onBeforeUnmount, watch, watchEffect } from 'vue';
+import { useRoute } from 'vue-router';
+// 导入组件
+import HeaderComponent from '@/components/DCS/HeaderComponent.vue';
+import PageNav from '@/components/GeneralComponents/control/PageNavComponent.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 WorkModeControl from '@/components/GeneralComponents/control/WorkModeControl2Component.vue';
+import TankNavigation from '@/components/GeneralComponents/control/TankNavigationComponent.vue';
+
+// 导入其他依赖
+import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment';
+import { useEquipmentLayout } from '@/hooks/useEquipmentLayout';
+import { navItems_Na2SO4 } from '@/config';
+import { stompClient } from '@/utils/ws/stompClient';
+import { updateZTPageConfig } from '@/api/dcs/configurePage';
+
+const route = useRoute();
 
 // 状态变量
-const isLoading = ref(true)
-const hasError = ref(false)
-const errorMessage = ref('')
-const isEmptyData = ref(false)
+const isLoading = ref(true);
+const hasError = ref(false);
+const errorMessage = ref('');
+const isEmptyData = ref(false);
+const fixedHeader = ref(null);
+const headerHeight = ref(0);
+
+const flowCode = ref(route.name);
+const title = navItems_Na2SO4.find(i => i.code === flowCode.value)?.label || '';
 
-const flowCode = route.name
+// 导航相关变量
+const allTanks = ref([]);  // 所有罐体列表
+const currentNavIndex = ref(-1);  // 当前激活的导航项索引
 
-const title = navItems_Na2SO4.find(i => i.code === flowCode)?.label || ''
+// 设备配置和数据
+const deviceConfigGroup = ref({});// 设备配置分组
+const deviceDataGroup = ref({});// 设备数据分组
+const pageParams = ref({});
+
+const { generatePageParams } = useEquipmentLayout();
 
 // 监控flowCode变化
 watch(() => route.name, (newVal) => {
-    console.log('路由变化:', newVal)
-    flowCode = newVal
-    loadInitialData()
-})
-const deviceConfigGroup = ref({})// 设备配置分组 - 用于渲染设备结构
-const deviceDataGroup = ref({})// 设备数据分组 - 用于提供设备数据
-const pageParams = ref({})
-
-const { generatePageParams } = useEquipmentLayout()
+    console.log('路由变化:', newVal);
+    flowCode.value = newVal;
+    loadInitialData();
+});
+
+// 生成罐体导航列表(直接取deviceConfigGroup的tankName)
+watchEffect(() => {
+    // 收集所有罐体名称
+    const tanks = [];
+
+    // 直接使用deviceConfigGroup的键作为罐体名称
+    Object.keys(deviceConfigGroup.value).forEach(tankName => {
+        tanks.push({
+            uniqueId: `tank-${tankName}`,
+            tankName,
+            shortName: shortenTankName(tankName),
+        });
+    });
+
+    allTanks.value = tanks;
+    currentNavIndex.value = -1;
+});
+
+// 计算固定头部高度
+const calculateHeaderHeight = () => {
+    if (fixedHeader.value) {
+        headerHeight.value = fixedHeader.value.offsetHeight;
+    }
+};
 
 async function loadInitialData() {
     try {
-        isLoading.value = true
-        hasError.value = false
-        errorMessage.value = ''
+        isLoading.value = true;
+        hasError.value = false;
+        errorMessage.value = '';
 
-        const res = await getPageEquipmentGroupByTankByFlowCode(flowCode)
+        const res = await getPageEquipmentGroupByTankByFlowCode(flowCode.value);
 
         deviceConfigGroup.value = Object.fromEntries(
             Object.entries(res.data || {}).filter(([_, equipments]) => equipments?.length > 0)
-        )
-        // console.log('设备配置分组:', deviceConfigGroup.value)
-        isEmptyData.value = Object.keys(deviceConfigGroup.value).length === 0
+        );
+        isEmptyData.value = Object.keys(deviceConfigGroup.value).length === 0;
 
-        pageParams.value = generatePageParams(deviceConfigGroup.value)
-        await updatePageConfig()
+        pageParams.value = generatePageParams(deviceConfigGroup.value);
+        // console.log('页面参数:', pageParams.value);
+        await updatePageConfig();
     } catch (err) {
-        hasError.value = true
-        errorMessage.value = err.message || '加载设备分组失败'
-        console.error('加载页面设备分组失败', err)
+        hasError.value = true;
+        errorMessage.value = err.message || '加载设备分组失败';
+        console.error('加载页面设备分组失败', err);
     } finally {
-        isLoading.value = false
+        isLoading.value = false;
     }
 }
 
 async function updatePageConfig() {
     try {
-        // console.log('更新页面配置:', flowCode, pageParams.value)
-        await updateZTPageConfig(flowCode, pageParams.value)
+        await updateZTPageConfig(flowCode.value, pageParams.value);
 
         // 先取消可能存在的订阅,避免重复订阅
-        stompClient.unsubscribeFromPage(flowCode)
+        stompClient.unsubscribeFromPage(flowCode.value);
 
-        stompClient.subscribeToPage(flowCode, data => {
-            // console.log('收到页面实时数据推送:', data)
+        stompClient.subscribeToPage(flowCode.value, data => {
             if (data && Object.keys(data).length > 0) {
                 try {
                     // 按key逐个更新,避免整体替换导致页面闪烁
                     for (const key in data) {
-                        deviceDataGroup.value[key] = data[key] || []
+                        deviceDataGroup.value[key] = data[key] || [];
                     }
                 } catch (updateErr) {
-                    console.error('更新设备数据失败:', updateErr)
+                    console.error('更新设备数据失败:', updateErr);
                 }
             }
-        })
+        });
     } catch (err) {
-        console.error('页面配置失败:', err)
+        console.error('页面配置失败:', err);
         // 配置失败也可以继续显示已有数据
     }
 }
+
 /**
  * 根据设备类型编码判断设备类型并返回对应组件
- * @param {number|string} equipmentType - 设备类型编码(1、5为阀门,2为泵,4为传感器)
- * @returns {Component} 对应的设备组件
  */
 function getComponentByType(equipmentType) {
-    // 处理空值或无效类型情况
     if (equipmentType === undefined || equipmentType === null) {
-        console.error('设备类型编码不能为空')
-        return 'div'
+        console.error('设备类型编码不能为空');
+        return 'div';
     }
 
-    // 将输入转换为数字类型(兼容字符串形式的数字,如"1")
-    const type = Number(equipmentType)
+    const type = Number(equipmentType);
 
-    // 校验是否为有效数字
     if (isNaN(type)) {
-        console.error('无效的设备类型编码:', equipmentType)
-        return 'div'
+        console.error('无效的设备类型编码:', equipmentType);
+        return 'div';
     }
 
-    // 根据类型编码返回对应组件
     if (type === 1 || type === 5) {
-        // console.log(`设备类型编码${type}判定为阀门`)
-        return ValveControlComponent
+        return ValveControlComponent;
     } else if (type === 2) {
-        // console.log(`设备类型编码${type}判定为泵`)
-        return PumpControlComponent
+        return PumpControlComponent;
     } else if (type === 4) {
-        // console.log(`设备类型编码${type}判定为传感器`)
-        return SensorControl
+        return SensorControl;
     } else {
-        // 未知类型
-        console.warn(`设备类型编码${type}无法识别,使用默认div`)
-        return 'div'
+        console.warn(`设备类型编码${type}无法识别,使用默认div`);
+        return 'div';
     }
 }
 
 function getValueByCode(tankName, code) {
-    const dataArr = deviceDataGroup.value[tankName] || []
-    const target = dataArr.find(item => item.code === code)
-    return target ? target.value : []
+    const dataArr = deviceDataGroup.value[tankName] || [];
+    const target = dataArr.find(item => item.code === code);
+    return target ? target.value : [];
+}
+
+// 导航功能:滚动到指定罐体
+function scrollToTank(tank, index) {
+    currentNavIndex.value = index;
+    const elementId = `tank-${tank.tankName}`;
+
+    const element = document.getElementById(elementId);
+    if (element) {
+        // 计算滚动位置时考虑固定头部的高度
+        window.scrollTo({
+            top: element.offsetTop - headerHeight.value - 20,  // 顶部留一些间距
+            behavior: 'smooth'
+        });
+    }
+}
+
+// 处理滚动位置变化事件
+function handleScrollPositionChange(index) {
+    currentNavIndex.value = index;
+}
+
+// 缩短罐体名称用于导航显示
+function shortenTankName(fullName) {
+    if (fullName.length > 18) {
+        return fullName.substring(0, 16) + '...';
+    }
+    return fullName;
 }
 
 onMounted(() => {
-    console.log('组件挂载')
-    loadInitialData()
-})
+    console.log('组件挂载');
+    // 计算头部高度
+    calculateHeaderHeight();
+    // 监听窗口大小变化,重新计算头部高度
+    window.addEventListener('resize', calculateHeaderHeight);
+    loadInitialData();
+});
 
 onBeforeUnmount(() => {
-    console.log('组件卸载,取消订阅')
-    stompClient.unsubscribeFromPage(flowCode)
-})
+    console.log('组件卸载,取消订阅');
+    stompClient.unsubscribeFromPage(flowCode.value);
+    window.removeEventListener('resize', calculateHeaderHeight);
+});
 </script>
 
 <style scoped lang="scss">
 .dcs_tanks {
     width: 100%;
     min-height: 100vh;
-    padding: 24px 32px;
     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;
-    align-items: center;
+    gap: 30px;
+    width: 100%;
+}
 
-    // 罐体容器改为纵向排列
-    .tanks_container {
-        display: flex;
-        flex-direction: column;
-        gap: 30px;
-        width: 100%;
-        max-width: 1400px;
-        margin-top: 20px;
-
-        // 每个罐体占一行
-        .tank_row {
-            background: #1a1a1a;
-            border-radius: 12px;
-            padding: 16px 20px;
-            width: 100%;
-            display: flex;
-            flex-direction: column;
-            gap: 16px;
-
-            .tank_title {
-                font-size: 20px;
-                color: #fff;
-                padding-bottom: 8px;
-                border-bottom: 1px solid rgba(255, 255, 255, 0.1);
-                text-align: center;
-            }
+// 每个罐体区域
+.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;
+    /* 用于滚动定位时预留空间 */
+}
 
-            // 设备列表改为横向排列,可滚动
-            .equipment_list {
-                display: flex;
-                gap: 16px;
-                overflow-x: auto;
-                padding: 8px 0;
-                min-height: 120px;
-                align-items: center;
-                /* 确保有足够高度显示设备 */
-
-                /* 滚动条美化 */
-                &::-webkit-scrollbar {
-                    height: 6px;
-                }
+.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;
+}
 
-                &::-webkit-scrollbar-track {
-                    background: rgba(255, 255, 255, 0.05);
-                    border-radius: 3px;
-                }
+// 设备网格布局
+.equipment_grid {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    /* 默认4列布局 */
+    gap: 24px;
+    /* 增大间距避免拥挤 */
+    padding: 12px 0;
+}
 
-                &::-webkit-scrollbar-thumb {
-                    background: rgba(255, 255, 255, 0.2);
-                    border-radius: 3px;
-                }
-            }
-        }
+// 统一设备项样式
+.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;
+}
 
-        /* 无数据提示样式 */
-        .no_data {
-            color: white;
-            margin-top: 20px;
-            padding: 20px;
-            border-radius: 8px;
-            background-color: rgba(255, 255, 255, 0.1);
-            width: 100%;
-            max-width: 1400px;
-            text-align: center;
-        }
+/* 确保子组件正确填充容器 */
+: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;
 
-    /* 状态提示样式 */
-    .loading,
-    .error {
+    button {
+        margin-top: 10px;
+        padding: 8px 16px;
+        background-color: #00C851;
         color: white;
-        margin-top: 20px;
-        padding: 20px;
-        border-radius: 8px;
-        background-color: rgba(255, 255, 255, 0.1);
-        width: 100%;
-        max-width: 1400px;
-        text-align: center;
-    }
+        border: none;
+        border-radius: 4px;
+        cursor: pointer;
+        transition: background-color 0.2s;
 
-    .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;
-            }
+        &: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>

+ 331 - 147
ui/src/views/controlPage/flowSelect/solidLiquidSeparation/index.vue

@@ -1,278 +1,462 @@
 <template>
     <div class="dcs_tanks">
-        <HeaderComponent :title="title" backTo="/controlPage/flowSelect" />
-        <PageNav :items="navItems_Na2SO4" :currentCode="flowCode" />
-        <!-- 加载状态提示 -->
-        <div v-if="isLoading" class="loading">
-            正在加载设备数据...
-        </div>
-        <!-- 错误状态提示 -->
-        <div v-if="hasError" class="error">
-            <p>加载数据失败: {{ errorMessage }}</p>
-            <button @click="loadInitialData">重试</button>
+        <!-- 固定在顶部的头部区域 -->
+        <div class="fixed-header" ref="fixedHeader">
+            <HeaderComponent title="硫酸钠反应流程" backTo="/controlPage/flowSelect" />
+            <PageNav :items="navItems_Na2SO4" :currentCode="flowCode" />
         </div>
 
-        <div v-else class="tanks_container">
-            <!-- 每个罐体占一行 -->
-            <div v-for="(equipments, tankName) in deviceConfigGroup" :key="tankName" class="tank_row">
-                <h2 class="tank_title">{{ tankName }}</h2>
-                <div class="equipment_list">
-                    <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)" />
+        <!-- 主内容区(包含导航),添加顶部间距避免被固定头部遮挡 -->
+        <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">
+                                <!-- 添加其他设备组件 -->
+                                <PumpControlComponent title="原矿进料" :dataArr="[5, 100, 100]"
+                                    v-if="tankName === '1#反应釜-C3001'" id="equipment-special-pump"
+                                    class="equipment_item" />
+                                <WorkModeControl v-if="tankName === '兑卤器低位槽-V3001'" class="equipment_item" />
+                                <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)"
+                                    :id="`equipment-${equipment.code}`" class="equipment_item" />
+                            </div>
+                        </div>
+
+                        <!-- 无数据提示 -->
+                        <div v-if="isEmptyData" class="no_data">
+                            未获取到设备数据
+                        </div>
+                    </div>
                 </div>
-            </div>
 
-            <!-- 无数据提示 -->
-            <div v-if="isEmptyData" class="no-data">
-                未获取到设备数据
+                <!-- 右侧导航栏 -->
+                <TankNavigation :tanks="allTanks" :current-index="currentNavIndex" :header-height="headerHeight"
+                    @tankClick="scrollToTank" @scrollPositionChange="handleScrollPositionChange" />
             </div>
         </div>
     </div>
 </template>
 
 <script setup>
-import HeaderComponent from '@/components/DCS/HeaderComponent.vue'
-import PageNav from '@/components/GeneralComponents/control/PageNavComponent.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 { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment'
-import { useEquipmentLayout } from '@/hooks/useEquipmentLayout'
-import { navItems_Na2SO4 } from '@/config'
-import { stompClient } from '@/utils/ws/stompClient'
-import { updateZTPageConfig } from '@/api/dcs/configurePage'
-
-const route = useRoute()
+import { ref, onMounted, onBeforeUnmount, watch, watchEffect } from 'vue';
+import { useRoute } from 'vue-router';
+// 导入组件
+import HeaderComponent from '@/components/DCS/HeaderComponent.vue';
+import PageNav from '@/components/GeneralComponents/control/PageNavComponent.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 WorkModeControl from '@/components/GeneralComponents/control/WorkModeControl2Component.vue';
+import TankNavigation from '@/components/GeneralComponents/control/TankNavigationComponent.vue';
+
+// 导入其他依赖
+import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment';
+import { useEquipmentLayout } from '@/hooks/useEquipmentLayout';
+import { navItems_Na2SO4 } from '@/config';
+import { stompClient } from '@/utils/ws/stompClient';
+import { updateZTPageConfig } from '@/api/dcs/configurePage';
+
+const route = useRoute();
 
 // 状态变量
-const isLoading = ref(true)// 加载状态
-const hasError = ref(false)// 错误状态
-const errorMessage = ref('')// 错误信息
-const isEmptyData = ref(false)// 是否无数据
+const isLoading = ref(true);
+const hasError = ref(false);
+const errorMessage = ref('');
+const isEmptyData = ref(false);
+const fixedHeader = ref(null);
+const headerHeight = ref(0);
+
+const flowCode = ref(route.name);
+const title = navItems_Na2SO4.find(i => i.code === flowCode.value)?.label || '';
+
+// 导航相关变量
+const allTanks = ref([]);  // 所有罐体列表
+const currentNavIndex = ref(-1);  // 当前激活的导航项索引
 
-const flowCode = route.name
+// 设备配置和数据
+const deviceConfigGroup = ref({});// 设备配置分组
+const deviceDataGroup = ref({});// 设备数据分组
+const pageParams = ref({});
 
-const title = navItems_Na2SO4.find(i => i.code === flowCode)?.label || ''
+const { generatePageParams } = useEquipmentLayout();
 
 // 监控flowCode变化
 watch(() => route.name, (newVal) => {
-    console.log('路由变化:', newVal)
-    flowCode = newVal
-    loadInitialData()
-})
-const deviceConfigGroup = ref({})// 设备配置分组 - 用于渲染设备结构
-const deviceDataGroup = ref({})// 设备数据分组 - 用于提供设备数据
-const pageParams = ref({})
-
-const { generatePageParams } = useEquipmentLayout()
+    console.log('路由变化:', newVal);
+    flowCode.value = newVal;
+    loadInitialData();
+});
+
+// 生成罐体导航列表(直接取deviceConfigGroup的tankName)
+watchEffect(() => {
+    // 收集所有罐体名称
+    const tanks = [];
+
+    // 直接使用deviceConfigGroup的键作为罐体名称
+    Object.keys(deviceConfigGroup.value).forEach(tankName => {
+        tanks.push({
+            uniqueId: `tank-${tankName}`,
+            tankName,
+            shortName: shortenTankName(tankName),
+        });
+    });
+
+    allTanks.value = tanks;
+    currentNavIndex.value = -1;
+});
+
+// 计算固定头部高度
+const calculateHeaderHeight = () => {
+    if (fixedHeader.value) {
+        headerHeight.value = fixedHeader.value.offsetHeight;
+    }
+};
 
 async function loadInitialData() {
     try {
-        isLoading.value = true
-        hasError.value = false
-        errorMessage.value = ''
+        isLoading.value = true;
+        hasError.value = false;
+        errorMessage.value = '';
 
-        const res = await getPageEquipmentGroupByTankByFlowCode(flowCode)
+        const res = await getPageEquipmentGroupByTankByFlowCode(flowCode.value);
 
         deviceConfigGroup.value = Object.fromEntries(
             Object.entries(res.data || {}).filter(([_, equipments]) => equipments?.length > 0)
-        )
-        // console.log('设备配置分组:', deviceConfigGroup.value)
-        isEmptyData.value = Object.keys(deviceConfigGroup.value).length === 0
+        );
+        isEmptyData.value = Object.keys(deviceConfigGroup.value).length === 0;
 
-        pageParams.value = generatePageParams(deviceConfigGroup.value)
-        await updatePageConfig()
+        pageParams.value = generatePageParams(deviceConfigGroup.value);
+        // console.log('页面参数:', pageParams.value);
+        await updatePageConfig();
     } catch (err) {
-        hasError.value = true
-        errorMessage.value = err.message || '加载设备分组失败'
-        console.error('加载页面设备分组失败', err)
+        hasError.value = true;
+        errorMessage.value = err.message || '加载设备分组失败';
+        console.error('加载页面设备分组失败', err);
     } finally {
-        isLoading.value = false
+        isLoading.value = false;
     }
 }
 
 async function updatePageConfig() {
     try {
-        // console.log('更新页面配置:', flowCode, pageParams.value)
-        await updateZTPageConfig(flowCode, pageParams.value)
+        await updateZTPageConfig(flowCode.value, pageParams.value);
 
         // 先取消可能存在的订阅,避免重复订阅
-        stompClient.unsubscribeFromPage(flowCode)
+        stompClient.unsubscribeFromPage(flowCode.value);
 
-        stompClient.subscribeToPage(flowCode, data => {
-            // console.log('收到页面实时数据推送:', data)
+        stompClient.subscribeToPage(flowCode.value, data => {
             if (data && Object.keys(data).length > 0) {
                 try {
                     // 按key逐个更新,避免整体替换导致页面闪烁
                     for (const key in data) {
-                        deviceDataGroup.value[key] = data[key] || []
+                        deviceDataGroup.value[key] = data[key] || [];
                     }
                 } catch (updateErr) {
-                    console.error('更新设备数据失败:', updateErr)
+                    console.error('更新设备数据失败:', updateErr);
                 }
             }
-        })
+        });
     } catch (err) {
-        console.error('页面配置失败:', err)
+        console.error('页面配置失败:', err);
         // 配置失败也可以继续显示已有数据
     }
 }
+
 /**
  * 根据设备类型编码判断设备类型并返回对应组件
- * @param {number|string} equipmentType - 设备类型编码(1、5为阀门,2为泵,4为传感器)
- * @returns {Component} 对应的设备组件
  */
 function getComponentByType(equipmentType) {
-    // 处理空值或无效类型情况
     if (equipmentType === undefined || equipmentType === null) {
-        console.error('设备类型编码不能为空')
-        return 'div'
+        console.error('设备类型编码不能为空');
+        return 'div';
     }
 
-    // 将输入转换为数字类型(兼容字符串形式的数字,如"1")
-    const type = Number(equipmentType)
+    const type = Number(equipmentType);
 
-    // 校验是否为有效数字
     if (isNaN(type)) {
-        console.error('无效的设备类型编码:', equipmentType)
-        return 'div'
+        console.error('无效的设备类型编码:', equipmentType);
+        return 'div';
     }
 
-    // 根据类型编码返回对应组件
     if (type === 1 || type === 5) {
-        // console.log(`设备类型编码${type}判定为阀门`)
-        return ValveControlComponent
+        return ValveControlComponent;
     } else if (type === 2) {
-        // console.log(`设备类型编码${type}判定为泵`)
-        return PumpControlComponent
+        return PumpControlComponent;
     } else if (type === 4) {
-        // console.log(`设备类型编码${type}判定为传感器`)
-        return SensorControl
+        return SensorControl;
     } else {
-        // 未知类型
-        console.warn(`设备类型编码${type}无法识别,使用默认div`)
-        return 'div'
+        console.warn(`设备类型编码${type}无法识别,使用默认div`);
+        return 'div';
     }
 }
 
 function getValueByCode(tankName, code) {
-    const dataArr = deviceDataGroup.value[tankName] || []
-    const target = dataArr.find(item => item.code === code)
-    return target ? target.value : []
+    const dataArr = deviceDataGroup.value[tankName] || [];
+    const target = dataArr.find(item => item.code === code);
+    return target ? target.value : [];
+}
+
+// 导航功能:滚动到指定罐体
+function scrollToTank(tank, index) {
+    currentNavIndex.value = index;
+    const elementId = `tank-${tank.tankName}`;
+
+    const element = document.getElementById(elementId);
+    if (element) {
+        // 计算滚动位置时考虑固定头部的高度
+        window.scrollTo({
+            top: element.offsetTop - headerHeight.value - 20,  // 顶部留一些间距
+            behavior: 'smooth'
+        });
+    }
+}
+
+// 处理滚动位置变化事件
+function handleScrollPositionChange(index) {
+    currentNavIndex.value = index;
+}
+
+// 缩短罐体名称用于导航显示
+function shortenTankName(fullName) {
+    if (fullName.length > 18) {
+        return fullName.substring(0, 16) + '...';
+    }
+    return fullName;
 }
 
 onMounted(() => {
-    console.log('组件挂载')
-    loadInitialData()
-})
+    console.log('组件挂载');
+    // 计算头部高度
+    calculateHeaderHeight();
+    // 监听窗口大小变化,重新计算头部高度
+    window.addEventListener('resize', calculateHeaderHeight);
+    loadInitialData();
+});
 
 onBeforeUnmount(() => {
-    console.log('组件卸载,取消订阅')
-    stompClient.unsubscribeFromPage(flowCode)
-})
+    console.log('组件卸载,取消订阅');
+    stompClient.unsubscribeFromPage(flowCode.value);
+    window.removeEventListener('resize', calculateHeaderHeight);
+});
 </script>
 
 <style scoped lang="scss">
 .dcs_tanks {
     width: 100%;
     min-height: 100vh;
-    padding: 24px 32px;
     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;
-    flex-direction: column;
-    align-items: center;
+    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%;
-    max-width: 1400px;
-    margin-top: 20px;
 }
 
-// 每个罐体占一行
-.tank_row {
+// 每个罐体区域
+.tank_section {
     background: #1a1a1a;
     border-radius: 12px;
-    padding: 16px 20px;
+    padding: 20px 24px;
     width: 100%;
     display: flex;
     flex-direction: column;
-    gap: 16px;
+    gap: 20px;
+    box-sizing: border-box;
+    scroll-margin-top: 160px;
+    /* 用于滚动定位时预留空间 */
 }
 
 .tank_title {
     font-size: 20px;
     color: #fff;
-    padding-bottom: 8px;
+    padding-bottom: 12px;
     border-bottom: 1px solid rgba(255, 255, 255, 0.1);
     text-align: center;
+    margin: 0;
 }
 
-// 设备列表改为横向排列,可滚动
-.equipment_list {
-    display: flex;
-    gap: 16px;
-    overflow-x: auto;
-    padding: 8px 0;
-    min-height: 120px;
-    align-items: center;
-    /* 确保有足够高度显示设备 */
+// 设备网格布局
+.equipment_grid {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    /* 默认4列布局 */
+    gap: 24px;
+    /* 增大间距避免拥挤 */
+    padding: 12px 0;
 }
 
-/* 滚动条美化 */
-.equipment_list::-webkit-scrollbar {
-    height: 6px;
+// 统一设备项样式
+.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;
 }
 
-.equipment_list::-webkit-scrollbar-track {
-    background: rgba(255, 255, 255, 0.05);
-    border-radius: 3px;
+/* 确保子组件正确填充容器 */
+: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;
+    }
 }
 
-.equipment_list::-webkit-scrollbar-thumb {
-    background: rgba(255, 255, 255, 0.2);
-    border-radius: 3px;
+/* 无数据提示样式 */
+.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,
-.no-data {
+.error {
     color: white;
     margin-top: 20px;
     padding: 20px;
     border-radius: 8px;
     background-color: rgba(255, 255, 255, 0.1);
     width: 100%;
-    max-width: 1400px;
     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;
+        }
+    }
 }
 
-.error button {
-    margin-top: 10px;
-    padding: 8px 16px;
-    background-color: #00C851;
-    color: white;
-    border: none;
-    border-radius: 4px;
-    cursor: pointer;
-    transition: background-color 0.2s;
+/* 响应式布局 */
+@media (max-width: 1400px) {
+    .equipment_grid {
+        grid-template-columns: repeat(3, 1fr);
+        gap: 20px;
+    }
 }
 
-.error button:hover {
-    background-color: #007E33;
+@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>