Преглед на файлове

feat(flow):获取指定归属的流程导航

HMY преди 8 месеца
родител
ревизия
7527da30a8

+ 11 - 0
admin/src/main/java/com/dcs/hnyz/controller/FlowController.java

@@ -124,4 +124,15 @@ public class FlowController extends BaseController
     public AjaxResult changeStatus(@RequestBody Flow flow){
         return toAjax(flowService.updateFlow(flow));
     }
+
+    /**
+     * 获取指定归属的流程导航
+     * @param attribution 流程归属,如 "Na2SO4"
+     * @param pageType 页面类型,flowSelect 或 paramConfiguration
+     */
+    @GetMapping("/getFlowNav")
+    public AjaxResult getFlowNav(@RequestParam String attribution,
+                                 @RequestParam String pageType){
+        return success(flowService.getFlowNav(attribution,pageType));
+    }
 }

+ 5 - 0
admin/src/main/java/com/dcs/hnyz/domain/Flow.java

@@ -48,6 +48,11 @@ public class Flow extends BaseEntity {
     @Excel(name = "流程标识")
     @ApiModelProperty(value = "流程标识")
     private String flowCode;
+
+    @Excel(name = "流程归属")
+    @ApiModelProperty(value = "流程归属")
+    private String flowAttribution;
+
     /**
      * 启用状态(0-启用,1-禁用)
      */

+ 17 - 0
admin/src/main/java/com/dcs/hnyz/domain/vo/FlowNavVO.java

@@ -0,0 +1,17 @@
+package com.dcs.hnyz.domain.vo;
+
+import lombok.Data;
+
+/**
+ * FlowNavVO
+ *
+ * @author: hmy
+ * @date: 2025/8/18 14:26
+ */
+@Data
+public class FlowNavVO {
+    private String label; // 前端显示名称
+    private String code;  // 用于匹配 currentCode
+    private String path;  // 前端路由路径
+}
+

+ 9 - 0
admin/src/main/java/com/dcs/hnyz/service/IFlowService.java

@@ -1,6 +1,7 @@
 package com.dcs.hnyz.service;
 
 import com.dcs.hnyz.domain.Flow;
+import com.dcs.hnyz.domain.vo.FlowNavVO;
 
 import java.util.List;
 import java.util.Map;
@@ -87,4 +88,12 @@ public interface IFlowService
      * 根据流程编码获取流程
      * */
     Flow getFlowByCode(String flowCode);
+
+    /**
+     * 获取流程导航
+     * @param attribution
+     * @param pageType
+     * @return
+     */
+    List<FlowNavVO> getFlowNav(String attribution, String pageType);
 }

+ 18 - 0
admin/src/main/java/com/dcs/hnyz/service/impl/FlowServiceImpl.java

@@ -5,6 +5,7 @@ import com.dcs.common.enums.GeneralStatus;
 import com.dcs.hnyz.domain.Equipment;
 import com.dcs.hnyz.domain.EquipmentParam;
 import com.dcs.hnyz.domain.Flow;
+import com.dcs.hnyz.domain.vo.FlowNavVO;
 import com.dcs.hnyz.mapper.FlowMapper;
 import com.dcs.hnyz.service.IEquipmentService;
 import com.dcs.hnyz.service.IFlowService;
@@ -161,4 +162,21 @@ public class FlowServiceImpl implements IFlowService
         wrapper.eq(Flow::getFlowCode,flowCode);
         return flowMapper.selectOne(wrapper);
     }
+
+    @Override
+    public List<FlowNavVO> getFlowNav(String attribution, String pageType) {
+        LambdaQueryWrapper<Flow> wrapper=new LambdaQueryWrapper<>();
+        wrapper.eq(Flow::getFlowAttribution,attribution);
+        wrapper.eq(Flow::getStatus,GeneralStatus.ENABLE.getCode());
+        wrapper.orderByAsc(Flow::getSort);
+        List<Flow> flows = flowMapper.selectList(wrapper);
+        return flows.stream().map(f -> {
+            FlowNavVO vo = new FlowNavVO();
+            vo.setLabel(f.getFlowName());
+            vo.setCode(f.getFlowCode());
+            vo.setPath("/controlPage/" + pageType + "/" + f.getFlowCode());
+            return vo;
+        }).collect(Collectors.toList());
+    }
+
 }

+ 357 - 0
ui/src/views/controlPage/flowSelect/FlowControlPage.vue

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

+ 346 - 0
ui/src/views/controlPage/paramConfiguration/ParamConfigPage.vue

@@ -0,0 +1,346 @@
+<template>
+    <div class="param_config_page">
+        <!-- 固定头部 -->
+        <div class="fixed_header" ref="fixedHeader">
+            <HeaderComponent title="硫酸钠 - 参数配置" backTo="/controlPage/flowSelect" />
+            <PageNav :items="paramItems_Na2SO4" :currentCode="pageCode" @navClick="onNavClick" />
+        </div>
+
+        <div class="main_container">
+            <div class="main_content">
+                <div class="content_area">
+                    <!-- 加载状态 -->
+                    <div v-if="isLoading" class="loading">正在加载参数配置...</div>
+
+                    <!-- 错误状态 -->
+                    <div v-if="hasError" class="error">
+                        <p>加载配置失败: {{ errorMessage }}</p>
+                        <button @click="loadConfigData">重试</button>
+                    </div>
+
+                    <!-- 数据展示 -->
+                    <div v-else class="config_container">
+                        <div v-for="(equipments, tankName) in filteredDeviceConfigGroup" :key="tankName"
+                            class="tank_section" :id="`tank_${tankName}`">
+                            <h2 class="tank_title">{{ tankName }}</h2>
+                            <div class="params_grid">
+                                <div v-if="!isInitialParamsLoaded" class="loading">加载参数中...</div>
+                                <ParamConfigComponent v-if="isInitialParamsLoaded" v-for="equipment in equipments"
+                                    :key="equipment.code" :title="equipment.title + '-' + equipment.equipmentName"
+                                    :code="equipment.code" :initial-params="getInitialParams(equipment.code)"
+                                    :id="`device_${equipment.code}`" />
+                            </div>
+                        </div>
+
+                        <div v-if="isEmptyData" class="no_data">未获取到设备配置信息</div>
+                    </div>
+                </div>
+
+                <TankNavigation :tanks="tankNavList" :current-index="currentNavIndex" :header-height="headerHeight"
+                    @tankClick="scrollToTank" @scrollPositionChange="handleScrollPositionChange" />
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import HeaderComponent from '@/components/DCS/HeaderComponent.vue'
+import PageNav from '@/components/GeneralComponents/control/PageNavComponent.vue'
+import ParamConfigComponent from '@/components/GeneralComponents/control/paramConfigComponent.vue'
+import TankNavigation from '@/components/GeneralComponents/control/TankNavigationComponent.vue'
+import { getPageEquipmentGroupByTankByFlowCode } from '@/api/hnyz/equipment'
+import { getAllParamConfigDataByCodeList } from '@/api/hnyz/param'
+import { paramItems_Na2SO4 } from '@/config'
+
+const route = useRoute()
+const router = useRouter()
+
+// 动态路由 pageCode
+const pageCode = ref(route.params.pageCode)
+
+// 自动生成 flowCode: 首字母大写 + "_Control"
+const flowCode = computed(() =>pageCode.value.charAt(0).toUpperCase() + pageCode.value.slice(1))
+
+// 状态
+const isLoading = ref(true)
+const hasError = ref(false)
+const errorMessage = ref('')
+const isEmptyData = ref(false)
+const fixedHeader = ref(null)
+const headerHeight = ref(0)
+const isInitialParamsLoaded = ref(false)
+
+// 设备数据
+const deviceConfigGroup = ref({})
+const initialParamData = ref({})
+
+// 导航
+const tankNavList = ref([])
+const currentNavIndex = ref(-1)
+
+// 过滤阀门设备
+const filteredDeviceConfigGroup = computed(() => {
+    const filtered = {}
+    Object.entries(deviceConfigGroup.value).forEach(([tankName, equipments]) => {
+        const validEquipments = equipments.filter(e => ![1, 5].includes(Number(e.equipmentType)))
+        if (validEquipments.length > 0) filtered[tankName] = validEquipments
+    })
+    isEmptyData.value = Object.keys(filtered).length === 0
+    return filtered
+})
+
+const generateTankNavList = computed(() => {
+    return Object.keys(filteredDeviceConfigGroup.value).map(tankName => ({
+        uniqueId: `tank_${tankName}`,
+        tankName,
+        shortName: tankName.length > 18 ? tankName.slice(0, 17) + '...' : tankName,
+        fullName: tankName
+    }))
+})
+
+// 计算头部高度
+const calculateHeaderHeight = () => {
+    if (fixedHeader.value) headerHeight.value = fixedHeader.value.offsetHeight
+}
+
+// 页面滚动
+function handleScrollPositionChange(index) { currentNavIndex.value = index }
+function scrollToTank(tank, index) {
+    currentNavIndex.value = index
+    const element = document.getElementById(`tank_${tank.tankName}`)
+    if (element) window.scrollTo({ top: element.offsetTop - headerHeight.value - 20, behavior: 'smooth' })
+}
+
+// 点击导航栏跳转页面
+function onNavClick(item) {
+    router.push(item.path)
+}
+
+// 加载设备和参数
+async function loadConfigData() {
+    try {
+        isLoading.value = true
+        hasError.value = false
+        isInitialParamsLoaded.value = false
+
+        const res = await getPageEquipmentGroupByTankByFlowCode(flowCode.value)
+        deviceConfigGroup.value = Object.fromEntries(
+            Object.entries(res.data || {}).filter(([_, equipments]) => equipments?.length > 0)
+        )
+
+        if (Object.keys(filteredDeviceConfigGroup.value).length > 0) await loadInitialParamValues()
+        else isInitialParamsLoaded.value = true
+    } catch (err) {
+        hasError.value = true
+        errorMessage.value = err.message || '加载配置失败'
+        isInitialParamsLoaded.value = true
+    } finally { isLoading.value = false }
+}
+
+async function loadInitialParamValues() {
+    try {
+        const allCodes = []
+        Object.values(filteredDeviceConfigGroup.value).forEach(arr => arr.forEach(e => allCodes.push(e.code)))
+        const res = await getAllParamConfigDataByCodeList(allCodes)
+        initialParamData.value = res.data || {}
+    } catch (err) {
+        initialParamData.value = {}
+    } finally { isInitialParamsLoaded.value = true }
+}
+
+function getInitialParams(code) {
+    const paramCount = getParamCountByCode(code)
+    return initialParamData.value[code] || new Array(paramCount).fill(0)
+}
+
+function getParamCountByCode(code) {
+    const type = getDeviceTypeByCode(code)
+    return paramTemplates[type] || 0
+}
+
+function getDeviceTypeByCode(code) {
+    const upperType = code.toUpperCase()
+    if (upperType.startsWith('PH')) return 'ph'
+    if (upperType.startsWith('PT')) return 'pressuregauge'
+    if (upperType.startsWith('TT')) return 'thermometer'
+    if (upperType.startsWith('FIT')) return 'flowmeter'
+    if (upperType.startsWith('P') || upperType.startsWith('M')) return 'pump'
+    if (upperType.startsWith('LT')) return 'levelmeter'
+    return 'unknown'
+}
+
+const paramTemplates = { pump: 3, flowmeter: 5, thermometer: 2, ph: 3, levelmeter: 5, pressuregauge: 3, unknown: 0 }
+
+// 监听路由参数变化,重新加载页面数据
+watch(
+    () => route.params.pageCode,
+    (newCode) => {
+        pageCode.value = newCode
+        loadConfigData()
+    }
+)
+
+onMounted(() => {
+    calculateHeaderHeight()
+    window.addEventListener('resize', calculateHeaderHeight)
+    loadConfigData()
+
+    watch(
+        () => filteredDeviceConfigGroup.value,
+        () => {
+            tankNavList.value = generateTankNavList.value
+            currentNavIndex.value = -1
+        }
+    )
+})
+
+onBeforeUnmount(() => { window.removeEventListener('resize', calculateHeaderHeight) })
+</script>
+
+<style scoped>
+/* 样式保持原先完整版本即可 */
+.param_config_page {
+    width: 100%;
+    min-height: 100vh;
+    padding: 0;
+    background-color: #252525;
+}
+
+.fixed_header {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 1000;
+    background-color: #252525;
+    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
+}
+
+.main_container {
+    padding: 140px 20px 24px;
+    box-sizing: border-box;
+}
+
+.main_content {
+    display: flex;
+    gap: 20px;
+    max-width: 1700px;
+    margin: 0 auto;
+}
+
+.content_area {
+    flex: 1;
+    min-width: 1350px;
+}
+
+.config_container {
+    width: 100%;
+    padding: 20px 0;
+}
+
+.tank_section {
+    margin-bottom: 30px;
+    background: #1a1a1a;
+    border-radius: 8px;
+    padding: 20px;
+    border: 1px solid #72E0FF;
+    scroll-margin-top: 160px;
+}
+
+.tank_title {
+    margin: 0 0 20px 0;
+    padding-bottom: 10px;
+    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+    font-size: 18px;
+    color: #76E1FF;
+}
+
+.params_grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
+    gap: 20px;
+}
+
+.loading,
+.error,
+.no_data {
+    width: 100%;
+    margin: 20px auto;
+    padding: 20px;
+    border-radius: 8px;
+    text-align: center;
+    background-color: rgba(255, 255, 255, 0.1);
+    color: #fff;
+}
+
+.loading {
+    color: #1890ff;
+}
+
+.error {
+    color: #f5222d;
+}
+
+.error button {
+    padding: 8px 16px;
+    background: #f5222d;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    margin-top: 10px;
+}
+
+.error button:hover {
+    background: #d91111;
+}
+
+@media (max-width:1400px) {
+    .params_grid {
+        grid-template-columns: repeat(2, 1fr);
+        gap: 20px;
+    }
+}
+
+@media (max-width:1200px) {
+    .main_container {
+        padding: 140px 15px 16px;
+    }
+
+    .main_content {
+        max-width: 100%;
+    }
+
+    .content_area {
+        min-width: auto;
+    }
+}
+
+@media (max-width:992px) {
+    .main_content {
+        flex-direction: column;
+    }
+
+    .params_grid {
+        grid-template-columns: 1fr;
+        gap: 16px;
+    }
+
+    .main_container {
+        padding: 120px 15px 16px;
+    }
+}
+
+@media (max-width:768px) {
+    .tank_section {
+        padding: 15px;
+        margin-bottom: 20px;
+    }
+
+    .params_grid {
+        gap: 14px;
+    }
+}
+</style>