瀏覽代碼

修改工程安全 大坝安全监测内容

BAI 6 天之前
父節點
當前提交
8f5474add5
共有 4 個文件被更改,包括 1100 次插入149 次删除
  1. 58 18
      README.md
  2. 364 0
      src/components/DamDeformSection.vue
  3. 586 0
      src/components/DamSeepageSection.vue
  4. 92 131
      src/views/mainPages/EngineeringSafetyView.vue

+ 58 - 18
README.md

@@ -32,7 +32,15 @@ src/
 │   ├── GradientOverlay.vue  # 四周暗角渐变装饰层
 │   ├── DataCard.vue         # 数据卡片组件
 │   ├── ReservoirChart.vue   # 水库图表组件
-│   └── ...                  # 其他辅助组件
+│   ├── SidebarButtons.vue   # 侧边栏快捷按钮
+│   ├── DeviceInfoPanel.vue  # 设备信息面板
+│   ├── DeviceHealthPanel.vue# 设备健康度面板
+│   ├── DeviceRightPanel.vue # 设备右侧详情面板
+│   ├── DeviceArchivePanel.vue # 设备档案面板
+│   ├── TempStressCard.vue   # 温度应力卡片
+│   ├── HydrologyForecastPanel.vue # 水文预报面板
+│   ├── DamDeformSection.vue # 坝体形变监测可视化(位移分布 + 测点数据)
+│   └── DamSeepageSection.vue# 坝体渗压监测可视化(浸润线 + 渗压计数据)
 ├── views/
 │   ├── HomeView.vue         # 主容器页面:前台大屏入口
@@ -44,7 +52,7 @@ src/
 │   │   ├── OverviewView.vue           # 流域总览
 │   │   ├── HydrologyForecastView.vue  # 水文四预
 │   │   ├── GateControlView.vue        # 闸门控制(大屏版)
-│   │   ├── EngineeringSafetyView.vue  # 工程安全运维
+│   │   ├── EngineeringSafetyView.vue  # 工程安全运维(含形变/渗压监测、设备安全面板)
 │   │   ├── LifecycleView.vue          # 全生命周期管理
 │   │   ├── VideoMonitorView.vue       # 视频监控
 │   │   ├── InspectionView.vue         # 管线监测
@@ -54,6 +62,7 @@ src/
 │   └── admin/               # 【后台工作台】页面
 │       ├── DeviceMaintainView.vue          # 设备运维管理
 │       ├── PatrolPlanView.vue              # 巡检计划
+│       ├── PatrolPlanCreateView.vue        # 巡检计划新建
 │       ├── PatrolRecordView.vue            # 巡检记录
 │       ├── PatrolHiddenView.vue            # 隐患台账
 │       ├── WaterRainView.vue               # 水雨情实时监测
@@ -62,7 +71,16 @@ src/
 │       ├── HydroForecastView.vue           # 水情预报
 │       ├── WaterResourceAllocationView.vue # 水资源调度(7 个功能标签页)
 │       ├── GateControlAdminView.vue        # 闸门运行控制
-│       └── BenefitSummaryView.vue          # 工程效益
+│       ├── BenefitSummaryView.vue          # 工程效益
+│       ├── VideoLiveView.vue               # 视频监控 - 实时画面
+│       ├── VideoDeviceView.vue             # 视频监控 - 设备管理
+│       ├── VideoCaptureView.vue            # 视频监控 - 抓拍记录
+│       ├── ArchiveFilesView.vue            # 资料档案 - 工程档案
+│       ├── ArchiveOrdersView.vue           # 资料档案 - 调令文件
+│       ├── ArchiveLifecycleView.vue        # 资料档案 - 全生命周期
+│       ├── SysRoleView.vue                 # 系统权限 - 角色管理
+│       ├── SysUserView.vue                 # 系统权限 - 用户账号
+│       └── SysLogView.vue                  # 系统权限 - 操作日志
 └── assets/                  # 静态资源
     ├── AlibabaPuHuiTi-3/    # 阿里普惠体字库
@@ -91,7 +109,6 @@ src/
 
 ```
 ├── 首页
-├── 视频监控
 ├── 日常巡检
 │   ├── 巡检计划
 │   ├── 巡检记录
@@ -100,22 +117,32 @@ src/
 ├── 闸门控制
 ├── 水雨情监测
 │   ├── 实时监测
-│   ├── 水情地图
-│   ├── 历史数据
-│   └── 水情预报
+│   └── 历史数据
 ├── 水资源调度
-│   ├── 调度总览
-│   ├── 灌溉供水
-│   ├── 人畜饮水
-│   ├── 工业供水
-│   ├── 生态补水
-│   ├── 调度计划
-│   └── 供水网络
+│   ├── 供水水量
+│   └── 生态补水
 ├── 工程效益
 ├── 资料档案
+│   ├── 工程档案
+│   ├── 调令文件
+│   └── 全生命周期
 └── 系统权限
+    ├── 角色管理
+    ├── 用户账号
+    └── 操作日志
 ```
 
+### 工程安全运维页面(EngineeringSafetyView)
+
+工程安全运维页面采用 **左侧面板 + 右侧面板** 布局:
+
+- **左侧面板**:安全状态评估、巡检记录、坝体安全监测(形变/渗压 tab 切换)
+- **右侧面板**:设备安全(设备完好率仪表盘、阈值报警、设备清单树)
+
+坝体安全监测包含两个子组件:
+- **DamDeformSection** — 坝体形变监测:位移分布图 + 箭头向量 + 测点数据表格
+- **DamSeepageSection** — 坝体渗压监测:浸润线(实测/设计/警戒)+ 渗压计点位 + 数据表格
+
 ### 核心依赖说明
 
 | 依赖 | 版本 | 说明 |
@@ -139,7 +166,20 @@ npm run dev
 
 ### 设计规范
 
-- 前台大屏:深色科技风(蓝黑色调 + 渐变发光文字 + Cesium 三维底图)
-- 后台工作台:轻量化蓝白玻璃拟态风(白色半透明背景 + backdrop-filter 模糊)
-- 自适应:autofit.js(设计稿 1920×1080)
-- 字体:阿里巴巴普惠体(Alibaba PuHuiTi-3)
+- **前台大屏**:深色科技风(蓝黑色调 + 渐变发光文字 + Cesium 三维底图)
+  - 组件背景: `rgba(0, 20, 40, 0.5)` 半透明深蓝
+  - 边框光效: `rgba(0, 212, 255, 0.12)` 青色发光描边
+  - 文字颜色: `#e0fcff`(主)、`rgba(123, 190, 246, 0.7)`(辅)
+  - Canvas 图表: 深色网格 + 渐变填充 + 发光线条
+- **后台工作台**:轻量化蓝白玻璃拟态风(白色半透明背景 + backdrop-filter 模糊)
+- **自适应**:autofit.js(设计稿 1920×1080)
+- **字体**:阿里巴巴普惠体(Alibaba PuHuiTi-3)
+
+### 大坝渗流监测单位说明
+
+大坝渗流监测(渗压计)中,**库水位、设计水位、渗压计实测值等统一使用 `m`(米)为单位,这符合水利行业专业标准**。
+
+- 渗压计测量的是渗透(孔隙)水压力,工程上通常将压力值换算为**等效水头高度(米)** 进行记录和展示
+- 换算依据:**1 米水柱 ≈ 9.81 kPa**
+- 行业规范中,水位高程计算公式直接以米为单位输出
+- 测压管读数需换算为等效水头,以米为单位记录(参照《混凝土坝安全监测技术规范》)

+ 364 - 0
src/components/DamDeformSection.vue

@@ -0,0 +1,364 @@
+<!-- 坝体形变监测可视化:位移分布 + 测点数据 -->
+<template>
+  <div ref="sectionRef" class="dam-deform-section">
+    <!-- 指标行 -->
+    <div class="stats-row">
+      <div class="stat-item">
+        <span class="stat-label">最大水平位移</span>
+        <span class="stat-value" style="color:#62f6fb;">12.6</span>
+        <span class="stat-unit">mm</span>
+      </div>
+      <div class="stat-divider"></div>
+      <div class="stat-item">
+        <span class="stat-label">最大沉降</span>
+        <span class="stat-value" style="color:#fbbf24;">8.3</span>
+        <span class="stat-unit">mm</span>
+      </div>
+      <div class="stat-divider"></div>
+      <div class="stat-item">
+        <span class="stat-label">变形速率</span>
+        <span class="stat-value" style="color:#22c55e;">0.02</span>
+        <span class="stat-unit">mm/d</span>
+      </div>
+    </div>
+
+    <!-- Canvas 绘制区 -->
+    <div class="canvas-wrapper">
+      <canvas ref="canvasRef" :width="cw" :height="ch"></canvas>
+    </div>
+
+    <!-- 图例 -->
+    <div class="legend-row">
+      <div class="legend-item">
+        <span class="legend-line legend-line-vector"></span>
+        <span class="legend-label">位移向量(mm)</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-dot legend-dot-point"></span>
+        <span class="legend-label">测点</span>
+      </div>
+    </div>
+
+    <!-- 形变测点数据表格 -->
+    <div class="gauge-table">
+      <table>
+        <colgroup>
+          <col style="width:38px" />
+          <col style="width:auto" />
+          <col style="width:62px" />
+          <col style="width:62px" />
+          <col style="width:62px" />
+          <col style="width:44px" />
+        </colgroup>
+        <thead>
+          <tr>
+            <th>编号</th>
+            <th>位置</th>
+            <th>水平(mm)</th>
+            <th>沉降(mm)</th>
+            <th>合计(mm)</th>
+            <th>状态</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="p in points" :key="p.id" :class="p.status">
+            <td>{{ p.id }}</td>
+            <td class="td-left">{{ p.pos }}</td>
+            <td>{{ p.horiz.toFixed(1) }}</td>
+            <td>{{ p.vert.toFixed(1) }}</td>
+            <td>{{ p.total.toFixed(1) }}</td>
+            <td>
+              <span :class="['status-tag', p.status]">
+                {{ { normal: '正常', warn: '注意', danger: '超限' }[p.status] }}
+              </span>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'DamDeformSection',
+  data() {
+    return {
+      cw: 320,
+      ch: 150,
+      animFrame: null,
+      time: 0,
+      scale: 1,
+      points: [
+        { id: 'D01', pos: '坝顶上游', xRatio: 0.15, horiz: 12.6, vert: 8.3, total: 15.1, normH: 15, normV: 10, status: 'normal' },
+        { id: 'D02', pos: '坝顶中部', xRatio: 0.30, horiz: 10.2, vert: 6.5, total: 12.1, normH: 15, normV: 10, status: 'normal' },
+        { id: 'D03', pos: '坝顶下游', xRatio: 0.45, horiz: 8.5, vert: 5.2, total: 10.0, normH: 15, normV: 10, status: 'normal' },
+        { id: 'D04', pos: '坝基上游', xRatio: 0.60, horiz: 4.2, vert: 3.8, total: 5.7, normH: 10, normV: 8, status: 'normal' },
+        { id: 'D05', pos: '坝基下游', xRatio: 0.75, horiz: 2.1, vert: 1.5, total: 2.6, normH: 10, normV: 8, status: 'normal' },
+      ],
+    }
+  },
+  computed: {
+    maxDisplacement() {
+      return Math.max(...this.points.map(p => p.total))
+    },
+  },
+  mounted() {
+    this.resizeCanvas()
+    this.startAnimation()
+    window.addEventListener('resize', this.resizeCanvas)
+  },
+  beforeUnmount() {
+    if (this.animFrame) cancelAnimationFrame(this.animFrame)
+    window.removeEventListener('resize', this.resizeCanvas)
+  },
+  methods: {
+    resizeCanvas() {
+      const el = this.$refs.sectionRef
+      if (!el) return
+      const w = el.clientWidth - 12
+      if (w <= 0) return
+      const base = 320
+      this.scale = w / base
+      this.cw = Math.round(w)
+      this.ch = Math.round(150 * this.scale)
+    },
+    startAnimation() {
+      const draw = () => {
+        // 如果 canvas 还未初始化(v-show 隐藏时),每帧重试
+        if (this.cw <= 0) this.resizeCanvas()
+        this.time++
+        this.updatePoints()
+        this.drawCanvas()
+        this.animFrame = requestAnimationFrame(draw)
+      }
+      this.animFrame = requestAnimationFrame(draw)
+    },
+    updatePoints() {
+      this.points.forEach(p => {
+        const flutter = Math.sin(this.time * 0.008 + p.xRatio * 3) * 0.2
+        p.horiz = (p.horiz + flutter * 0.1)
+        p.vert = (p.vert + flutter * 0.05)
+        p.total = Math.sqrt(p.horiz * p.horiz + p.vert * p.vert)
+        if (p.horiz >= p.normH * 1.1) p.status = 'danger'
+        else if (p.horiz >= p.normH * 0.85) p.status = 'warn'
+        else p.status = 'normal'
+      })
+    },
+    // s = scale factor for font sizes
+    drawCanvas() {
+      const canvas = this.$refs.canvasRef
+      if (!canvas) return
+      const ctx = canvas.getContext('2d')
+      const W = this.cw
+      const H = this.ch
+      const s = this.scale
+      const pad = { left: 45 * s, right: 10 * s, top: 12 * s, bottom: 20 * s }
+      const drawW = W - pad.left - pad.right
+      const drawH = H - pad.top - pad.bottom
+
+      ctx.clearRect(0, 0, W, H)
+
+      // 网格
+      ctx.save()
+      ctx.strokeStyle = 'rgba(0, 212, 255, 0.06)'
+      ctx.lineWidth = 0.5
+      for (let i = 0; i <= 4; i++) {
+        const y = pad.top + (i / 4) * drawH
+        ctx.beginPath()
+        ctx.moveTo(pad.left, y)
+        ctx.lineTo(W - pad.right, y)
+        ctx.stroke()
+      }
+      ctx.restore()
+
+      // 坝体轮廓
+      const damLeft = pad.left + drawW * 0.08
+      const damRight = pad.left + drawW * 0.82
+      const damTop = pad.top + drawH * 0.10
+      const damBottom = pad.top + drawH
+
+      ctx.save()
+      ctx.beginPath()
+      ctx.moveTo(damLeft, damTop)
+      ctx.lineTo(damRight, damTop)
+      ctx.lineTo(damRight + drawW * 0.04, damBottom)
+      ctx.lineTo(damLeft - drawW * 0.02, damBottom)
+      ctx.closePath()
+      ctx.fillStyle = 'rgba(30, 64, 175, 0.2)'
+      ctx.fill()
+      ctx.strokeStyle = 'rgba(0, 212, 255, 0.25)'
+      ctx.lineWidth = 1.2
+      ctx.stroke()
+
+      // 分层线
+      ctx.save()
+      ctx.beginPath()
+      ctx.rect(damLeft, damTop, damRight - damLeft + drawW * 0.04, damBottom - damTop)
+      ctx.clip()
+      ctx.strokeStyle = 'rgba(0, 212, 255, 0.03)'
+      ctx.lineWidth = 0.5
+      for (let i = 0; i < 12; i++) {
+        const y = damTop + (i / 12) * (damBottom - damTop)
+        ctx.beginPath()
+        ctx.moveTo(damLeft, y)
+        ctx.lineTo(damRight + drawW * 0.04, y)
+        ctx.stroke()
+      }
+      ctx.restore()
+      ctx.restore()
+
+      // 坝顶标注
+      const fs = 13 * s  // base font size ~13px
+      ctx.save()
+      ctx.fillStyle = 'rgba(123, 190, 246, 0.5)'
+      ctx.font = `${fs}px sans-serif`
+      ctx.textAlign = 'center'
+      ctx.fillText('坝顶', (damLeft + damRight) / 2, damTop - 5 * s)
+
+      // 上游(左侧竖排)
+      ctx.save()
+      ctx.translate(damLeft - 10 * s, (damTop + damBottom) / 2)
+      ctx.rotate(-Math.PI / 2)
+      ctx.textAlign = 'center'
+      ctx.font = `${fs}px sans-serif`
+      ctx.fillText('上游', 0, 0)
+      ctx.restore()
+
+      // 下游(右侧竖排)
+      ctx.save()
+      ctx.translate(damRight + 10 * s, (damTop + damBottom) / 2)
+      ctx.rotate(Math.PI / 2)
+      ctx.textAlign = 'center'
+      ctx.font = `${fs}px sans-serif`
+      ctx.fillText('下游', 0, 0)
+      ctx.restore()
+
+      ctx.restore()
+
+      // 测点 + 位移箭头
+      this.points.forEach((p, idx) => {
+        const px = pad.left + (p.xRatio / 1.0) * drawW
+        const py = damTop + (idx / (this.points.length - 1)) * (damBottom - damTop) * 0.65 + damTop * 0.3
+        const dx = p.horiz * 0.8 * s
+        const dy = p.vert * 0.8 * s
+
+        // 原始位置到当前位置的虚线
+        ctx.save()
+        ctx.setLineDash([2, 2])
+        ctx.strokeStyle = 'rgba(123, 190, 246, 0.2)'
+        ctx.lineWidth = 1
+        ctx.beginPath()
+        ctx.moveTo(px, py)
+        ctx.lineTo(px + dx, py + dy)
+        ctx.stroke()
+        ctx.restore()
+
+        // 位移箭头
+        ctx.save()
+        const color = p.status === 'danger' ? '#ef4444' : p.status === 'warn' ? '#fbbf24' : '#62f6fb'
+        const endX = px + dx
+        const endY = py + dy
+        ctx.strokeStyle = color
+        ctx.lineWidth = 2
+        ctx.shadowColor = color
+        ctx.shadowBlur = 4
+        ctx.beginPath()
+        ctx.moveTo(px, py)
+        ctx.lineTo(endX, endY)
+        ctx.stroke()
+        const angle = Math.atan2(dy, dx)
+        ctx.beginPath()
+        ctx.moveTo(endX, endY)
+        ctx.lineTo(endX - 4 * Math.cos(angle - 0.4), endY - 4 * Math.sin(angle - 0.4))
+        ctx.moveTo(endX, endY)
+        ctx.lineTo(endX - 4 * Math.cos(angle + 0.4), endY - 4 * Math.sin(angle + 0.4))
+        ctx.stroke()
+        ctx.restore()
+
+        // 测点圆点
+        ctx.save()
+        ctx.beginPath()
+        ctx.arc(px, py, 4, 0, Math.PI * 2)
+        ctx.fillStyle = color
+        ctx.fill()
+        ctx.strokeStyle = 'rgba(0,30,60,0.8)'
+        ctx.lineWidth = 2
+        ctx.stroke()
+        ctx.restore()
+
+        // 编号和数值标签
+        ctx.save()
+        ctx.fillStyle = '#e0fcff'
+        ctx.font = `${fs}px sans-serif`
+        ctx.textAlign = 'center'
+        ctx.fillText(p.id, px, py - 10 * s)
+        ctx.fillStyle = 'rgba(123, 190, 246, 0.6)'
+        ctx.font = `${fs - 1}px sans-serif`
+        ctx.fillText(p.total.toFixed(1), px + dx + 2 * s, py + dy + 18 * s)
+        ctx.restore()
+      })
+
+      // 图例(已移到 HTML 中)
+    },
+  }
+}
+</script>
+
+<style scoped>
+.dam-deform-section {
+  background: rgba(0, 20, 40, 0.5);
+  border-radius: 4px;
+  border: 1px solid rgba(0, 212, 255, 0.12);
+  padding: 4px;
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+}
+
+.canvas-wrapper { width: 100%; }
+.canvas-wrapper canvas { display: block; width: 100%; height: auto; }
+
+/* 图例 */
+.legend-row { display: flex; justify-content: center; gap: 24px; padding: 0 8px; flex-wrap: wrap; }
+.legend-item { display: flex; align-items: center; gap: 4px; }
+.legend-line { width: 22px; height: 3px; border-radius: 1px; }
+.legend-line-vector { background: #62f6fb; }
+.legend-dot { width: 8px; height: 8px; border-radius: 50%; }
+.legend-dot-point { background: #62f6fb; }
+.legend-label { font-size: 12px; color: rgba(123, 190, 246, 0.8); }
+
+/* 指标行 */
+.stats-row { display: flex; align-items: center; gap: 0; margin-bottom: 6px; }
+.stat-item {
+  flex: 1; display: flex; align-items: center; justify-content: center; gap: 3px;
+  background: rgba(0,20,40,0.5); border: 1px solid rgba(0,212,255,0.1);
+  border-radius: 3px; padding: 3px 6px;
+}
+.stat-divider { width: 4px; flex-shrink: 0; }
+.stat-label { font-size: 11px; color: rgba(123,190,246,0.7); }
+.stat-value { font-size: 16px; font-weight: bold; min-width: 24px; text-align: right; }
+.stat-unit { font-size: 11px; color: rgba(123,190,246,0.5); }
+
+
+
+/* 表格 */
+.gauge-table { width: 100%; overflow-x: auto; }
+.gauge-table table { width: 100%; border-collapse: collapse; font-size: 10px; }
+.gauge-table th {
+  color: rgba(123,190,246,0.8); background: rgba(0,20,40,0.4);
+  padding: 3px 6px; text-align: center; border-bottom: 1px solid rgba(0,212,255,0.1);
+  font-weight: normal; font-size: 11px;
+}
+.gauge-table td {
+  color: #e0fcff; padding: 2px 6px; text-align: center;
+  border-bottom: 1px solid rgba(0,212,255,0.05); font-size: 11px; line-height: 1.8;
+}
+.gauge-table td.td-left { text-align: left; white-space: nowrap; }
+.gauge-table tr.danger td { background: rgba(239,68,68,0.08); }
+.gauge-table tr.warn td { background: rgba(251,191,36,0.06); }
+.status-tag { font-size: 10px; padding: 1px 6px; border-radius: 2px; }
+.status-tag.normal { color: #22c55e; background: rgba(34,197,94,0.15); }
+.status-tag.warn { color: #fbbf24; background: rgba(251,191,36,0.15); }
+.status-tag.danger { color: #ef4444; background: rgba(239,68,68,0.15); }
+</style>

+ 586 - 0
src/components/DamSeepageSection.vue

@@ -0,0 +1,586 @@
+<!-- 坝体断面渗压可视化:浸润线抛物线 + 实际水位浮动 -->
+<template>
+  <div ref="sectionRef" class="dam-seepage-section">
+    <!-- 阈值图例 -->
+    <div class="legend-row">
+      <div class="legend-item">
+        <span class="legend-line legend-line-measured"></span>
+        <span class="legend-label">实测线</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-line legend-line-design"></span>
+        <span class="legend-label">设计线</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-line legend-line-warning"></span>
+        <span class="legend-label">警戒线</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-dot legend-dot-gauge"></span>
+        <span class="legend-label">渗压计</span>
+      </div>
+    </div>
+
+    <!-- 指标行 -->
+    <div class="stats-row">
+      <div class="stat-item">
+        <span class="stat-label">库水位</span>
+        <span class="stat-value" style="color:#00d4ff;">{{ currentWaterLevel.toFixed(1) }}</span>
+        <span class="stat-unit">m</span>
+      </div>
+      <div class="stat-divider"></div>
+      <div class="stat-item">
+        <span class="stat-label">设计水位</span>
+        <span class="stat-value" style="color:#fbbf24;">{{ designWaterLevel }}</span>
+        <span class="stat-unit">m</span>
+      </div>
+      <div class="stat-divider"></div>
+      <div class="stat-item">
+        <span class="stat-label">实测渗压</span>
+        <span class="stat-value" :style="{ color: phreaticStatusColor }">{{ currentPhreaticRatio.toFixed(0) }}%</span>
+        <span class="stat-unit">阈值</span>
+      </div>
+    </div>
+
+    <!-- Canvas 绘制区 -->
+    <div class="canvas-wrapper">
+      <canvas ref="canvasRef" :width="cw" :height="ch"></canvas>
+    </div>
+
+    <!-- 渗压计数据表格 -->
+    <div class="gauge-table">
+      <table>
+        <colgroup>
+          <col style="width:38px" />
+          <col style="width:auto" />
+          <col style="width:68px" />
+          <col style="width:68px" />
+          <col style="width:68px" />
+          <col style="width:44px" />
+        </colgroup>
+        <thead>
+          <tr>
+            <th>编号</th>
+            <th>位置</th>
+            <th>实测(m)</th>
+            <th>设计(m)</th>
+            <th>警戒(m)</th>
+            <th>状态</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="g in gauges" :key="g.id" :class="g.status">
+            <td>{{ g.id }}</td>
+            <td class="td-left">{{ g.pos }}</td>
+            <td>{{ g.measured.toFixed(1) }}</td>
+            <td>{{ g.design }}</td>
+            <td>{{ g.warning }}</td>
+            <td>
+              <span :class="['status-tag', g.status]">
+                {{ { normal: '正常', warn: '注意', danger: '超限' }[g.status] }}
+              </span>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'DamSeepageSection',
+  data() {
+    return {
+      cw: 320,
+      ch: 150,
+      scale: 1,
+      animFrame: null,
+      time: 0,
+      // 水位参数(相对坝基高程,单位:m)
+      baseWaterLevel: 8.5,       // 基准水位
+      designWaterLevel: 10.2,    // 设计水位
+      warningWaterLevel: 11.5,   // 警戒水位
+      waveAmplitude: 0.3,        // 波动幅度
+      // 渗压计数据(沿坝体水平分布)
+      gauges: [
+        { id: 'P05', pos: '坝体上游', xRatio: 0.30, measured: 8.2, design: 8.5, warning: 9.8, status: 'normal' },
+        { id: 'P06', pos: '坝体中部', xRatio: 0.47, measured: 6.8, design: 7.0, warning: 8.5, status: 'normal' },
+        { id: 'P07', pos: '坝体中后', xRatio: 0.64, measured: 4.5, design: 5.2, warning: 6.8, status: 'normal' },
+        { id: 'P08', pos: '坝体下游', xRatio: 0.82, measured: 2.8, design: 3.5, warning: 5.0, status: 'normal' },
+      ],
+      // 浸润线抛物线控制点(基准值)
+      phreaticControlPoints: [
+        { x: 0.20, y: 8.5 },   // 起始点(上游坝面附近)
+        { x: 0.32, y: 7.2 },
+        { x: 0.47, y: 5.8 },
+        { x: 0.62, y: 4.2 },
+        { x: 0.77, y: 2.8 },
+        { x: 0.90, y: 1.5 },   // 结束点(下游坝面附近)
+      ],
+      // 设计浸润线控制点(设计值)
+      designControlPoints: [
+        { x: 0.20, y: 10.2 },
+        { x: 0.32, y: 8.5 },
+        { x: 0.47, y: 6.8 },
+        { x: 0.62, y: 5.0 },
+        { x: 0.77, y: 3.2 },
+        { x: 0.90, y: 1.8 },
+      ],
+      // 警戒浸润线控制点
+      warningControlPoints: [
+        { x: 0.20, y: 11.5 },
+        { x: 0.32, y: 9.8 },
+        { x: 0.47, y: 8.0 },
+        { x: 0.62, y: 6.2 },
+        { x: 0.77, y: 4.2 },
+        { x: 0.90, y: 2.5 },
+      ],
+    }
+  },
+  computed: {
+    currentWaterLevel() {
+      // 基准水位 + 正弦波动模拟水位浮动
+      return this.baseWaterLevel + Math.sin(this.time * 0.02) * this.waveAmplitude
+    },
+    // 当前浸润线相对设计值的比例
+    currentPhreaticRatio() {
+      const currentMaxY = Math.max(...this.phreaticControlPoints.map(p => p.y))
+      const designMaxY = Math.max(...this.designControlPoints.map(p => p.y))
+      return (currentMaxY / designMaxY) * 85 + Math.sin(this.time * 0.015) * 5
+    },
+    phreaticStatusColor() {
+      const r = this.currentPhreaticRatio
+      if (r < 85) return '#22c55e'
+      if (r < 100) return '#fbbf24'
+      return '#ef4444'
+    },
+  },
+  mounted() {
+    this.resizeCanvas()
+    this.startAnimation()
+    window.addEventListener('resize', this.resizeCanvas)
+  },
+  beforeUnmount() {
+    if (this.animFrame) cancelAnimationFrame(this.animFrame)
+    window.removeEventListener('resize', this.resizeCanvas)
+  },
+  methods: {
+    resizeCanvas() {
+      const el = this.$refs.sectionRef
+      if (!el) return
+      const w = el.clientWidth - 12
+      if (w <= 0) return
+      const base = 320
+      this.scale = w / base
+      this.cw = Math.round(w)
+      this.ch = Math.round(150 * this.scale)
+    },
+    startAnimation() {
+      const draw = () => {
+        // 如果 canvas 还未初始化(v-show 隐藏时),每帧重试
+        if (this.cw <= 0) this.resizeCanvas()
+        this.time++
+        this.updateGaugeStatus()
+        this.drawCanvas()
+        this.animFrame = requestAnimationFrame(draw)
+      }
+      this.animFrame = requestAnimationFrame(draw)
+    },
+    updateGaugeStatus() {
+      const wl = this.currentWaterLevel
+      this.gauges.forEach(g => {
+        // 每个渗压计随水位浮动
+        const fluctuation = Math.sin(this.time * 0.01 + g.xRatio * 5) * 0.15
+        g.measured = this.getPhreaticY(g.xRatio) + fluctuation
+        if (g.measured >= g.warning) g.status = 'danger'
+        else if (g.measured >= g.design) g.status = 'warn'
+        else g.status = 'normal'
+      })
+    },
+    // 通过贝塞尔插值获取浸润线某点的Y值
+    getPhreaticY(xRatio, points) {
+      const pts = points || this.phreaticControlPoints
+      if (xRatio <= pts[0].x) return pts[0].y
+      if (xRatio >= pts[pts.length - 1].x) return pts[pts.length - 1].y
+      for (let i = 0; i < pts.length - 1; i++) {
+        if (xRatio >= pts[i].x && xRatio <= pts[i + 1].x) {
+          const t = (xRatio - pts[i].x) / (pts[i + 1].x - pts[i].x)
+          // 余弦插值使曲线更平滑(抛物线效果)
+          const st = (1 - Math.cos(t * Math.PI)) / 2
+          return pts[i].y + (pts[i + 1].y - pts[i].y) * st
+        }
+      }
+      return pts[pts.length - 1].y
+    },
+    // 绘制浸润线(平滑抛物线)
+    drawPhreaticLine(ctx, points, color, dash, label) {
+      const W = this.cw
+      const H = this.ch
+      const s = this.scale
+      const pad = { left: 45 * s, right: 10 * s, top: 12 * s, bottom: 20 * s }
+      const drawW = W - pad.left - pad.right
+      const drawH = H - pad.top - pad.bottom
+      const maxY = 14
+
+      ctx.save()
+      ctx.beginPath()
+      ctx.setLineDash(dash || [])
+      ctx.strokeStyle = color
+      ctx.lineWidth = dash ? 1.8 * s : 2.5 * s
+      ctx.shadowColor = color
+      ctx.shadowBlur = dash ? 0 : 4 * s
+
+      // 抛物线插值:贝塞尔曲线
+      const segments = 40
+      const step = (points[points.length - 1].x - points[0].x) / segments
+      let first = true
+      for (let i = 0; i <= segments; i++) {
+        const x = points[0].x + i * step
+        const y = this.getPhreaticY(x, points)
+        const px = pad.left + (x / 1.0) * drawW
+        const py = pad.top + (1 - y / maxY) * drawH
+        if (first) { ctx.moveTo(px, py); first = false }
+        else ctx.lineTo(px, py)
+      }
+      ctx.stroke()
+      ctx.restore()
+    },
+    drawCanvas() {
+      const canvas = this.$refs.canvasRef
+      if (!canvas) return
+      const ctx = canvas.getContext('2d')
+      const W = this.cw
+      const H = this.ch
+      const s = this.scale
+      const pad = { left: 45 * s, right: 10 * s, top: 12 * s, bottom: 20 * s }
+      const drawW = W - pad.left - pad.right
+      const drawH = H - pad.top - pad.bottom
+      const maxY = 14
+      const fs = 14 * s  // base font size
+
+      ctx.clearRect(0, 0, W, H)
+
+      // --- 背景网格 ---
+      ctx.save()
+      ctx.strokeStyle = 'rgba(0, 212, 255, 0.08)'
+      ctx.lineWidth = 0.5
+      for (let i = 0; i <= 5; i++) {
+        const y = pad.top + (i / 5) * drawH
+        ctx.beginPath()
+        ctx.moveTo(pad.left, y)
+        ctx.lineTo(W - pad.right, y)
+        ctx.stroke()
+      }
+      // Y轴标签(高程)
+      ctx.fillStyle = 'rgba(123, 190, 246, 0.6)'
+      ctx.font = `${fs}px sans-serif`
+      ctx.textAlign = 'right'
+      for (let i = 0; i <= 5; i++) {
+        const val = maxY - (i / 5) * maxY
+        const y = pad.top + (i / 5) * drawH
+        ctx.fillText(val.toFixed(1), pad.left - 6 * s, y + 4 * s)
+      }
+      // Y轴标题
+      ctx.save()
+      ctx.translate(12 * s, pad.top + drawH / 2)
+      ctx.rotate(-Math.PI / 2)
+      ctx.fillStyle = 'rgba(123, 190, 246, 0.5)'
+      ctx.font = `${fs}px sans-serif`
+      ctx.textAlign = 'center'
+      ctx.fillText('水位(m)', 0, 0)
+      ctx.restore()
+      ctx.restore()
+
+      // --- 坝体轮廓 ---
+      ctx.save()
+      const damLeft = pad.left + drawW * 0.20
+      const damRight = pad.left + drawW * 0.92
+      const damTop = pad.top + drawH * 0.08
+      const damBottom = pad.top + drawH
+
+      // 坝体主体(梯形)
+      ctx.beginPath()
+      ctx.moveTo(damLeft, damTop)
+      ctx.lineTo(damRight, damTop)
+      ctx.lineTo(damRight + drawW * 0.05, damBottom)
+      ctx.lineTo(damLeft - drawW * 0.02, damBottom)
+      ctx.closePath()
+      ctx.fillStyle = 'rgba(30, 64, 175, 0.25)'
+      ctx.fill()
+      ctx.strokeStyle = 'rgba(0, 212, 255, 0.3)'
+      ctx.lineWidth = 1.5 * s
+      ctx.stroke()
+
+      // 坝体填充纹理(水平线)
+      ctx.save()
+      ctx.beginPath()
+      ctx.rect(damLeft, damTop, damRight - damLeft + drawW * 0.05, damBottom - damTop)
+      ctx.clip()
+      ctx.strokeStyle = 'rgba(0, 212, 255, 0.04)'
+      ctx.lineWidth = 0.5
+      for (let i = 0; i < 20; i++) {
+        const y = damTop + (i / 20) * (damBottom - damTop)
+        ctx.beginPath()
+        ctx.moveTo(damLeft, y)
+        ctx.lineTo(damRight + drawW * 0.05, y)
+        ctx.stroke()
+      }
+      ctx.restore()
+      ctx.restore()
+
+      // --- 坝顶标注 ---
+      ctx.save()
+      ctx.fillStyle = 'rgba(123, 190, 246, 0.6)'
+      ctx.font = `${fs}px sans-serif`
+      ctx.textAlign = 'left'
+      ctx.fillText('上游', damLeft, damTop - 5 * s)
+      ctx.textAlign = 'center'
+      ctx.fillText('坝顶', (damLeft + damRight) / 2, damTop - 5 * s)
+      ctx.textAlign = 'right'
+      ctx.fillText('下游', damRight, damTop - 5 * s)
+      ctx.restore()
+      ctx.restore()
+
+      // --- 库区水体(上游侧),带波动效果 ---
+      const wl = this.currentWaterLevel
+      const wlY = pad.top + (1 - wl / maxY) * drawH
+      // 水位线必须在坝顶以下
+      const finalWlY = Math.min(wlY, damTop + 5)
+
+      ctx.save()
+      // 水体渐变
+      const waterGrad = ctx.createLinearGradient(damLeft - drawW * 0.3, finalWlY, damLeft - drawW * 0.3, damBottom)
+      waterGrad.addColorStop(0, 'rgba(0, 150, 255, 0.35)')
+      waterGrad.addColorStop(1, 'rgba(0, 80, 180, 0.5)')
+      ctx.fillStyle = waterGrad
+
+      // 水体路径:上游侧矩形 + 波动水面线
+      ctx.beginPath()
+      const waterLeft = pad.left
+      ctx.moveTo(waterLeft, finalWlY)
+      // 波浪线
+      for (let x = waterLeft; x <= damLeft; x += 3) {
+        const waveY = finalWlY + Math.sin((x + this.time * 2) * 0.05) * 2.5
+        ctx.lineTo(x, waveY)
+      }
+      ctx.lineTo(damLeft, damBottom)
+      ctx.lineTo(waterLeft, damBottom)
+      ctx.closePath()
+      ctx.fill()
+
+      // 水位线(亮线)
+      ctx.save()
+      ctx.beginPath()
+      // 水面的波浪亮线,只在水体区域绘制
+      for (let x = waterLeft; x <= damLeft; x += 2) {
+        const waveY = finalWlY + Math.sin((x + this.time * 2) * 0.05) * 2.5
+        if (x === waterLeft) ctx.moveTo(x, waveY)
+        else ctx.lineTo(x, waveY)
+      }
+      ctx.strokeStyle = 'rgba(0, 212, 255, 0.7)'
+      ctx.lineWidth = 1.5
+      ctx.shadowColor = '#00d4ff'
+      ctx.shadowBlur = 6
+      ctx.stroke()
+      ctx.restore()
+      ctx.restore()
+
+      // --- 浸润线(3条:实测、设计、警戒) ---
+      // 1. 警戒浸润线(虚线)
+      this.drawPhreaticLine(ctx, this.warningControlPoints, 'rgba(239, 68, 68, 0.5)', [5, 4], '警戒线')
+
+      // 2. 设计浸润线(虚线)
+      this.drawPhreaticLine(ctx, this.designControlPoints, 'rgba(251, 191, 36, 0.6)', [4, 3], '设计线')
+
+      // 3. 实测浸润线(实线,带发光)
+      // 实时调整实测线:受水位波动影响
+      const wlFactor = wl / this.baseWaterLevel
+      const adjustedPoints = this.phreaticControlPoints.map(p => ({
+        x: p.x,
+        y: p.y * wlFactor,
+      }))
+      this.drawPhreaticLine(ctx, adjustedPoints, '#00d4ff', [], '实测线')
+
+      // --- 渗压计点位 ---
+      this.gauges.forEach(g => {
+        const px = pad.left + (g.xRatio / 1.0) * drawW
+        const py = pad.top + (1 - g.measured / maxY) * drawH
+
+        // 渗压计圆点
+        ctx.save()
+        ctx.beginPath()
+        ctx.arc(px, py, 5, 0, Math.PI * 2)
+        const dotColor = g.status === 'danger' ? '#ef4444' : g.status === 'warn' ? '#fbbf24' : '#22c55e'
+        ctx.fillStyle = dotColor
+        ctx.fill()
+        ctx.strokeStyle = 'rgba(0, 30, 60, 0.8)'
+        ctx.lineWidth = 2
+        ctx.stroke()
+        // 发光
+        ctx.shadowColor = dotColor
+        ctx.shadowBlur = 6
+        ctx.fill()
+        ctx.restore()
+
+        // 标签
+        ctx.save()
+        ctx.fillStyle = '#e0fcff'
+        ctx.font = `${fs}px sans-serif`
+        ctx.textAlign = 'center'
+        ctx.fillText(g.id, px, py - 10 * s)
+        ctx.fillStyle = 'rgba(123, 190, 246, 0.6)'
+        ctx.font = `${fs - 1}px sans-serif`
+        ctx.fillText(g.measured.toFixed(1) + 'm', px, py + 16 * s)
+        ctx.restore()
+      })
+
+      // --- 底部X轴标注 ---
+      ctx.save()
+      ctx.fillStyle = 'rgba(123, 190, 246, 0.5)'
+      ctx.font = `${fs}px sans-serif`
+      ctx.textAlign = 'center'
+
+      ctx.restore()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.dam-seepage-section {
+  background: rgba(0, 20, 40, 0.5);
+  border-radius: 4px;
+  border: 1px solid rgba(0, 212, 255, 0.12);
+  padding: 4px;
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+}
+
+/* 图例 */
+.legend-row {
+  display: flex;
+  justify-content: center;
+  gap: 16px;
+  padding: 4px 8px;
+  flex-wrap: wrap;
+}
+.legend-item {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+.legend-line {
+  width: 22px;
+  height: 3px;
+  border-radius: 1px;
+}
+.legend-line-measured { background: #00d4ff; }
+.legend-line-design { background: #fbbf24; }
+.legend-line-warning { background: #ef4444; }
+.legend-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+}
+.legend-dot-gauge { background: #22c55e; }
+.legend-label {
+  font-size: 12px;
+  color: rgba(123, 190, 246, 0.8);
+}
+
+/* Canvas 容器 */
+.canvas-wrapper {
+  position: relative;
+  width: 100%;
+}
+.canvas-wrapper canvas {
+  display: block;
+  width: 100%;
+  height: auto;
+}
+
+/* 指标行 */
+.stats-row {
+  display: flex;
+  align-items: center;
+  gap: 0;
+  margin-bottom: 6px;
+}
+.stat-item {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 3px;
+  background: rgba(0, 20, 40, 0.5);
+  border: 1px solid rgba(0, 212, 255, 0.1);
+  border-radius: 3px;
+  padding: 3px 6px;
+}
+.stat-divider {
+  width: 4px;
+  flex-shrink: 0;
+}
+.stat-label {
+  font-size: 11px;
+  color: rgba(123, 190, 246, 0.7);
+}
+.stat-value {
+  font-size: 16px;
+  font-weight: bold;
+  min-width: 24px;
+  text-align: right;
+}
+.stat-unit {
+  font-size: 11px;
+  color: rgba(123, 190, 246, 0.5);
+}
+
+/* 渗压计表格 */
+.gauge-table {
+  width: 100%;
+  overflow-x: auto;
+}
+.gauge-table table {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 10px;
+}
+.gauge-table th {
+  color: rgba(123, 190, 246, 0.8);
+  background: rgba(0, 20, 40, 0.4);
+  padding: 3px 6px;
+  text-align: center;
+  border-bottom: 1px solid rgba(0, 212, 255, 0.1);
+  font-weight: normal;
+  font-size: 11px;
+}
+.gauge-table td {
+  color: #e0fcff;
+  padding: 2px 6px;
+  text-align: center;
+  border-bottom: 1px solid rgba(0, 212, 255, 0.05);
+  font-size: 11px;
+  line-height: 1.8;
+}
+.gauge-table td.td-left {
+  text-align: left;
+  white-space: nowrap;
+}
+.gauge-table tr.danger td {
+  background: rgba(239, 68, 68, 0.08);
+}
+.gauge-table tr.warn td {
+  background: rgba(251, 191, 36, 0.06);
+}
+.status-tag {
+  font-size: 10px;
+  padding: 1px 6px;
+  border-radius: 2px;
+}
+.status-tag.normal { color: #22c55e; background: rgba(34, 197, 94, 0.15); }
+.status-tag.warn { color: #fbbf24; background: rgba(251, 191, 36, 0.15); }
+.status-tag.danger { color: #ef4444; background: rgba(239, 68, 68, 0.15); }
+</style>

+ 92 - 131
src/views/mainPages/EngineeringSafetyView.vue

@@ -60,91 +60,6 @@
             </div>
           </div>
 
-          <!-- ===== 数据框2:坝体整体变形安全 ===== -->
-          <div class="data-card mt-10">
-            <div class="card-header">
-              <h3 class="card-title">坝体整体变形安全</h3>
-            </div>
-            <div class="card-body chart-body">
-              <div class="deform-section">
-                <div class="conclusion-row">
-                  <span class="conclusion-row-label">整体变形状态判定</span>
-                  <span class="conclusion-row-value text-green">稳定</span>
-                </div>
-                <div class="deform-metrics">
-                  <div class="metric-item">
-                    <span class="metric-item-label">坝顶最大水平位移</span>
-                    <span class="metric-item-value">12.6<span class="metric-item-unit">mm</span></span>
-                    <span class="metric-item-tag tag-ok">正常</span>
-                  </div>
-                  <div class="metric-item">
-                    <span class="metric-item-label">坝体累计垂直沉降</span>
-                    <span class="metric-item-value">8.3<span class="metric-item-unit">mm</span></span>
-                    <span class="metric-item-tag tag-ok">正常</span>
-                  </div>
-                </div>
-                <div class="deform-rate">
-                  <span class="rate-label">形变速率判定</span>
-                  <span class="rate-tag tag-ok">匀速稳定</span>
-                  <span class="rate-hint">速率 0.02mm/d</span>
-                </div>
-                <div class="deform-trend">
-                  <span class="trend-label">24h整体形变趋势</span>
-                  <div class="trend-chart-box">
-                    <canvas id="deformTrendChart" width="290" height="42"></canvas>
-                  </div>
-                </div>
-              </div>
-            </div>
-          </div>
-
-          <!-- ===== 数据框3:坝体渗流防渗安全 ===== -->
-          <div class="data-card mt-10">
-            <div class="card-header">
-              <h3 class="card-title">坝体渗流防渗安全</h3>
-            </div>
-            <div class="card-body chart-body">
-              <div class="seepage-section">
-                <div class="conclusion-row">
-                  <span class="conclusion-row-label">渗流整体安全状态</span>
-                  <span class="conclusion-row-value text-green">防渗正常</span>
-                </div>
-                <div class="deform-metrics">
-                  <div class="metric-item">
-                    <span class="metric-item-label">坝基典型扬压力均值</span>
-                    <span class="metric-item-value">36.8<span class="metric-item-unit">kPa</span></span>
-                    <span class="metric-item-tag tag-ok">正常</span>
-                  </div>
-                  <div class="metric-item">
-                    <span class="metric-item-label">坝基平均渗流量</span>
-                    <span class="metric-item-value">2.4<span class="metric-item-unit">L/s</span></span>
-                    <span class="metric-item-tag tag-ok">正常</span>
-                  </div>
-                </div>
-                <div class="deform-rate">
-                  <span class="rate-label">渗流稳定性判定</span>
-                  <span class="rate-tag tag-ok">平稳无波动</span>
-                </div>
-                <div class="balance-row">
-                  <span class="balance-row-label">绕坝渗流左右岸平衡</span>
-                  <div class="balance-bars">
-                    <div class="balance-bar-line">
-                      <span class="balance-bar-side">左岸</span>
-                      <div class="balance-track"><div class="balance-fill" style="width:48%"></div></div>
-                      <span class="balance-bar-val">48%</span>
-                    </div>
-                    <div class="balance-bar-line">
-                      <span class="balance-bar-side">右岸</span>
-                      <div class="balance-track"><div class="balance-fill fill-right" style="width:52%"></div></div>
-                      <span class="balance-bar-val">52%</span>
-                    </div>
-                  </div>
-                  <span class="balance-result tag-ok">均衡</span>
-                </div>
-              </div>
-            </div>
-          </div>
-
           <!-- 巡检记录 -->
           <div class="data-card mt-10">
             <div class="card-header">
@@ -187,6 +102,48 @@
             </div>
           </div>
 
+          <!-- ===== 数据框3:坝体安全监测(形变/渗压切换) ===== -->
+          <div class="data-card mt-10">
+            <div class="card-header">
+              <h3 class="card-title">坝体安全监测</h3>
+            </div>
+            <div class="card-body chart-body">
+              <!-- 切换按钮 -->
+              <div class="tab-toggle-row">
+                <button class="tab-toggle-btn" :class="{ active: mergedTab === 'deform' }" @click="switchMergedTab('deform')">
+                  <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
+                    <polyline points="1 13 5 9 8 11 14 4"/>
+                    <polyline points="10 4 14 4 14 8"/>
+                  </svg>
+                  形变监测
+                </button>
+                <button class="tab-toggle-btn" :class="{ active: mergedTab === 'seepage' }" @click="switchMergedTab('seepage')">
+                  <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
+                    <path d="M2 14 L14 14 M8 14 L8 2 M4 6 Q8 2 12 6"/>
+                  </svg>
+                  渗压监测
+                </button>
+              </div>
+              <!-- 形变内容 -->
+              <div v-show="mergedTab === 'deform'" class="deform-section">
+                <div class="conclusion-row" style="margin-bottom:6px;">
+                  <span class="conclusion-row-label">整体变形状态判定</span>
+                  <span class="conclusion-row-value text-green">稳定</span>
+                </div>
+                <DamDeformSection />
+              </div>
+
+              <!-- 渗压内容 -->
+              <div v-show="mergedTab === 'seepage'" class="seepage-section">
+                <div class="conclusion-row" style="margin-bottom:6px;">
+                  <span class="conclusion-row-label">渗流整体安全状态</span>
+                  <span class="conclusion-row-value text-green">防渗正常</span>
+                </div>
+                <DamSeepageSection />
+              </div>
+            </div>
+          </div>
+
         </div>
 
         <!-- ========== 右侧面板 ========== -->
@@ -372,12 +329,17 @@
       </div>
     </div>
 
+
+
   </div>
 </template>
 <script>
 import * as echarts from "echarts";
+import DamSeepageSection from '../../components/DamSeepageSection.vue'
+import DamDeformSection from '../../components/DamDeformSection.vue'
 export default {
   name: "EngineeringSafetyView",
+  components: { DamSeepageSection, DamDeformSection },
   data() {
     return {
       // 监控摘要
@@ -466,7 +428,7 @@ export default {
         { id: "T001", device: "钢筋应力-R01-03", threshold: "200 MPa", current: "246 MPa", over: "23%", level: "critical", time: "2026-06-08 11:00" },
         { id: "T002", device: "基岩应力-Y02-05", threshold: "150 MPa", current: "172 MPa", over: "14.7%", level: "critical", time: "2026-06-08 10:10" },
       ],
-      deformTrendChart: null,
+      mergedTab: 'deform',
     };
   },
   computed: {
@@ -485,13 +447,11 @@ export default {
     this.$nextTick(() => {
       this.initGaugeChart();
       this.renderSummaryPieChart();
-      this.renderDeformTrendChart();
     });
   },
   beforeUnmount() {
     if (this.summaryPieChart) this.summaryPieChart.dispose();
     if (this.deviceGaugeChart) this.deviceGaugeChart.dispose();
-    if (this.deformTrendChart) this.deformTrendChart.dispose();
   },
   methods: {
     // --- 设备清单树 ---
@@ -510,6 +470,10 @@ export default {
         node.expanded = !node.expanded;
       }
     },
+    // --- 坝体安全监测 Tab 切换 ---
+    switchMergedTab(tab) {
+      this.mergedTab = tab;
+    },
     // --- 监控摘要 ---
     toggleSummaryCard(name) {
       if (this.activeSummaryCard === name) {
@@ -597,53 +561,20 @@ export default {
         });
       }
     },
-    // --- 24h整体形变趋势(中心线偏移) ---
-    renderDeformTrendChart() {
-      if (!echarts) return;
-      const el = document.getElementById("deformTrendChart");
-      if (!el) return;
-      if (this.deformTrendChart) this.deformTrendChart.dispose();
-      this.deformTrendChart = echarts.init(el, null, { renderer: "canvas" });
-      const hours = ["00", "02", "04", "06", "08", "10", "12", "14", "16", "18", "20", "22"];
-      // 以中心线为基准的偏移值(mm),正=偏右/上,负=偏左/下
-      const values = [0.02, 0.05, 0.07, 0.04, 0.08, 0.06, 0.03, 0.01, 0.05, 0.07, 0.04, -0.02];
-      this.deformTrendChart.setOption({
-        animation: false,
-        grid: { left: 0, right: 0, top: 0, bottom: 0 },
-        xAxis: { type: "category", data: hours, show: false },
-        yAxis: { type: "value", show: false, min: -0.1, max: 0.1 },
-        tooltip: { trigger: "axis", formatter: p => `偏移: ${p[0].value} mm`, textStyle: { fontSize: 11, color: "#e0fcff" }, backgroundColor: "rgba(0,20,40,0.85)", borderColor: "rgba(0,212,255,0.3)", borderWidth: 1, padding: [4, 8] },
-        series: [{
-          type: "bar",
-          barWidth: "50%",
-          data: values.map(v => ({
-            value: v,
-            itemStyle: {
-              color: v >= 0 ? "rgba(98,246,251,0.8)" : "rgba(239,68,68,0.7)",
-              borderRadius: v >= 0 ? [2, 2, 0, 0] : [0, 0, 2, 2],
-            },
-          })),
-          markLine: {
-            silent: true,
-            symbol: "none",
-            lineStyle: { color: "rgba(98,246,251,0.4)", width: 1, type: "dashed" },
-            label: { show: false },
-            data: [{ yAxis: 0 }],
-          },
-        }],
-      });
-    },
   },
 };
 </script>
 <style scoped>
 .safety-container { width: 100%; height: 100%; display: flex; flex-direction: column; position: relative; overflow: hidden; }
-.safety-scroll-area { position: absolute; left: 0; right: 0; top: 80px; bottom: 0; overflow-y: auto; overflow-x: hidden; z-index: 5; padding-bottom: 40px; pointer-events: none; }
+.safety-scroll-area { position: absolute; left: 0; right: 0; top: 80px; bottom: 0; overflow-y: auto; overflow-x: hidden; z-index: 5; padding-bottom: 40px; pointer-events: none; display: flex; flex-direction: column; align-items: center; }
 .safety-scroll-area::-webkit-scrollbar { width: 4px; }
 .safety-scroll-area::-webkit-scrollbar-track { background: rgba(0,20,40,0.5); }
 .safety-scroll-area::-webkit-scrollbar-thumb { background: rgba(0,212,255,0.3); border-radius: 2px; }
-.tab-content { width: 100%; display: flex; justify-content: space-between; padding: 0 20px; box-sizing: border-box; }
-.left-sidebar { width: 350px; flex-shrink: 0; pointer-events: auto; display: flex; flex-direction: column; }
+.tab-content { width: 100%; flex-shrink: 0; display: flex; justify-content: space-between; padding: 0 20px; box-sizing: border-box; }
+
+
+
+.left-sidebar { width: 420px; flex-shrink: 0; pointer-events: auto; display: flex; flex-direction: column; }
 .right-sidebar { width: 380px; flex-shrink: 0; pointer-events: auto; display: flex; flex-direction: column; }
 .mt-10 { margin-top: 8px; }
 
@@ -758,10 +689,7 @@ export default {
 .metric-item-tag { font-size: 9px; padding: 0 5px; border-radius: 2px; line-height: 16px; }
 .metric-item-tag.tag-ok { color: #22c55e; background: rgba(34,197,94,0.15); }
 
-/* Rate row */
-.deform-rate { display: flex; align-items: center; gap: 6px; padding: 4px 8px; background: rgba(0,20,40,0.4); border-radius: 3px; }
-.rate-label { color: #7bbef6; font-size: 12px; }
-.rate-tag { font-size: 11px; font-weight: bold; padding: 1px 6px; border-radius: 2px; }
+
 .rate-tag.tag-ok { color: #22c55e; background: rgba(34,197,94,0.15); }
 .rate-hint { margin-left: auto; color: #7bbef6; font-size: 11px; }
 
@@ -789,6 +717,39 @@ export default {
 canvas { display: block; margin: 0 auto; }
 canvas#summaryPieChart { display: flex !important; padding-left: 35px !important; padding-right: 35px !important; }
 
+/* ========== Tab Toggle Buttons ========== */
+.tab-toggle-row {
+  display: flex;
+  gap: 4px;
+  margin-bottom: 6px;
+}
+.tab-toggle-btn {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 5px;
+  padding: 5px 0;
+  background: rgba(0, 20, 40, 0.4);
+  border: 1px solid rgba(0, 212, 255, 0.15);
+  border-radius: 4px;
+  color: rgba(123, 190, 246, 0.7);
+  font-size: 12px;
+  cursor: pointer;
+  transition: all 0.2s;
+  font-family: inherit;
+}
+.tab-toggle-btn:hover {
+  border-color: rgba(0, 212, 255, 0.3);
+  color: #e0fcff;
+  background: rgba(0, 30, 60, 0.6);
+}
+.tab-toggle-btn.active {
+  background: rgba(0, 212, 255, 0.1);
+  border-color: rgba(0, 212, 255, 0.4);
+  color: #00d4ff;
+}
+
 /* ========== Tree ========== */
 .tree-search-bar { display: flex; align-items: center; gap: 6px; padding: 4px 8px; background: rgba(0,20,40,0.5); border: 1px solid rgba(0,212,255,0.15); border-radius: 4px; margin-bottom: 4px; }
 .tree-search-icon { flex-shrink: 0; }