ソースを参照

[Mod 0000] 修改张家峁页面样式逻辑

wangkeyi 1 日 前
親
コミット
73eb3afb2a

+ 213 - 0
src/views/vent/home/selfIgnition/components/FiberTemperatureChart.vue

@@ -0,0 +1,213 @@
+<template>
+  <div ref="chartRef" class="fiber-chart"></div>
+</template>
+
+<script lang="ts" setup>
+  import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
+  import * as echarts from 'echarts';
+  import type { FiberChartData } from '../data';
+
+  const props = defineProps<{
+    data: FiberChartData;
+    active?: boolean;
+  }>();
+
+  const chartRef = ref<HTMLElement>();
+  let chart: echarts.ECharts | undefined;
+  let resizeObserver: ResizeObserver | undefined;
+
+  function buildOption(data: FiberChartData) {
+    const peakIndex = data.peakIndex;
+    const peakPercent = ((peakIndex + 0.5) / data.categories.length) * 100;
+    const labelIndexes = [0, 3, 6, 8, 11];
+
+    return {
+      animationDuration: 800,
+      backgroundColor: 'transparent',
+      textStyle: {
+        fontFamily: 'Microsoft YaHei, sans-serif',
+      },
+      tooltip: {
+        trigger: 'axis',
+        backgroundColor: 'rgba(5, 25, 45, 0.92)',
+        borderColor: 'rgba(61, 246, 255, 0.55)',
+        borderWidth: 1,
+        textStyle: { color: '#fff', fontSize: 10 },
+        axisPointer: {
+          lineStyle: { color: 'rgba(255, 157, 28, 0.65)', type: 'dashed' },
+        },
+      },
+      grid: {
+        left: 40,
+        right: 8,
+        top: 22,
+        bottom: 22,
+      },
+      xAxis: {
+        type: 'category',
+        boundaryGap: false,
+        data: data.categories,
+        axisLine: {
+          lineStyle: { color: 'rgba(61, 246, 255, 0.35)' },
+        },
+        axisTick: { show: false },
+        axisLabel: {
+          color: '#8fc7e8',
+          fontSize: 9,
+          margin: 8,
+          interval: 0,
+          formatter: (value: string, index: number) => (labelIndexes.includes(index) ? value : ''),
+        },
+      },
+      yAxis: {
+        type: 'value',
+        min: 0,
+        max: 1000,
+        interval: 200,
+        axisLine: { show: false },
+        axisTick: { show: false },
+        axisLabel: {
+          color: '#8fc7e8',
+          fontSize: 9,
+        },
+        splitLine: {
+          lineStyle: { color: 'rgba(61, 246, 255, 0.12)' },
+        },
+      },
+      series: [
+        {
+          name: '预警线',
+          type: 'line',
+          data: data.dashed,
+          smooth: 0.35,
+          symbol: 'none',
+          lineStyle: {
+            color: 'rgba(255, 179, 71, 0.85)',
+            width: 1.2,
+            type: 'dashed',
+          },
+          emphasis: { disabled: true },
+        },
+        {
+          name: '光纤温度',
+          type: 'line',
+          data: data.solid,
+          smooth: 0.35,
+          symbol: 'circle',
+          symbolSize: 4,
+          itemStyle: {
+            color: '#ffb347',
+            borderColor: '#fff',
+            borderWidth: 1,
+          },
+          lineStyle: {
+            color: '#ff7a1a',
+            width: 2,
+          },
+          areaStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: 'rgba(255, 122, 26, 0.32)' },
+              { offset: 1, color: 'rgba(255, 122, 26, 0)' },
+            ]),
+          },
+          markLine: {
+            silent: true,
+            symbol: 'none',
+            label: { show: false },
+            lineStyle: {
+              color: 'rgba(255, 77, 79, 0.85)',
+              type: 'dashed',
+              width: 1,
+            },
+            data: [{ xAxis: data.categories[peakIndex] }],
+          },
+          markPoint: {
+            symbol: 'circle',
+            symbolSize: 8,
+            itemStyle: {
+              color: '#ffb347',
+              borderColor: '#fff',
+              borderWidth: 2,
+              shadowBlur: 12,
+              shadowColor: 'rgba(255, 122, 26, 0.9)',
+            },
+            label: { show: false },
+            data: [{ coord: [data.categories[peakIndex], data.solid[peakIndex]] }],
+          },
+        },
+      ],
+      graphic: [
+        {
+          type: 'text',
+          left: `${peakPercent - 8}%`,
+          top: '26%',
+          style: {
+            text: '预警确认',
+            fill: '#ffd15c',
+            fontSize: 10,
+            backgroundColor: 'rgba(255, 157, 28, 0.24)',
+            borderColor: '#ff9d1c',
+            borderWidth: 1,
+            padding: [2, 4],
+          },
+        },
+        {
+          type: 'text',
+          left: `${peakPercent - 7}%`,
+          top: '38%',
+          style: {
+            text: '立即报警',
+            fill: '#fff',
+            fontSize: 10,
+            backgroundColor: 'rgba(255, 77, 79, 0.5)',
+            borderColor: '#ff4d4f',
+            borderWidth: 1,
+            padding: [2, 4],
+          },
+        },
+      ],
+    };
+  }
+
+  function renderChart() {
+    const el = chartRef.value;
+    if (!el || el.clientWidth === 0 || el.clientHeight === 0) return;
+    if (!chart) {
+      chart = echarts.init(el, undefined, {
+        renderer: 'canvas',
+        devicePixelRatio: Math.max(window.devicePixelRatio || 1, 2),
+      });
+      chart.setOption(buildOption(props.data));
+    }
+    chart.resize();
+  }
+
+  watch(
+    () => props.active,
+    () => {
+      nextTick(renderChart);
+    }
+  );
+
+  onMounted(() => {
+    resizeObserver = new ResizeObserver(renderChart);
+    if (chartRef.value) {
+      resizeObserver.observe(chartRef.value);
+    }
+    renderChart();
+  });
+
+  onUnmounted(() => {
+    resizeObserver?.disconnect();
+    chart?.dispose();
+    chart = undefined;
+  });
+</script>
+
+<style lang="less" scoped>
+  .fiber-chart {
+    width: 100%;
+    height: 100%;
+    background: radial-gradient(ellipse at 58% 42%, rgba(255, 122, 26, 0.08), transparent 62%);
+  }
+</style>

+ 446 - 80
src/views/vent/home/selfIgnition/components/ManualView.vue

@@ -10,34 +10,57 @@
       :visible="true"
       :visible="true"
     >
     >
       <div class="manual-form">
       <div class="manual-form">
+        <div class="form-toolbar">
+          <div class="target-row">
+            <span class="target-label">评价对象:</span>
+            <PanelSelect v-model="manageSystemId" :options="sceneSelectOptions" @update:model-value="loadLatest" />
+          </div>
+          <div class="eval-type-tabs">
+            <span :class="{ active: evalType === 1 }" @click="changeEvalType(1)">采前评价</span>
+            <span :class="{ active: evalType === 2 }" @click="changeEvalType(2)">采中评价</span>
+          </div>
+        </div>
+
         <div class="group-list">
         <div class="group-list">
-          <div v-for="group in groups" :key="group.key" class="form-group">
-            <div class="group-collapse" @click="toggleGroup(group.key)">
-              <img class="group-icon" src="@/assets/images/vent/home/selfIgnition/form-icon.svg" alt="" />
-              <span>{{ group.title }}</span>
-              <i class="collapse-arrow" :class="{ collapsed: collapsed[group.key] }"></i>
-            </div>
-            <template v-if="!collapsed[group.key]">
-              <div class="group-title">
-                <span class="title-bars">
-                  <i v-for="n in 4" :key="n"></i>
-                </span>
-                <span>{{ group.title }}指标</span>
-                <span class="title-dots">
-                  <i v-for="n in 3" :key="n"></i>
-                  <b></b>
-                </span>
+          <div v-if="loadingOptions" class="form-loading">评价指标加载中...</div>
+          <template v-else>
+            <div v-for="group in fieldGroups" :key="group.key" class="form-group">
+              <div class="group-collapse" @click="toggleGroup(group.key)">
+                <img class="group-icon" src="@/assets/images/vent/home/selfIgnition/form-icon.svg" alt="" />
+                <span>{{ group.title }}</span>
+                <i class="collapse-arrow" :class="{ collapsed: collapsed[group.key] }"></i>
               </div>
               </div>
-              <div class="field-list">
-                <div v-for="field in group.fields" :key="field.key" class="field-row">
-                  <span class="field-label">{{ field.label }}:</span>
-                  <PanelSelect v-model="field.value" :options="field.options" />
+              <template v-if="!collapsed[group.key]">
+                <div class="group-title">
+                  <span class="title-bars">
+                    <i v-for="n in 4" :key="n"></i>
+                  </span>
+                  <span>{{ group.title }}指标</span>
+                  <span class="title-dots">
+                    <i v-for="n in 3" :key="n"></i>
+                    <b></b>
+                  </span>
                 </div>
                 </div>
-              </div>
-            </template>
-          </div>
+                <div class="field-list">
+                  <div v-for="item in group.fields" :key="item.code" class="field-row">
+                    <span class="field-label" :title="item.name">{{ item.name }}:</span>
+                    <PanelSelect v-model="formValues[FIELD_BY_CODE[item.code]]" :options="selectOptions(item)" />
+                  </div>
+                </div>
+              </template>
+            </div>
+            <div v-if="!fieldGroups.length" class="form-loading">暂无评价指标</div>
+          </template>
+        </div>
+
+        <div class="form-actions">
+          <button class="action-button preview-button" :disabled="busy" @click="preview">
+            {{ previewing ? '计算中...' : '计算预览' }}
+          </button>
+          <button class="action-button submit-button" :disabled="busy" @click="submit">
+            {{ submitting ? '提交中...' : '提交' }}
+          </button>
         </div>
         </div>
-        <button class="action-button submit-button" @click="submit">提交</button>
       </div>
       </div>
     </ModuleCommon>
     </ModuleCommon>
 
 
@@ -53,37 +76,41 @@
       <div class="result-panel">
       <div class="result-panel">
         <div class="result-top">
         <div class="result-top">
           <div class="score-line">
           <div class="score-line">
-            <span class="score-label">风险分值:</span>
-            <img class="score-info" src="@/assets/images/vent/home/selfIgnition/info-icon.svg" alt="根据人工录入指标计算" />
-            <span class="score-value">{{ result.score.toFixed(1) }}</span>
+            <span class="score-label">安全评价得分:</span>
+            <img class="score-info" src="@/assets/images/vent/home/selfIgnition/info-icon.svg" alt="由后端算法计算" />
+            <span class="score-value">{{ resultScore }}</span>
           </div>
           </div>
           <div class="gauge-line">
           <div class="gauge-line">
             <div class="gauge">
             <div class="gauge">
               <img class="gauge-segment gauge-segment-1" src="@/assets/images/vent/home/selfIgnition/risk-gauge.png" alt="" />
               <img class="gauge-segment gauge-segment-1" src="@/assets/images/vent/home/selfIgnition/risk-gauge.png" alt="" />
               <img class="gauge-segment gauge-segment-2" src="@/assets/images/vent/home/selfIgnition/risk-gauge.png" alt="" />
               <img class="gauge-segment gauge-segment-2" src="@/assets/images/vent/home/selfIgnition/risk-gauge.png" alt="" />
             </div>
             </div>
-            <span class="risk-tag" :style="{ '--risk-color': result.color }">{{ result.levelText }}</span>
+            <span class="risk-tag" :style="{ '--risk-color': resultRiskColor }">{{ resultTagText }}</span>
           </div>
           </div>
         </div>
         </div>
 
 
         <div class="result-info">
         <div class="result-info">
           <div class="info-row"
           <div class="info-row"
-            ><span>评价对象:</span><em>{{ result.target }}</em></div
-          >
-          <div class="info-row"
-            ><span>数据时间:</span><em>{{ result.time }}</em></div
+            ><span>评价对象:</span><em>{{ resultTarget }}</em></div
           >
           >
           <div class="info-row"
           <div class="info-row"
-            ><span>数据来源:</span><em>{{ result.source }}</em></div
+            ><span>数据时间:</span><em>{{ resultTime }}</em></div
           >
           >
+          <div class="info-row"><span>数据来源:</span><em>人工录入</em></div>
         </div>
         </div>
 
 
         <div class="level-table">
         <div class="level-table">
-          <div class="level-head"><span>评价等级</span><span>风险分值范围</span><span>对应颜色</span></div>
-          <div v-for="item in levels" :key="item.level" class="level-row" :class="{ highlight: item.level === result.highlightLevel }">
+          <div class="level-head"><span>评价等级</span><span>评分范围</span><span>对应颜色</span></div>
+          <div
+            v-for="item in levels"
+            :key="item.level"
+            class="level-row"
+            :class="{ highlight: item.level === resultLevel }"
+            :style="item.level === resultLevel ? { '--highlight-color': resultRiskColor } : undefined"
+          >
             <span class="level-name">
             <span class="level-name">
               <i :style="{ background: item.color, boxShadow: `0 0 8px ${item.color}` }"></i>
               <i :style="{ background: item.color, boxShadow: `0 0 8px ${item.color}` }"></i>
-              {{ item.name }}
+              {{ item.level }}
             </span>
             </span>
             <span>{{ item.range }}</span>
             <span>{{ item.range }}</span>
             <span>{{ item.colorName }}</span>
             <span>{{ item.colorName }}</span>
@@ -92,36 +119,37 @@
 
 
         <div class="suggestion-box">
         <div class="suggestion-box">
           <span class="suggestion-icon"></span>
           <span class="suggestion-icon"></span>
-          <p>{{ result.suggestion }}</p>
+          <p>{{ resultSuggestion }}</p>
         </div>
         </div>
 
 
-        <button class="action-button report-button" @click="showReport = true">查看评价报告</button>
+        <button class="action-button report-button" :disabled="!hasEvaluation" @click="showReport = true">查看评价报告</button>
       </div>
       </div>
     </ModuleCommon>
     </ModuleCommon>
 
 
     <Transition name="ignition-fade">
     <Transition name="ignition-fade">
       <div v-if="showReport" class="report-mask" @click.self="showReport = false">
       <div v-if="showReport" class="report-mask" @click.self="showReport = false">
         <div class="report-dialog">
         <div class="report-dialog">
-          <div class="report-title">采空区自燃风险评价报告</div>
+          <div class="report-title">自然发火安全评价报告</div>
           <div class="report-body">
           <div class="report-body">
             <div class="report-row"
             <div class="report-row"
-              ><span>评价对象</span><em>{{ result.target }}</em></div
+              ><span>评价对象</span><em>{{ resultTarget }}</em></div
             >
             >
             <div class="report-row"
             <div class="report-row"
-              ><span>风险分值</span><em>{{ result.score.toFixed(1) }}</em></div
+              ><span>评价类型</span><em>{{ evalTypeText }}</em></div
             >
             >
             <div class="report-row"
             <div class="report-row"
-              ><span>评价等级</span><em :style="{ color: result.color }">{{ result.levelText }}</em></div
+              ><span>安全评价得分</span><em>{{ resultScore }}</em></div
             >
             >
             <div class="report-row"
             <div class="report-row"
-              ><span>数据时间</span><em>{{ result.time }}</em></div
+              ><span>风险等级</span><em :style="{ color: resultRiskColor }">{{ resultTagText }}</em></div
             >
             >
             <div class="report-row"
             <div class="report-row"
-              ><span>数据来源</span><em>{{ result.source }}</em></div
+              ><span>数据时间</span><em>{{ resultTime }}</em></div
             >
             >
+            <div class="report-row"><span>数据来源</span><em>人工录入</em></div>
             <div class="report-suggestion">
             <div class="report-suggestion">
               <span class="suggestion-icon"></span>
               <span class="suggestion-icon"></span>
-              <p>{{ result.suggestion }}</p>
+              <p>{{ resultSuggestion }}</p>
             </div>
             </div>
           </div>
           </div>
           <button class="action-button report-close" @click="showReport = false">关闭</button>
           <button class="action-button report-close" @click="showReport = false">关闭</button>
@@ -132,22 +160,101 @@
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
-  import { reactive, ref } from 'vue';
+  import { computed, onMounted, reactive, ref } from 'vue';
+  import { useRoute } from 'vue-router';
   import ModuleCommon from '../../configurable/components/ModuleCommon.vue';
   import ModuleCommon from '../../configurable/components/ModuleCommon.vue';
   import PanelSelect from './PanelSelect.vue';
   import PanelSelect from './PanelSelect.vue';
   import { EMPTY_MODULE_DATA, panelShowStyle } from '../utils';
   import { EMPTY_MODULE_DATA, panelShowStyle } from '../utils';
-  import { INITIAL_EVALUATION, RISK_LEVELS, SUGGESTIONS, calcManualScore, createManualGroups, getRiskLevel } from '../data';
-  import type { EvaluationResult, ManualGroup } from '../data';
   import { useMessage } from '/@/hooks/web/useMessage';
   import { useMessage } from '/@/hooks/web/useMessage';
-
+  import {
+    calculateSafetyEvaluation,
+    getSafetyEvaluationByScene,
+    getSafetyItemOptions,
+    getSafetySceneList,
+    saveSafetyEvaluation,
+  } from '../safetyEvaluation.api';
+  import { EVAL_RISK_LEVELS, EVAL_RISK_MAP, FIELD_BY_CODE, FALLBACK_SAFETY_ITEMS, FALLBACK_SCENE } from '../evaluation.config';
+  import type { SafetyEvaluationEntity, SafetyItem, SafetyScene } from '../evaluation.config';
+
+  const route = useRoute();
   const { createMessage } = useMessage();
   const { createMessage } = useMessage();
 
 
-  const groups = ref<ManualGroup[]>(createManualGroups());
+  const levels = EVAL_RISK_LEVELS;
+  const items = ref<SafetyItem[]>([]);
+  const scenes = ref<SafetyScene[]>([]);
+  const manageSystemId = ref('');
+  const evalType = ref(1);
+  const formValues = reactive<Record<string, string>>({});
   const collapsed = reactive<Record<string, boolean>>({});
   const collapsed = reactive<Record<string, boolean>>({});
-  const levels = RISK_LEVELS;
-  const result = ref<EvaluationResult>({ ...INITIAL_EVALUATION });
+  const evaluation = ref<SafetyEvaluationEntity | null>(null);
+  const loadingOptions = ref(false);
+  const loadingScenes = ref(false);
+  const previewing = ref(false);
+  const submitting = ref(false);
   const showReport = ref(false);
   const showReport = ref(false);
 
 
+  const busy = computed(() => loadingOptions.value || previewing.value || submitting.value);
+
+  const sceneSelectOptions = computed(() =>
+    scenes.value.map((scene) => ({
+      label: scene.systemname,
+      value: scene.id,
+      score: 0,
+    }))
+  );
+
+  const selectedScene = computed(() => scenes.value.find((scene) => scene.id === manageSystemId.value));
+
+  const targetName = computed(() => {
+    const scene = selectedScene.value;
+    if (!scene) return '未选择评价对象';
+    const name = scene.systemname;
+    return name.endsWith('工作面') || name.endsWith('采空区') ? name : `${name}工作面`;
+  });
+
+  const visibleItems = computed(() =>
+    items.value.filter((item) => {
+      const weight = evalType.value === 2 ? item.midWeight : item.preWeight;
+      return weight !== null && weight !== undefined;
+    })
+  );
+
+  const fieldGroups = computed(() => {
+    const groups = [
+      { key: 'pre', title: '采前评价', fields: visibleItems.value.filter((item) => item.code <= 6) },
+      { key: 'condition', title: '开采条件', fields: visibleItems.value.filter((item) => item.code >= 7 && item.code <= 11) },
+    ];
+    if (evalType.value === 2) {
+      groups.push({ key: 'monitor', title: '监测与措施', fields: visibleItems.value.filter((item) => item.code >= 12) });
+    }
+    return groups.filter((group) => group.fields.length);
+  });
+
+  const hasEvaluation = computed(() => !!evaluation.value);
+  const resultLevel = computed(() => evaluation.value?.riskLevel || '');
+  const resultLevelMeta = computed(() => EVAL_RISK_MAP[resultLevel.value] || null);
+  const resultRiskColor = computed(() => resultLevelMeta.value?.color || '#3df6ff');
+  const resultTagText = computed(() => resultLevel.value || '未评价');
+  const resultTarget = computed(() => evaluation.value?.workfaceName || targetName.value);
+  const resultTime = computed(() => evaluation.value?.createTime || '--');
+  const resultSuggestion = computed(() => resultLevelMeta.value?.suggestion || '请完成左侧指标选择并提交评价。');
+  const evalTypeText = computed(() => (evalType.value === 1 ? '采前评价' : '采中评价'));
+
+  const resultScore = computed(() => {
+    const value = evaluation.value?.totalScore;
+    if (value === null || value === undefined || value === '') return '--';
+    const score = Number(value);
+    return Number.isFinite(score) ? score.toFixed(2) : String(value);
+  });
+
+  function selectOptions(item: SafetyItem) {
+    return item.options.map((option) => ({
+      label: option.name,
+      value: option.name,
+      score: option.normalizedScore,
+    }));
+  }
+
   function toggleGroup(key: string) {
   function toggleGroup(key: string) {
     collapsed[key] = !collapsed[key];
     collapsed[key] = !collapsed[key];
   }
   }
@@ -157,22 +264,180 @@
     return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
     return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
   }
   }
 
 
-  function submit() {
-    const score = calcManualScore(groups.value);
-    const risk = getRiskLevel(score);
-    result.value = {
-      score,
-      level: risk.level,
-      levelText: risk.name,
-      color: risk.color,
-      highlightLevel: risk.level,
-      target: result.value.target,
-      time: formatDateTime(new Date()),
-      source: '人工录入',
-      suggestion: SUGGESTIONS[risk.level],
+  function normalizeItems(list: any[]): SafetyItem[] {
+    return (list || [])
+      .map((item) => ({
+        code: Number(item.code),
+        name: item.name,
+        preWeight: item.preWeight ?? null,
+        midWeight: item.midWeight ?? null,
+        options: (item.options || []).map((option: any) => {
+          if (typeof option === 'string') return { name: option, normalizedScore: 0 };
+          return { name: option.name, normalizedScore: option.normalizedScore ?? 0 };
+        }),
+      }))
+      .filter((item) => item.code && item.options.length);
+  }
+
+  function initDefaults() {
+    items.value.forEach((item) => {
+      const field = FIELD_BY_CODE[item.code];
+      if (field && !formValues[field]) {
+        formValues[field] = item.options[0]?.name || '';
+      }
+    });
+  }
+
+  async function loadItems() {
+    loadingOptions.value = true;
+    try {
+      const res: any = await getSafetyItemOptions();
+      const list = Array.isArray(res) ? res : (res?.result ?? []);
+      items.value = normalizeItems(list);
+      if (!items.value.length) {
+        items.value = normalizeItems(FALLBACK_SAFETY_ITEMS);
+      }
+    } catch {
+      items.value = normalizeItems(FALLBACK_SAFETY_ITEMS);
+      createMessage.warning('评价指标接口异常,已使用内置指标字典');
+    } finally {
+      loadingOptions.value = false;
+      initDefaults();
+    }
+  }
+
+  async function loadScenes() {
+    loadingScenes.value = true;
+    try {
+      const res: any = await getSafetySceneList();
+      const records = res?.records || res?.result?.records || [];
+      const list: SafetyScene[] = records.map((record: any) => ({
+        id: String(record.id ?? record.deviceID ?? ''),
+        systemname: record.systemname || record.name || record.sysname || '未命名场景',
+      }));
+      scenes.value = list.length ? list : [{ ...FALLBACK_SCENE }];
+    } catch {
+      scenes.value = [{ ...FALLBACK_SCENE }];
+    } finally {
+      loadingScenes.value = false;
+      const queryScene = (route.query.systemID || route.query.manageSystemId || route.query.sceneId) as string;
+      if (queryScene && !scenes.value.some((scene) => scene.id === queryScene)) {
+        scenes.value.unshift({ id: queryScene, systemname: '当前场景' });
+      }
+      manageSystemId.value = queryScene || scenes.value[0]?.id || '';
+    }
+  }
+
+  function applyEvaluation(entity: SafetyEvaluationEntity) {
+    if (!entity) return;
+    evaluation.value = {
+      ...entity,
+      createTime: entity.createTime || formatDateTime(new Date()),
     };
     };
-    createMessage.success('评价完成,风险分值已更新');
+    items.value.forEach((item) => {
+      const field = FIELD_BY_CODE[item.code];
+      const value = entity[field];
+      if (field && value) {
+        formValues[field] = value;
+      }
+    });
   }
   }
+
+  async function loadLatest() {
+    evaluation.value = null;
+    if (!manageSystemId.value) {
+      return;
+    }
+    try {
+      const res: any = await getSafetyEvaluationByScene(manageSystemId.value);
+      const data = res?.result ?? res;
+      const entity = evalType.value === 2 ? data?.miningEval : data?.beforeEval;
+      if (entity) {
+        applyEvaluation(entity);
+      } else {
+        evaluation.value = null;
+      }
+    } catch {
+      evaluation.value = null;
+    }
+  }
+
+  function changeEvalType(type: number) {
+    if (evalType.value === type) return;
+    evalType.value = type;
+    evaluation.value = null;
+    loadLatest();
+  }
+
+  function buildPayload() {
+    const payload: Record<string, any> = {
+      evalType: evalType.value,
+      workfaceName: targetName.value,
+    };
+    if (manageSystemId.value) {
+      payload.manageSystemId = manageSystemId.value;
+    }
+    visibleItems.value.forEach((item) => {
+      const field = FIELD_BY_CODE[item.code];
+      if (field) {
+        payload[field] = formValues[field];
+      }
+    });
+    return payload;
+  }
+
+  function validateForm() {
+    for (const item of visibleItems.value) {
+      const field = FIELD_BY_CODE[item.code];
+      if (field && !formValues[field]) {
+        createMessage.warning(`请选择【${item.name}】`);
+        return false;
+      }
+    }
+    return true;
+  }
+
+  function getApiErrorMessage(error: any, fallback: string) {
+    const status = error?.response?.status || error?.status || error?.code;
+    if (status === 404) {
+      return '评价接口未部署(404),请确认后端服务';
+    }
+    return error?.message || error?.msg || fallback;
+  }
+
+  async function preview() {
+    if (!validateForm()) return;
+    previewing.value = true;
+    try {
+      const res: any = await calculateSafetyEvaluation(buildPayload());
+      applyEvaluation(res?.result ?? res);
+      createMessage.success('计算完成');
+    } catch (error) {
+      createMessage.error(getApiErrorMessage(error, '计算失败,请检查指标选项'));
+    } finally {
+      previewing.value = false;
+    }
+  }
+
+  async function submit() {
+    if (!validateForm()) return;
+    submitting.value = true;
+    try {
+      const res: any = await saveSafetyEvaluation(buildPayload());
+      applyEvaluation(res?.result ?? res);
+      createMessage.success('评价已保存');
+    } catch (error) {
+      createMessage.error(getApiErrorMessage(error, '评价保存失败'));
+    } finally {
+      submitting.value = false;
+    }
+  }
+
+  onMounted(async () => {
+    await loadItems();
+    await loadScenes();
+    await loadLatest();
+  });
 </script>
 </script>
 
 
 <style lang="less" scoped>
 <style lang="less" scoped>
@@ -182,6 +447,7 @@
   }
   }
 
 
   .manual-form {
   .manual-form {
+    position: relative;
     display: flex;
     display: flex;
     flex-direction: column;
     flex-direction: column;
     height: 100%;
     height: 100%;
@@ -189,14 +455,75 @@
     box-sizing: border-box;
     box-sizing: border-box;
   }
   }
 
 
+  .form-toolbar {
+    flex: none;
+  }
+
+  .target-row {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    margin-bottom: 8px;
+
+    :deep(.ignition-select) {
+      flex: 1;
+      min-width: 0;
+      height: 32px;
+      font-size: 13px;
+    }
+  }
+
+  .target-label {
+    flex: none;
+    color: #fff;
+    font-size: 13px;
+    white-space: nowrap;
+  }
+
+  .eval-type-tabs {
+    display: flex;
+    gap: 6px;
+    margin-bottom: 8px;
+
+    span {
+      flex: 1;
+      height: 30px;
+      line-height: 30px;
+      border: 1px solid rgba(61, 246, 255, 0.25);
+      background: rgba(8, 42, 70, 0.6);
+      color: #a9dcf4;
+      font-size: 13px;
+      text-align: center;
+      cursor: pointer;
+      transition: all 0.2s;
+
+      &.active {
+        border-color: rgba(61, 246, 255, 0.7);
+        background: linear-gradient(180deg, #2a5985 0%, #1d4773 100%);
+        color: #fff;
+      }
+    }
+  }
+
   .group-list {
   .group-list {
     flex: 1;
     flex: 1;
     min-height: 0;
     min-height: 0;
+    overflow-y: auto;
+    overflow-x: hidden;
     padding-right: 2px;
     padding-right: 2px;
+
+    &::-webkit-scrollbar {
+      width: 4px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: rgba(61, 246, 255, 0.35);
+      border-radius: 2px;
+    }
   }
   }
 
 
   .form-group {
   .form-group {
-    margin-bottom: 12px;
+    margin-bottom: 8px;
 
 
     &:last-child {
     &:last-child {
       margin-bottom: 0;
       margin-bottom: 0;
@@ -307,22 +634,56 @@
     align-items: center;
     align-items: center;
     gap: 6px;
     gap: 6px;
     min-width: 0;
     min-width: 0;
+    min-height: 42px;
 
 
     :deep(.ignition-select) {
     :deep(.ignition-select) {
       flex: 1;
       flex: 1;
       min-width: 0;
       min-width: 0;
-      height: 38px;
-      font-size: 14px;
+      height: 34px;
+      font-size: 13px;
     }
     }
   }
   }
 
 
   .field-label {
   .field-label {
-    width: 88px;
+    width: 126px;
     flex: none;
     flex: none;
     text-align: right;
     text-align: right;
     color: #fff;
     color: #fff;
-    font-size: 14px;
-    white-space: nowrap;
+    font-size: 12px;
+    line-height: 1.3;
+    white-space: normal;
+    word-break: break-all;
+  }
+
+  .form-loading {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    min-height: 160px;
+    color: #a9dcf4;
+    font-size: 13px;
+  }
+
+  .form-actions {
+    display: flex;
+    flex: none;
+    gap: 8px;
+    margin-top: 8px;
+
+    .action-button {
+      flex: 1;
+      width: auto;
+      margin-top: 0;
+    }
+  }
+
+  .preview-button {
+    background: linear-gradient(180deg, rgba(13, 74, 115, 0.9) 0%, rgba(8, 47, 77, 0.95) 100%);
+
+    &::after {
+      display: none;
+    }
   }
   }
 
 
   .result-panel {
   .result-panel {
@@ -350,7 +711,7 @@
 
 
   .score-label {
   .score-label {
     color: #fff;
     color: #fff;
-    font-size: 20px;
+    font-size: 19px;
     letter-spacing: 1px;
     letter-spacing: 1px;
   }
   }
 
 
@@ -415,7 +776,7 @@
     white-space: nowrap;
     white-space: nowrap;
     -webkit-mask: url('@/assets/images/vent/home/selfIgnition/risk-tag.svg') no-repeat center / 100% 100%;
     -webkit-mask: url('@/assets/images/vent/home/selfIgnition/risk-tag.svg') no-repeat center / 100% 100%;
     mask: url('@/assets/images/vent/home/selfIgnition/risk-tag.svg') no-repeat center / 100% 100%;
     mask: url('@/assets/images/vent/home/selfIgnition/risk-tag.svg') no-repeat center / 100% 100%;
-    background: var(--risk-color, #ff9d1c);
+    background: var(--risk-color, #3df6ff);
   }
   }
 
 
   .result-info {
   .result-info {
@@ -500,7 +861,7 @@
     }
     }
 
 
     &.highlight {
     &.highlight {
-      outline: 1px solid #ff9d1c;
+      outline: 1px solid var(--highlight-color, #ff9d1c);
       outline-offset: -1px;
       outline-offset: -1px;
       background-color: rgba(255, 157, 28, 0.08);
       background-color: rgba(255, 157, 28, 0.08);
       box-shadow: inset 0 0 18px rgba(255, 157, 28, 0.16);
       box-shadow: inset 0 0 18px rgba(255, 157, 28, 0.16);
@@ -577,11 +938,20 @@
       filter 0.2s,
       filter 0.2s,
       box-shadow 0.2s;
       box-shadow 0.2s;
 
 
-    &:hover {
+    &:hover:not(:disabled) {
       filter: brightness(1.12);
       filter: brightness(1.12);
       box-shadow: 0 0 16px rgba(61, 246, 255, 0.35);
       box-shadow: 0 0 16px rgba(61, 246, 255, 0.35);
     }
     }
 
 
+    &:disabled {
+      opacity: 0.5;
+      cursor: not-allowed;
+
+      &::after {
+        display: none;
+      }
+    }
+
     &::after {
     &::after {
       content: '';
       content: '';
       position: absolute;
       position: absolute;
@@ -594,10 +964,6 @@
     }
     }
   }
   }
 
 
-  .submit-button {
-    margin-top: 8px;
-  }
-
   .report-dialog {
   .report-dialog {
     width: 540px;
     width: 540px;
     padding: 22px 26px 24px;
     padding: 22px 26px 24px;

+ 388 - 410
src/views/vent/home/selfIgnition/components/MonitorView.vue

@@ -1,243 +1,203 @@
 <template>
 <template>
   <div class="ignition-view monitor-view">
   <div class="ignition-view monitor-view">
-    <!-- 左上:设备运行状态 -->
+    <!-- 左上:自然发火阶段 -->
     <ModuleCommon
     <ModuleCommon
-      :show-style="panelShowStyle(23, 105, 328, 430)"
+      :show-style="panelShowStyle(20, 77, 380, 330)"
       :module-data="EMPTY_MODULE_DATA"
       :module-data="EMPTY_MODULE_DATA"
-      module-name="设备运行状态"
-      device-type="ignition_device_status"
+      module-name="自然发火阶段"
+      device-type="ignition_fire_stage"
       :data="{}"
       :data="{}"
       :visible="true"
       :visible="true"
     >
     >
-      <div class="device-panel">
-        <div v-for="device in devices" :key="device.name" class="device-item">
-          <span class="device-badge"><i></i></span>
-          <div class="device-main">
-            <div class="device-line">
-              <span class="device-name">{{ device.name }}</span>
-              <span class="device-rate">{{ runningRate(device) }}%</span>
-            </div>
-            <div class="device-bar"><i :style="{ width: `${runningRate(device)}%` }"></i></div>
-            <div class="device-meta">
-              安装 <em>{{ device.installed }}</em> 套 · 运行 <em>{{ device.running }}</em> 套
-            </div>
-          </div>
+      <div class="stage-panel">
+        <button class="evaluation-entry" @click="emit('view-evaluation')">查看评价结果</button>
+        <div class="stage-level" :style="{ color: NATURAL_FIRE_STAGE.color }">
+          {{ NATURAL_FIRE_STAGE.levelText }} {{ NATURAL_FIRE_STAGE.levelName }}
         </div>
         </div>
+        <div class="stage-bar"></div>
       </div>
       </div>
     </ModuleCommon>
     </ModuleCommon>
 
 
-    <!-- 左下:实时预警信息 -->
+    <!-- 左中:光纤测温 -->
     <ModuleCommon
     <ModuleCommon
-      :show-style="panelShowStyle(23, 555, 328, 420)"
+      class="fiber-module"
+      :show-style="panelShowStyle(20, 425, 380, 237)"
       :module-data="EMPTY_MODULE_DATA"
       :module-data="EMPTY_MODULE_DATA"
-      module-name="实时预警信息"
-      device-type="ignition_realtime_alert"
+      module-name="光纤测温"
+      device-type="ignition_fiber_temperature"
       :data="{}"
       :data="{}"
       :visible="true"
       :visible="true"
     >
     >
-      <div class="alert-panel">
-        <div v-for="alert in alerts" :key="alert.id" class="alert-item" :style="{ '--alert-color': alertColor(alert.level) }">
-          <div class="alert-top">
-            <span class="alert-time">{{ alert.time }}</span>
-            <span class="alert-level">{{ alertLevelName(alert.level) }}</span>
-            <span class="alert-location">{{ alert.location }}</span>
+      <div class="fiber-panel">
+        <FiberTemperatureChart :data="FIBER_CHART_DATA" :active="active" />
+      </div>
+    </ModuleCommon>
+
+    <!-- 右上:传感器监测 -->
+    <ModuleCommon
+      :show-style="panelShowStyle(1519, 77, 379, 330)"
+      :module-data="EMPTY_MODULE_DATA"
+      module-name="传感器监测"
+      device-type="ignition_sensor_monitor"
+      :data="{}"
+      :visible="true"
+    >
+      <div class="sensor-panel">
+        <div v-for="group in SENSOR_GROUPS" :key="group.name" class="sensor-group">
+          <div class="sensor-group-title">{{ group.name }}</div>
+          <div v-for="row in group.rows" :key="row.label" class="sensor-row">
+            <span>{{ row.label }}:</span>
+            <em>{{ row.value }}</em>
           </div>
           </div>
-          <div class="alert-message">{{ alert.message }}</div>
         </div>
         </div>
       </div>
       </div>
     </ModuleCommon>
     </ModuleCommon>
 
 
-    <!-- 右上:束管监测实时分析 -->
+    <!-- 右中:预警处置意见 -->
     <ModuleCommon
     <ModuleCommon
-      :show-style="panelShowStyle(380, 145, 1477, 390)"
+      :show-style="panelShowStyle(1519, 425, 379, 237)"
       :module-data="EMPTY_MODULE_DATA"
       :module-data="EMPTY_MODULE_DATA"
-      module-name="束管监测实时分析"
+      module-name="预警处置意见"
+      device-type="ignition_alert_advice"
+      :data="{}"
+      :visible="true"
+    >
+      <div class="advice-panel">
+        <span class="advice-icon"></span>
+        <p>{{ ALERT_ADVICE }}</p>
+      </div>
+    </ModuleCommon>
+
+    <!-- 左下:束管监测 -->
+    <ModuleCommon
+      class="wide-module"
+      :show-style="panelShowStyle(20, 710, 930, 349)"
+      :module-data="EMPTY_MODULE_DATA"
+      module-name="束管监测"
       device-type="ignition_bundle_monitor"
       device-type="ignition_bundle_monitor"
       :data="{}"
       :data="{}"
       :visible="true"
       :visible="true"
     >
     >
-      <div class="bundle-panel">
-        <div class="bundle-head">
-          <span class="col-point">测点名称</span>
-          <span class="col-value">O₂(%)</span>
-          <span class="col-value">CO(ppm)</span>
-          <span class="col-value">CH₄(%)</span>
-          <span class="col-value">C₂H₄(ppm)</span>
-          <span class="col-value">温度(℃)</span>
-          <span class="col-status">状态</span>
-        </div>
-        <div class="bundle-body">
-          <div v-for="row in bundleRows" :key="row.point" class="bundle-row">
-            <span class="col-point">{{ row.point }}</span>
-            <span class="col-value" :class="{ warning: Number(row.o2) < 18 }">{{ row.o2 }}</span>
-            <span class="col-value" :class="{ warning: row.co >= 24 }">{{ row.co }}</span>
-            <span class="col-value">{{ row.ch4 }}</span>
-            <span class="col-value">{{ row.c2h4 }}</span>
-            <span class="col-value">{{ row.temp.toFixed(1) }}</span>
-            <span class="col-status">
-              <i class="status-tag" :class="statusClass(row.status)">{{ row.status }}</i>
-            </span>
+      <div class="table-panel">
+        <div class="data-table bundle-table">
+          <div class="table-head">
+            <span>设备编号</span>
+            <span>氧气</span>
+            <span>氮气</span>
+            <span>一氧化碳</span>
+            <span>二氧化碳</span>
+            <span>甲烷</span>
+            <span>乙烯</span>
+            <span>乙烷</span>
+            <span>乙炔</span>
+          </div>
+          <div ref="bundleBodyRef" class="table-body">
+            <div v-for="(row, index) in pagedBundleRows" :key="`${row.deviceNo}-${bundlePage}-${index}`" class="table-row">
+              <span>{{ row.deviceNo }}</span>
+              <span>{{ row.o2 }}</span>
+              <span>{{ row.n2 }}</span>
+              <span>{{ row.co }}</span>
+              <span>{{ row.co2 }}</span>
+              <span>{{ row.ch4 }}</span>
+              <span>{{ row.c2h4 }}</span>
+              <span>{{ row.c2h6 }}</span>
+              <span>{{ row.c2h2 }}</span>
+            </div>
           </div>
           </div>
         </div>
         </div>
+        <div class="table-pager">
+          <i v-for="page in totalPages" :key="page" :class="{ active: page === bundlePage }" @click="changeBundlePage(page)"></i>
+        </div>
       </div>
       </div>
     </ModuleCommon>
     </ModuleCommon>
 
 
-    <!-- 右下:光纤测温趋势分析 -->
+    <!-- 右下:设备实时监测与分析 -->
     <ModuleCommon
     <ModuleCommon
-      :show-style="panelShowStyle(380, 555, 1477, 420)"
+      class="wide-module"
+      :show-style="panelShowStyle(969, 710, 929, 349)"
       :module-data="EMPTY_MODULE_DATA"
       :module-data="EMPTY_MODULE_DATA"
-      module-name="光纤测温趋势分析"
-      device-type="ignition_fiber_trend"
+      module-name="设备实时监测与分析"
+      device-type="ignition_device_monitor"
       :data="{}"
       :data="{}"
       :visible="true"
       :visible="true"
     >
     >
-      <div class="fiber-panel">
-        <div class="chart-area">
-          <div class="chart-legend">
-            <span class="legend-item"><i class="legend-max"></i>最高温度</span>
-            <span class="legend-item"><i class="legend-avg"></i>平均温度</span>
+      <div class="table-panel">
+        <div class="data-table device-table">
+          <div class="table-head">
+            <span>设备编号</span>
+            <span>氧气</span>
+            <span>一氧化碳</span>
+            <span>二氧化碳</span>
+            <span>甲烷</span>
+            <span>温度</span>
+            <span>压差</span>
           </div>
           </div>
-          <svg class="chart-svg" viewBox="0 0 1100 300" preserveAspectRatio="none">
-            <defs>
-              <linearGradient id="ignitionFiberArea" x1="0" y1="0" x2="0" y2="1">
-                <stop offset="0" stop-color="#3df6ff" stop-opacity="0.28" />
-                <stop offset="1" stop-color="#3df6ff" stop-opacity="0" />
-              </linearGradient>
-            </defs>
-            <g>
-              <line
-                v-for="tick in yTicks"
-                :key="`grid-${tick}`"
-                :x1="chartPadding.left"
-                :x2="chartWidth - chartPadding.right"
-                :y1="yPosition(tick)"
-                :y2="yPosition(tick)"
-                stroke="rgba(61, 246, 255, 0.14)"
-                stroke-width="1"
-                vector-effect="non-scaling-stroke"
-              />
-              <text
-                v-for="tick in yTicks"
-                :key="`label-${tick}`"
-                :x="chartPadding.left - 10"
-                :y="yPosition(tick) + 4"
-                fill="#8fc7e8"
-                font-size="11"
-                text-anchor="end"
-              >
-                {{ tick }}℃
-              </text>
-            </g>
-            <path :d="maxAreaPath" fill="url(#ignitionFiberArea)" />
-            <polyline :points="maxPoints" fill="none" stroke="#3df6ff" stroke-width="2" vector-effect="non-scaling-stroke" stroke-linejoin="round" />
-            <polyline
-              :points="avgPoints"
-              fill="none"
-              stroke="#ffd15c"
-              stroke-width="1.5"
-              stroke-dasharray="5 3"
-              vector-effect="non-scaling-stroke"
-              stroke-linejoin="round"
-            />
-            <g>
-              <text v-for="item in xLabels" :key="item.text" :x="item.x" :y="chartHeight - 8" fill="#8fc7e8" font-size="11" text-anchor="middle">
-                {{ item.text }}
-              </text>
-            </g>
-          </svg>
-        </div>
-        <div class="fiber-summary">
-          <div class="summary-item">
-            <span>最高温度</span>
-            <em class="max">{{ fiber.maxValue.toFixed(1) }}℃</em>
-          </div>
-          <div class="summary-item">
-            <span>平均温度</span>
-            <em class="avg">{{ fiber.avgValue.toFixed(1) }}℃</em>
-          </div>
-          <div class="summary-item">
-            <span>测温点数</span>
-            <em>{{ fiber.pointCount }} 个</em>
+          <div ref="deviceBodyRef" class="table-body">
+            <div v-for="(row, index) in pagedDeviceRows" :key="`${row.deviceNo}-${devicePage}-${index}`" class="table-row">
+              <span>{{ row.deviceNo }}</span>
+              <span>{{ row.o2 }}</span>
+              <span>{{ row.co }}</span>
+              <span>{{ row.co2 }}</span>
+              <span>{{ row.ch4 }}</span>
+              <span>{{ row.temp }}</span>
+              <span>{{ row.pressureDiff }}</span>
+            </div>
           </div>
           </div>
         </div>
         </div>
+        <div class="table-pager">
+          <i v-for="page in totalPages" :key="page" :class="{ active: page === devicePage }" @click="changeDevicePage(page)"></i>
+        </div>
       </div>
       </div>
     </ModuleCommon>
     </ModuleCommon>
   </div>
   </div>
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
-  import { computed } from 'vue';
+  import { computed, nextTick, ref } from 'vue';
   import ModuleCommon from '../../configurable/components/ModuleCommon.vue';
   import ModuleCommon from '../../configurable/components/ModuleCommon.vue';
+  import FiberTemperatureChart from './FiberTemperatureChart.vue';
   import { EMPTY_MODULE_DATA, panelShowStyle } from '../utils';
   import { EMPTY_MODULE_DATA, panelShowStyle } from '../utils';
-  import { RISK_LEVELS } from '../data';
-  import type { AlertItem, BundleRow, DeviceInstallItem, FiberTrend } from '../data';
-
-  const props = defineProps<{
-    devices: DeviceInstallItem[];
-    alerts: AlertItem[];
-    bundleRows: BundleRow[];
-    fiber: FiberTrend;
-  }>();
+  import { ALERT_ADVICE, BUNDLE_MONITOR_ROWS, DEVICE_MONITOR_ROWS, FIBER_CHART_DATA, NATURAL_FIRE_STAGE, SENSOR_GROUPS } from '../data';
 
 
-  const chartWidth = 1100;
-  const chartHeight = 300;
-  const chartPadding = { left: 52, right: 16, top: 16, bottom: 30 };
-  const yMin = 10;
-  const yMax = 40;
-  const yTicks = [10, 20, 30, 40];
+  defineProps<{
+    active?: boolean;
+  }>();
 
 
-  function runningRate(device: DeviceInstallItem) {
-    if (!device.installed) return '0';
-    return ((device.running / device.installed) * 100).toFixed(1);
-  }
+  const emit = defineEmits<{
+    (event: 'view-evaluation'): void;
+  }>();
 
 
-  function alertColor(level: number) {
-    return RISK_LEVELS[Math.min(Math.max(level, 1), 5) - 1].color;
-  }
+  const PAGE_SIZE = 8;
+  const bundlePage = ref(1);
+  const devicePage = ref(1);
+  const bundleBodyRef = ref<HTMLElement>();
+  const deviceBodyRef = ref<HTMLElement>();
 
 
-  function alertLevelName(level: number) {
-    return RISK_LEVELS[Math.min(Math.max(level, 1), 5) - 1].name.replace(/^[IVX]+\s*/, '');
-  }
+  const totalPages = computed(() => Math.max(1, Math.ceil(BUNDLE_MONITOR_ROWS.length / PAGE_SIZE)));
 
 
-  function statusClass(status: BundleRow['status']) {
-    if (status === '预警') return 'warning';
-    if (status === '关注') return 'attention';
-    return 'normal';
-  }
+  const pagedBundleRows = computed(() => {
+    const start = (bundlePage.value - 1) * PAGE_SIZE;
+    return BUNDLE_MONITOR_ROWS.slice(start, start + PAGE_SIZE);
+  });
 
 
-  function xPosition(index: number) {
-    const plotWidth = chartWidth - chartPadding.left - chartPadding.right;
-    const step = plotWidth / Math.max(1, props.fiber.times.length - 1);
-    return chartPadding.left + index * step;
-  }
+  const pagedDeviceRows = computed(() => {
+    const start = (devicePage.value - 1) * PAGE_SIZE;
+    return DEVICE_MONITOR_ROWS.slice(start, start + PAGE_SIZE);
+  });
 
 
-  function yPosition(value: number) {
-    const plotHeight = chartHeight - chartPadding.top - chartPadding.bottom;
-    const ratio = (value - yMin) / (yMax - yMin);
-    return chartPadding.top + (1 - Math.min(Math.max(ratio, 0), 1)) * plotHeight;
+  function changeBundlePage(page: number) {
+    bundlePage.value = page;
+    nextTick(() => {
+      if (bundleBodyRef.value) bundleBodyRef.value.scrollTop = 0;
+    });
   }
   }
 
 
-  function toPoints(values: number[]) {
-    return values.map((value, index) => `${xPosition(index)},${yPosition(value)}`).join(' ');
+  function changeDevicePage(page: number) {
+    devicePage.value = page;
+    nextTick(() => {
+      if (deviceBodyRef.value) deviceBodyRef.value.scrollTop = 0;
+    });
   }
   }
-
-  const maxPoints = computed(() => toPoints(props.fiber.maxTemp));
-  const avgPoints = computed(() => toPoints(props.fiber.avgTemp));
-
-  const maxAreaPath = computed(() => {
-    const plotBottom = chartHeight - chartPadding.bottom;
-    const firstX = xPosition(0);
-    const lastX = xPosition(props.fiber.maxTemp.length - 1);
-    return `M ${firstX},${plotBottom} L ${maxPoints.value.replace(/ /g, ' L ')} L ${lastX},${plotBottom} Z`;
-  });
-
-  const xLabels = computed(() => {
-    const indexes = [0, 4, 8, 12, 16, 20, props.fiber.times.length - 1];
-    return indexes.map((index) => ({
-      x: xPosition(index),
-      text: index === props.fiber.times.length - 1 ? '24:00' : props.fiber.times[index],
-    }));
-  });
 </script>
 </script>
 
 
 <style lang="less" scoped>
 <style lang="less" scoped>
@@ -246,106 +206,140 @@
     inset: 0;
     inset: 0;
   }
   }
 
 
-  .device-panel {
+  /* 自然发火阶段 */
+  .stage-panel {
     display: flex;
     display: flex;
     flex-direction: column;
     flex-direction: column;
-    gap: 9px;
+    align-items: center;
     height: 100%;
     height: 100%;
-    padding: 12px 12px 14px;
+    padding-top: 34px;
     box-sizing: border-box;
     box-sizing: border-box;
+    background: radial-gradient(ellipse at 50% 32%, rgba(255, 157, 28, 0.16), transparent 62%);
   }
   }
 
 
-  .device-item {
-    display: flex;
-    align-items: center;
-    flex: 1;
-    min-height: 0;
-    gap: 10px;
-    padding: 6px 12px 6px 10px;
-    box-sizing: border-box;
-    border: 1px solid rgba(61, 246, 255, 0.16);
-    background: linear-gradient(90deg, rgba(11, 57, 90, 0.72), rgba(8, 37, 61, 0.3));
+  .evaluation-entry {
+    width: 160px;
+    height: 40px;
+    border: 1px solid rgba(61, 246, 255, 0.65);
+    border-radius: 5px;
+    background: linear-gradient(180deg, #1b7fbe 0%, #0b4c7b 100%);
+    box-shadow: 0 0 14px rgba(61, 246, 255, 0.22);
+    color: #fff;
+    font-size: 15px;
+    letter-spacing: 1px;
+    cursor: pointer;
+    transition:
+      filter 0.2s,
+      box-shadow 0.2s;
+
+    &:hover {
+      filter: brightness(1.12);
+      box-shadow: 0 0 18px rgba(61, 246, 255, 0.42);
+    }
   }
   }
 
 
-  .device-badge {
-    position: relative;
-    width: 30px;
-    height: 30px;
-    flex: none;
-    border: 1px solid rgba(61, 246, 255, 0.55);
-    border-radius: 50%;
-    background: radial-gradient(circle, rgba(61, 246, 255, 0.28), rgba(5, 32, 55, 0.4));
+  .stage-level {
+    margin-top: 26px;
+    font-size: 42px;
+    line-height: 1.15;
+    letter-spacing: 4px;
+    white-space: nowrap;
+    text-shadow:
+      0 0 16px currentColor,
+      0 0 30px rgba(255, 157, 28, 0.35);
+  }
 
 
-    i {
+  .stage-bar {
+    position: relative;
+    width: 300px;
+    height: 42px;
+    margin-top: 20px;
+    clip-path: polygon(2% 0, 98% 0, 89% 100%, 11% 100%);
+    background: linear-gradient(180deg, rgba(61, 246, 255, 0.22) 0%, rgba(9, 39, 65, 0.9) 55%, rgba(4, 20, 35, 0.95) 100%);
+    filter: drop-shadow(0 6px 10px rgba(61, 246, 255, 0.25));
+
+    &::before {
+      content: '';
       position: absolute;
       position: absolute;
-      left: 50%;
-      top: 50%;
-      width: 10px;
-      height: 10px;
-      margin: -5px 0 0 -5px;
-      border-radius: 2px;
-      background: #3df6ff;
-      box-shadow: 0 0 9px rgba(61, 246, 255, 0.9);
+      top: 0;
+      left: 8%;
+      right: 8%;
+      height: 2px;
+      background: linear-gradient(90deg, transparent, #ff9d1c, #ffd15c, #ff9d1c, transparent);
+      box-shadow: 0 0 10px rgba(255, 157, 28, 0.65);
     }
     }
-  }
 
 
-  .device-main {
-    flex: 1;
-    min-width: 0;
+    &::after {
+      content: '';
+      position: absolute;
+      left: 10%;
+      right: 10%;
+      bottom: 0;
+      height: 1px;
+      background: linear-gradient(90deg, transparent, #3df6ff, transparent);
+      box-shadow: 0 0 12px rgba(61, 246, 255, 0.8);
+    }
   }
   }
 
 
-  .device-line {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
+  /* 光纤测温 */
+  :deep(.fiber-module .box1-center) {
+    height: calc(100% - 35px) !important;
   }
   }
 
 
-  .device-name {
-    color: #fff;
-    font-size: 14px;
+  .fiber-panel {
+    height: 100%;
+    padding: 2px 6px 2px;
+    box-sizing: border-box;
   }
   }
 
 
-  .device-rate {
-    color: #3df6ff;
-    font-family: electronicFont, sans-serif;
-    font-size: 18px;
+  /* 传感器监测 */
+  .sensor-panel {
+    display: flex;
+    flex-direction: column;
+    gap: 14px;
+    height: 100%;
+    padding: 10px 14px;
+    box-sizing: border-box;
   }
   }
 
 
-  .device-bar {
-    position: relative;
-    height: 4px;
-    margin: 7px 0 6px;
-    overflow: hidden;
-    background: rgba(61, 246, 255, 0.12);
-
-    i {
-      display: block;
-      height: 100%;
-      background: linear-gradient(90deg, #0e7fc1, #3df6ff);
-      box-shadow: 0 0 8px rgba(61, 246, 255, 0.7);
-      transition: width 0.4s ease;
-    }
+  .sensor-group-title {
+    height: 24px;
+    padding-left: 8px;
+    border-left: 2px solid #3df6ff;
+    background: linear-gradient(90deg, rgba(61, 246, 255, 0.55) 0%, rgba(61, 246, 255, 0.08) 70%, transparent 100%);
+    color: #fff;
+    font-size: 13px;
+    line-height: 24px;
+    letter-spacing: 1px;
   }
   }
 
 
-  .device-meta {
-    color: #9fcde3;
+  .sensor-row {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    height: 27px;
+    padding: 0 4px 0 10px;
+    border-bottom: 1px solid rgba(61, 246, 255, 0.12);
+    color: #d9f7ff;
     font-size: 12px;
     font-size: 12px;
 
 
     em {
     em {
       color: #3df6ff;
       color: #3df6ff;
       font-style: normal;
       font-style: normal;
       font-size: 13px;
       font-size: 13px;
+      text-shadow: 0 0 8px rgba(61, 246, 255, 0.45);
     }
     }
   }
   }
 
 
-  .alert-panel {
-    display: flex;
-    flex-direction: column;
-    gap: 8px;
+  /* 预警处置意见 */
+  .advice-panel {
+    position: relative;
     height: 100%;
     height: 100%;
-    padding: 12px;
+    padding: 14px 16px 14px 34px;
     box-sizing: border-box;
     box-sizing: border-box;
     overflow-y: auto;
     overflow-y: auto;
+    border: 1px solid rgba(61, 246, 255, 0.4);
+    background: url('@/assets/images/vent/home/selfIgnition/result-block-bg.png') no-repeat center / 100% 100%;
 
 
     &::-webkit-scrollbar {
     &::-webkit-scrollbar {
       width: 4px;
       width: 4px;
@@ -355,231 +349,215 @@
       background: rgba(61, 246, 255, 0.35);
       background: rgba(61, 246, 255, 0.35);
       border-radius: 2px;
       border-radius: 2px;
     }
     }
-  }
 
 
-  .alert-item {
-    flex: none;
-    padding: 8px 10px 8px 12px;
-    border-left: 2px solid var(--alert-color);
-    background: linear-gradient(90deg, rgba(11, 57, 90, 0.72), rgba(8, 37, 61, 0.25));
-  }
-
-  .alert-top {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    height: 20px;
+    p {
+      margin: 0;
+      color: #fff;
+      font-size: 14px;
+      line-height: 27px;
+      letter-spacing: 0.5px;
+    }
   }
   }
 
 
-  .alert-time {
-    color: #8fc7e8;
-    font-size: 12px;
-    font-family: electronicFont, sans-serif;
-    letter-spacing: 1px;
+  .advice-icon {
+    position: absolute;
+    left: 16px;
+    top: 20px;
+    width: 9px;
+    height: 9px;
+    background: #f5222d;
+    box-shadow: 0 0 9px rgba(245, 34, 45, 0.85);
+    transform: rotate(45deg);
   }
   }
 
 
-  .alert-level {
-    padding: 0 5px;
-    border: 1px solid var(--alert-color);
-    color: var(--alert-color);
-    font-size: 11px;
-    line-height: 16px;
+  /* 底部宽表格面板:使用短边框图,保证标题和装饰点居中 */
+  :deep(.wide-module .box1-top) {
+    --image-box1-top: url('@/assets/images/vent/border/box1-top.png');
+    background-image: var(--image-box1-top) !important;
   }
   }
 
 
-  .alert-location {
-    margin-left: auto;
-    color: #fff;
-    font-size: 12px;
+  :deep(.wide-module .box1-bottom) {
+    --image-box1-bottom: url('@/assets/images/vent/border/box1-bottom.png');
+    background-image: var(--image-box1-bottom) !important;
   }
   }
 
 
-  .alert-message {
-    margin-top: 5px;
-    color: #fff;
-    font-size: 13px;
-    line-height: 20px;
-  }
-
-  .bundle-panel {
+  .table-panel {
     display: flex;
     display: flex;
     flex-direction: column;
     flex-direction: column;
     height: 100%;
     height: 100%;
-    padding: 12px 16px 14px;
+    padding: 4px 12px 6px;
     box-sizing: border-box;
     box-sizing: border-box;
   }
   }
 
 
-  .bundle-head,
-  .bundle-row {
+  .data-table {
+    display: flex;
+    flex: 1;
+    min-height: 0;
+    flex-direction: column;
+    border: 1px solid rgba(61, 246, 255, 0.4);
+    background: rgba(6, 30, 52, 0.2);
+  }
+
+  .table-head,
+  .table-row {
     display: flex;
     display: flex;
     align-items: center;
     align-items: center;
     text-align: center;
     text-align: center;
   }
   }
 
 
-  .bundle-head {
+  .table-head {
     flex: none;
     flex: none;
     height: 38px;
     height: 38px;
-    border: 1px solid rgba(61, 246, 255, 0.4);
+    border-bottom: 1px solid rgba(61, 246, 255, 0.35);
+    background: linear-gradient(180deg, #0d3b58 0%, #0a2c48 100%);
     color: #3df6ff;
     color: #3df6ff;
     font-size: 14px;
     font-size: 14px;
-    letter-spacing: 1px;
-    background: linear-gradient(180deg, rgba(14, 70, 108, 0.85), rgba(10, 49, 79, 0.6));
+    letter-spacing: 0.5px;
   }
   }
 
 
-  .bundle-body {
-    display: flex;
+  .table-body {
     flex: 1;
     flex: 1;
     min-height: 0;
     min-height: 0;
-    flex-direction: column;
-    border: 1px solid rgba(61, 246, 255, 0.18);
-    border-top: none;
+    overflow-y: auto;
+
+    &::-webkit-scrollbar {
+      width: 8px;
+    }
+
+    &::-webkit-scrollbar-track {
+      background: rgba(61, 246, 255, 0.08);
+    }
+
+    &::-webkit-scrollbar-thumb {
+      border-radius: 4px;
+      background: linear-gradient(180deg, #3df6ff 0%, #0d7fc1 100%);
+    }
   }
   }
 
 
-  .bundle-row {
-    flex: 1;
-    min-height: 0;
+  .table-row {
+    height: 50px;
+    border-top: 1px solid rgba(61, 246, 255, 0.1);
+    background: #0b3355;
     color: #fff;
     color: #fff;
     font-size: 14px;
     font-size: 14px;
-    background: rgba(11, 51, 85, 0.55);
 
 
     &:nth-child(even) {
     &:nth-child(even) {
-      background: rgba(13, 65, 103, 0.62);
-    }
-
-    & + & {
-      border-top: 1px solid rgba(61, 246, 255, 0.1);
+      background: #0d4167;
     }
     }
   }
   }
 
 
-  .col-point {
-    width: 24%;
-    text-align: left;
-    padding-left: 22px;
-    box-sizing: border-box;
+  .table-head span,
+  .table-row span {
+    flex: none;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
   }
   }
 
 
-  .col-value {
-    width: 12%;
-  }
+  .bundle-table {
+    .table-head span:nth-child(1),
+    .table-row span:nth-child(1) {
+      width: 15%;
+    }
 
 
-  .col-status {
-    width: 16%;
-  }
+    .table-head span:nth-child(2),
+    .table-row span:nth-child(2) {
+      width: 10%;
+    }
 
 
-  .warning {
-    color: #ffd15c;
-    text-shadow: 0 0 8px rgba(255, 209, 92, 0.45);
-  }
+    .table-head span:nth-child(3),
+    .table-row span:nth-child(3) {
+      width: 10%;
+    }
 
 
-  .status-tag {
-    display: inline-block;
-    min-width: 52px;
-    padding: 2px 10px;
-    box-sizing: border-box;
-    border: 1px solid currentColor;
-    font-style: normal;
-    font-size: 12px;
-    line-height: 18px;
+    .table-head span:nth-child(4),
+    .table-row span:nth-child(4) {
+      width: 12%;
+    }
 
 
-    &.normal {
-      color: #3df6ff;
-      background: rgba(61, 246, 255, 0.08);
+    .table-head span:nth-child(5),
+    .table-row span:nth-child(5) {
+      width: 12%;
     }
     }
 
 
-    &.attention {
-      color: #ffd15c;
-      background: rgba(255, 209, 92, 0.08);
+    .table-head span:nth-child(6),
+    .table-row span:nth-child(6) {
+      width: 10%;
     }
     }
 
 
-    &.warning {
-      color: #ff9d1c;
-      background: rgba(255, 157, 28, 0.1);
+    .table-head span:nth-child(7),
+    .table-row span:nth-child(7) {
+      width: 10%;
     }
     }
-  }
 
 
-  .fiber-panel {
-    display: flex;
-    gap: 16px;
-    height: 100%;
-    padding: 12px 16px 14px;
-    box-sizing: border-box;
-  }
+    .table-head span:nth-child(8),
+    .table-row span:nth-child(8) {
+      width: 10%;
+    }
 
 
-  .chart-area {
-    display: flex;
-    flex: 1;
-    min-width: 0;
-    flex-direction: column;
+    .table-head span:nth-child(9),
+    .table-row span:nth-child(9) {
+      width: 11%;
+    }
   }
   }
 
 
-  .chart-legend {
-    display: flex;
-    align-items: center;
-    gap: 20px;
-    height: 26px;
-    flex: none;
-    color: #a9dcf4;
-    font-size: 12px;
-  }
+  .device-table {
+    .table-head span:nth-child(1),
+    .table-row span:nth-child(1) {
+      width: 18%;
+    }
 
 
-  .legend-item {
-    display: flex;
-    align-items: center;
-    gap: 6px;
-  }
+    .table-head span:nth-child(2),
+    .table-row span:nth-child(2) {
+      width: 13%;
+    }
 
 
-  .legend-max,
-  .legend-avg {
-    display: inline-block;
-    width: 16px;
-    height: 2px;
-  }
+    .table-head span:nth-child(3),
+    .table-row span:nth-child(3) {
+      width: 14%;
+    }
 
 
-  .legend-max {
-    background: #3df6ff;
-    box-shadow: 0 0 6px rgba(61, 246, 255, 0.8);
-  }
+    .table-head span:nth-child(4),
+    .table-row span:nth-child(4) {
+      width: 14%;
+    }
 
 
-  .legend-avg {
-    background: #ffd15c;
-  }
+    .table-head span:nth-child(5),
+    .table-row span:nth-child(5) {
+      width: 12%;
+    }
 
 
-  .chart-svg {
-    flex: 1;
-    min-height: 0;
-    width: 100%;
-  }
+    .table-head span:nth-child(6),
+    .table-row span:nth-child(6) {
+      width: 14%;
+    }
 
 
-  .fiber-summary {
-    display: flex;
-    flex: none;
-    width: 230px;
-    flex-direction: column;
-    justify-content: center;
-    gap: 18px;
-    padding-left: 22px;
-    border-left: 1px solid rgba(61, 246, 255, 0.18);
+    .table-head span:nth-child(7),
+    .table-row span:nth-child(7) {
+      width: 15%;
+    }
   }
   }
 
 
-  .summary-item {
+  .table-pager {
     display: flex;
     display: flex;
-    align-items: baseline;
-    justify-content: space-between;
-    color: #a9dcf4;
-    font-size: 13px;
-
-    em {
-      color: #fff;
-      font-style: normal;
-      font-family: electronicFont, sans-serif;
-      font-size: 26px;
-      line-height: 1;
-      text-shadow: 0 0 10px rgba(255, 255, 255, 0.25);
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    height: 24px;
+    flex: none;
 
 
-      &.max {
-        color: #3df6ff;
-      }
+    i {
+      width: 6px;
+      height: 6px;
+      border-radius: 1px;
+      background: rgba(61, 246, 255, 0.35);
+      cursor: pointer;
+      transition: all 0.2s;
 
 
-      &.avg {
-        color: #ffd15c;
+      &.active {
+        width: 16px;
+        background: #3df6ff;
+        box-shadow: 0 0 8px rgba(61, 246, 255, 0.85);
       }
       }
     }
     }
   }
   }

+ 25 - 10
src/views/vent/home/selfIgnition/components/PanelSelect.vue

@@ -11,7 +11,7 @@
     @keydown.space.prevent="toggle"
     @keydown.space.prevent="toggle"
     @keydown.esc="open = false"
     @keydown.esc="open = false"
   >
   >
-    <span class="ignition-select-value">{{ selectedLabel }}</span>
+    <span class="ignition-select-value" :title="selectedLabel">{{ selectedLabel }}</span>
     <i class="ignition-select-arrow"></i>
     <i class="ignition-select-arrow"></i>
     <Transition name="ignition-drop">
     <Transition name="ignition-drop">
       <div v-if="open" class="ignition-select-dropdown">
       <div v-if="open" class="ignition-select-dropdown">
@@ -20,6 +20,7 @@
           :key="option.value"
           :key="option.value"
           class="ignition-select-option"
           class="ignition-select-option"
           role="option"
           role="option"
+          :title="option.label"
           :aria-selected="option.value === modelValue"
           :aria-selected="option.value === modelValue"
           :class="{ active: option.value === modelValue }"
           :class="{ active: option.value === modelValue }"
           @click.stop="choose(option)"
           @click.stop="choose(option)"
@@ -52,14 +53,29 @@
     return props.options.find((option) => option.value === props.modelValue)?.label ?? '';
     return props.options.find((option) => option.value === props.modelValue)?.label ?? '';
   });
   });
 
 
+  function findScrollParent(element: HTMLElement | undefined): HTMLElement | null {
+    let node = element?.parentElement;
+    while (node) {
+      const overflowY = getComputedStyle(node).overflowY;
+      if (overflowY === 'auto' || overflowY === 'scroll') {
+        return node;
+      }
+      node = node.parentElement;
+    }
+    return null;
+  }
+
   function toggle() {
   function toggle() {
     open.value = !open.value;
     open.value = !open.value;
     if (open.value) {
     if (open.value) {
       nextTick(() => {
       nextTick(() => {
         const rect = rootRef.value?.getBoundingClientRect();
         const rect = rootRef.value?.getBoundingClientRect();
-        if (rect) {
-          dropUp.value = window.innerHeight - rect.bottom < 220;
-        }
+        if (!rect) return;
+        const scrollParent = findScrollParent(rootRef.value);
+        const bounds = scrollParent ? scrollParent.getBoundingClientRect() : { top: 0, bottom: window.innerHeight };
+        const spaceBelow = bounds.bottom - rect.bottom;
+        const spaceAbove = rect.top - bounds.top;
+        dropUp.value = spaceBelow < 220 && spaceAbove > spaceBelow;
       });
       });
     }
     }
   }
   }
@@ -164,14 +180,13 @@
   }
   }
 
 
   .ignition-select-option {
   .ignition-select-option {
-    height: 30px;
-    padding: 0 8px;
-    line-height: 30px;
+    min-height: 30px;
+    padding: 5px 8px;
+    line-height: 20px;
     color: #d9f7ff;
     color: #d9f7ff;
     font-size: 13px;
     font-size: 13px;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
+    white-space: normal;
+    word-break: break-all;
     cursor: pointer;
     cursor: pointer;
 
 
     &:hover {
     &:hover {

+ 103 - 0
src/views/vent/home/selfIgnition/data.ts

@@ -455,3 +455,106 @@ export function createSelfIgnitionData(): SelfIgnitionPageData {
     },
     },
   };
   };
 }
 }
+
+/* ==================== 设备实时监测与分析 ==================== */
+
+export interface SensorReading {
+  label: string;
+  value: string;
+}
+
+export interface SensorGroup {
+  name: string;
+  rows: SensorReading[];
+}
+
+export const SENSOR_GROUPS: SensorGroup[] = [
+  {
+    name: '工作面#1',
+    rows: [
+      { label: '回风隅角氧气', value: '20.9%' },
+      { label: '回风巷一氧化碳', value: '12ppm' },
+      { label: '回风巷温度', value: '26.5℃' },
+    ],
+  },
+  {
+    name: '工作面#2',
+    rows: [
+      { label: '回风隅角氧气', value: '20.8%' },
+      { label: '回风巷一氧化碳', value: '15ppm' },
+      { label: '回风巷温度', value: '27.1℃' },
+    ],
+  },
+];
+
+export const NATURAL_FIRE_STAGE = {
+  levelText: 'IV',
+  levelName: '高风险',
+  color: '#ff9d1c',
+};
+
+export const ALERT_ADVICE = '加密束管采样频次,加强回风顺槽气体分析;对高温测点进行连续测温跟踪;必要时调整推进速度并落实注浆与注氮措施。';
+
+export interface FiberChartData {
+  categories: string[];
+  solid: number[];
+  dashed: number[];
+  peakIndex: number;
+}
+
+export const FIBER_CHART_DATA: FiberChartData = {
+  categories: ['测点01', '测点02', '测点03', '测点04', '测点05', '测点06', '测点07', '测点08', '测点09', '测点10', '测点11', '测点12'],
+  solid: [120, 180, 140, 160, 110, 140, 130, 210, 800, 520, 360, 280],
+  dashed: [160, 200, 170, 190, 140, 170, 150, 240, 700, 480, 320, 260],
+  peakIndex: 8,
+};
+
+export interface BundleMonitorRow {
+  deviceNo: string;
+  o2: string;
+  n2: string;
+  co: number;
+  co2: string;
+  ch4: string;
+  c2h4: string;
+  c2h6: string;
+  c2h2: string;
+}
+
+const BUNDLE_DEVICE_NAMES = ['1#束管', '2#束管', '3#束管', '4#束管'];
+
+export const BUNDLE_MONITOR_ROWS: BundleMonitorRow[] = Array.from({ length: 40 }, (_, index) => {
+  return {
+    deviceNo: BUNDLE_DEVICE_NAMES[index % 4],
+    o2: (20.9 - (index % 12) * 0.08).toFixed(2),
+    n2: (78.1 + (index % 9) * 0.06).toFixed(2),
+    co: 6 + (index % 15) * 2,
+    co2: (0.03 + (index % 10) * 0.005).toFixed(3),
+    ch4: (0.18 + (index % 8) * 0.03).toFixed(2),
+    c2h4: (index % 5 === 0 ? 0.02 : 0).toFixed(2),
+    c2h6: (index % 6 === 0 ? 0.01 : 0).toFixed(2),
+    c2h2: (index % 7 === 0 ? 0.01 : 0).toFixed(2),
+  };
+});
+
+export interface DeviceMonitorRow {
+  deviceNo: string;
+  o2: string;
+  co: number;
+  co2: string;
+  ch4: string;
+  temp: string;
+  pressureDiff: string;
+}
+
+export const DEVICE_MONITOR_ROWS: DeviceMonitorRow[] = Array.from({ length: 40 }, (_, index) => {
+  return {
+    deviceNo: `${(index % 4) + 1}#密闭`,
+    o2: (20.4 - (index % 10) * 0.07).toFixed(2),
+    co: 8 + (index % 12) * 2,
+    co2: (0.04 + (index % 9) * 0.004).toFixed(3),
+    ch4: (0.22 + (index % 8) * 0.03).toFixed(2),
+    temp: (25.8 + (index % 10) * 0.5).toFixed(1),
+    pressureDiff: (12 + (index % 7) * 3).toFixed(0),
+  };
+});

+ 168 - 0
src/views/vent/home/selfIgnition/evaluation.config.ts

@@ -0,0 +1,168 @@
+export interface SafetyItemOption {
+  name: string;
+  normalizedScore: number;
+}
+
+export interface SafetyItem {
+  code: number;
+  name: string;
+  preWeight: number | null;
+  midWeight: number | null;
+  options: SafetyItemOption[];
+}
+
+export interface SafetyScene {
+  id: string;
+  systemname: string;
+}
+
+export interface SafetyEvaluationEntity {
+  id?: string;
+  evalType: number;
+  totalScore?: number | string | null;
+  riskLevel?: string;
+  workfaceName?: string;
+  createTime?: string;
+  [key: string]: any;
+}
+
+export interface EvalRiskLevel {
+  level: string;
+  range: string;
+  color: string;
+  colorName: string;
+  suggestion: string;
+}
+
+/** 后端字段名与 22 项指标 code 的对应关系 */
+export const FIELD_BY_CODE: Record<number, string> = {
+  1: 'metamorphicDegreeOption',
+  2: 'macroCoalRockOption',
+  3: 'spontaneousCombustionOption',
+  4: 'coalSeamThicknessOption',
+  5: 'coalSeamDipOption',
+  6: 'geologicalStructureOption',
+  7: 'miningDepthOption',
+  8: 'miningMethodOption',
+  9: 'miningHeightOption',
+  10: 'compoundGoafOption',
+  11: 'ventilationModeOption',
+  12: 'airLeakageOption',
+  13: 'miningSpeedOption',
+  14: 'tubeMonitoringOption',
+  15: 'safetyMonitoringOption',
+  16: 'opticalFiberOption',
+  17: 'manualDetectionOption',
+  18: 'firePreventionOption',
+  19: 'groutingSystemOption',
+  20: 'fireSprinklerOption',
+  21: 'co2InjectionOption',
+  22: 'goafFireStageOption',
+};
+
+/** 后端算法风险等级:分数越高越安全 */
+export const EVAL_RISK_LEVELS: EvalRiskLevel[] = [
+  {
+    level: '安全',
+    range: '≥90',
+    color: '#7ed321',
+    colorName: '绿色',
+    suggestion: '保持现有监测频次和防灭火措施,按计划开展日常巡检。',
+  },
+  {
+    level: '低风险',
+    range: '70~90',
+    color: '#3df6ff',
+    colorName: '蓝色',
+    suggestion: '关注束管测点变化趋势,定期校验传感器,保持采空区密闭完好。',
+  },
+  {
+    level: '中风险',
+    range: '50~70',
+    color: '#ff9d1c',
+    colorName: '橙色',
+    suggestion: '加密束管采样频次,加强回风顺槽气体分析;对高温测点连续测温跟踪;必要时调整推进速度并落实注浆、注氮措施。',
+  },
+  {
+    level: '高风险',
+    range: '<50',
+    color: '#ff4d4f',
+    colorName: '红色',
+    suggestion: '立即停止作业并撤出受影响区域人员,启动应急预案,实施连续监测与注浆注氮措施。',
+  },
+];
+
+export const EVAL_RISK_MAP: Record<string, EvalRiskLevel> = EVAL_RISK_LEVELS.reduce(
+  (result, item) => {
+    result[item.level] = item;
+    return result;
+  },
+  {} as Record<string, EvalRiskLevel>
+);
+
+/** 场景接口不可用时的兜底场景,manageSystemId 为空时仍可计算/保存 */
+export const FALLBACK_SCENE: SafetyScene = {
+  id: '',
+  systemname: '12采空区工作面',
+};
+
+const options = (names: string[]): SafetyItemOption[] => names.map((name) => ({ name, normalizedScore: 0 }));
+
+const item = (code: number, name: string, preWeight: number | null, midWeight: number | null, names: string[]): SafetyItem => ({
+  code,
+  name,
+  preWeight,
+  midWeight,
+  options: options(names),
+});
+
+/**
+ * 指标选项兜底字典,内容与《自然发火安全评价算法-前端对接文档》第 6 节一致。
+ * getItemOptions 接口异常时使用,保证表单仍然可以录入并提交后端计算。
+ */
+export const FALLBACK_SAFETY_ITEMS: SafetyItem[] = [
+  item(1, '变质程度', 0.06, 0.015, [
+    '无烟煤',
+    '贫煤',
+    '贫瘦煤',
+    '瘦煤',
+    '焦煤',
+    '肥煤',
+    '1/3焦煤',
+    '气肥煤',
+    '气煤',
+    '1/2中黏煤',
+    '弱黏煤',
+    '不黏煤',
+    '长焰煤',
+    '褐煤',
+  ]),
+  item(2, '主要宏观煤岩成分', 0.12, 0.03, ['丝炭', '镜煤', '亮煤', '暗煤']),
+  item(3, '自燃倾向性', 0.24, 0.06, ['Ⅰ类容易自燃', 'Ⅱ类自燃', 'Ⅲ类不易自燃']),
+  item(4, '工作面煤层煤厚', 0.12, 0.03, ['极薄煤层', '薄煤层', '中厚煤层', '厚煤层', '特厚煤层']),
+  item(5, '工作面煤层倾角', 0.12, 0.03, ['近水平煤层', '缓倾斜煤层', '倾斜煤层', '急倾斜煤层']),
+  item(6, '工作面地质构造', 0.1, 0.025, ['节理构造', '褶皱构造', '岩浆岩侵入体', '陷落柱', '断层构造']),
+  item(7, '工作面采深', 0.08, 0.02, ['深采深', '中深采深', '浅采深']),
+  item(8, '开采方法', 0.04, 0.01, ['综合机械化长壁开采', '普通炮采长壁开采', '综放开采', '短壁式', '柱式开采', '急倾斜特殊开采']),
+  item(9, '工作面采高', 0.06, 0.015, ['薄煤层工作面', '中厚煤层工作面', '大采高工作面', '超大采高工作面']),
+  item(10, '复合采空区', 0.04, 0.01, [
+    '上覆、相邻都无采空区',
+    '上覆无采空区,相邻一侧有采空区',
+    '上覆无采空区,相邻两侧有采空区',
+    '上覆有采空区,相邻没有采空区',
+    '上覆有采空区,相邻一侧有采空区',
+    '上覆有采空区,相邻两侧有采空区',
+  ]),
+  item(11, '负压通风方式', 0.02, 0.005, ['正压通风(均压工作面)', '负压通风(U型)', '负压通风(Y型,沿空留巷)']),
+  item(12, '漏风强度', null, 0.06, ['低漏风差值', '中漏风差值', '高漏风差值']),
+  item(13, '工作面回采速度', null, 0.06, ['正常回采', '停采/缓慢推进']),
+  item(14, '束管监测系统维护情况', null, 0.04, ['正常运行', '故障']),
+  item(15, '安全监控系统维护情况', null, 0.04, ['正常运行', '故障']),
+  item(16, '光纤测温系统维护情况', null, 0.04, ['正常运行', '故障']),
+  item(17, '人工检测执行情况', null, 0.04, ['正常执行', '未正常执行']),
+  item(18, '防灭火技术措施', null, 0.2, ['综合措施', '单项措施', '无措施']),
+  item(19, '注浆系统维护情况', null, 0.04, ['正常', '故障']),
+  item(20, '消防洒水系统维护情况', null, 0.04, ['正常', '故障']),
+  item(21, '灌注二氧化碳系统维护情况', null, 0.04, ['正常', '故障']),
+  item(22, '采空区自然发火阶段', null, 0.15, ['缓慢氧化阶段', '蓄热氧化阶段', '加速氧化阶段', '快速氧化阶段', '剧烈氧化阶段', '临燃氧化阶段']),
+];

+ 29 - 15
src/views/vent/home/selfIgnition/index.vue

@@ -18,7 +18,7 @@
       <OverviewView v-show="activeTab === 0" :overview="data.overview" :devices="data.devices" :overall="data.overall" :warnings="data.warnings" />
       <OverviewView v-show="activeTab === 0" :overview="data.overview" :devices="data.devices" :overall="data.overall" :warnings="data.warnings" />
 
 
       <!-- 设备实时监测与分析 -->
       <!-- 设备实时监测与分析 -->
-      <MonitorView v-show="activeTab === 1" :devices="data.devices" :alerts="data.alerts" :bundle-rows="data.bundleRows" :fiber="data.fiber" />
+      <MonitorView v-show="activeTab === 1" :active="activeTab === 1" @view-evaluation="activeTab = 2" />
 
 
       <!-- 人工录入与分析 -->
       <!-- 人工录入与分析 -->
       <ManualView v-show="activeTab === 2" />
       <ManualView v-show="activeTab === 2" />
@@ -36,26 +36,29 @@
   import { noHeadeLink } from '/@/layouts/default/layout.data';
   import { noHeadeLink } from '/@/layouts/default/layout.data';
 
 
   // 与其他 configurable 首页一致:把当前路由登记到布局的“无 header”白名单。
   // 与其他 configurable 首页一致:把当前路由登记到布局的“无 header”白名单。
-  // 该数组在布局 header 渲染前由本页面模块写入,不修改公共组件。
+  // 页面懒加载时 header 可能已经渲染,因此再用页面级 class 兜底隐藏,不修改公共组件。
   const PAGE_PATH = '/fire/monitor/selfIgnition';
   const PAGE_PATH = '/fire/monitor/selfIgnition';
   if (!noHeadeLink.includes(PAGE_PATH)) {
   if (!noHeadeLink.includes(PAGE_PATH)) {
     noHeadeLink.push(PAGE_PATH);
     noHeadeLink.push(PAGE_PATH);
   }
   }
 
 
-  const PAGE_STYLE_ID = 'self-ignition-fullscreen-style';
+  const PAGE_CLASS = 'self-ignition-page';
+  // HMR 会先后触发新旧实例的挂载/卸载,用实例标记避免旧实例把新实例的样式类移除
+  const PAGE_INSTANCE = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
 
 
-  function mountPageStyle() {
-    if (document.getElementById(PAGE_STYLE_ID)) return;
-    const style = document.createElement('style');
-    style.id = PAGE_STYLE_ID;
-    style.textContent = `.jeecg-layout-header { display: none !important; }`;
-    document.head.appendChild(style);
+  function mountPageClass() {
+    document.documentElement.dataset.selfIgnitionPage = PAGE_INSTANCE;
+    document.documentElement.classList.add(PAGE_CLASS);
   }
   }
 
 
-  function unmountPageStyle() {
-    document.getElementById(PAGE_STYLE_ID)?.remove();
+  function unmountPageClass() {
+    if (document.documentElement.dataset.selfIgnitionPage !== PAGE_INSTANCE) return;
+    document.documentElement.classList.remove(PAGE_CLASS);
+    delete document.documentElement.dataset.selfIgnitionPage;
   }
   }
 
 
+  mountPageClass();
+
   const route = useRoute();
   const route = useRoute();
   const rootRef = ref<HTMLElement>();
   const rootRef = ref<HTMLElement>();
   const scaleX = ref(1);
   const scaleX = ref(1);
@@ -105,7 +108,7 @@
   }
   }
 
 
   onMounted(() => {
   onMounted(() => {
-    mountPageStyle();
+    mountPageClass();
     initDataInterval();
     initDataInterval();
     updateScale();
     updateScale();
     if (typeof ResizeObserver !== 'undefined' && rootRef.value) {
     if (typeof ResizeObserver !== 'undefined' && rootRef.value) {
@@ -116,15 +119,15 @@
   });
   });
 
 
   onActivated(() => {
   onActivated(() => {
-    mountPageStyle();
+    mountPageClass();
   });
   });
 
 
   onDeactivated(() => {
   onDeactivated(() => {
-    unmountPageStyle();
+    unmountPageClass();
   });
   });
 
 
   onUnmounted(() => {
   onUnmounted(() => {
-    unmountPageStyle();
+    unmountPageClass();
     clearInterval(dataInterval);
     clearInterval(dataInterval);
     resizeObserver?.disconnect();
     resizeObserver?.disconnect();
     window.removeEventListener('resize', updateScale);
     window.removeEventListener('resize', updateScale);
@@ -239,3 +242,14 @@
     background-image: none;
     background-image: none;
   }
   }
 </style>
 </style>
+
+<style lang="less">
+  // 本页面全屏展示:隐藏布局顶部栏和左下角悬浮菜单,右侧操作栏保留
+  html.self-ignition-page {
+    .jeecg-layout-header,
+    .bottom-side,
+    .menu-show-icon {
+      display: none !important;
+    }
+  }
+</style>

+ 45 - 0
src/views/vent/home/selfIgnition/safetyEvaluation.api.ts

@@ -0,0 +1,45 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  itemOptions = '/safety/safetyEvaluation/getItemOptions',
+  calculate = '/safety/safetyEvaluation/calculate',
+  calculateAndSave = '/safety/safetyEvaluation/calculateAndSave',
+  calculateSample = '/safety/safetyEvaluation/calculateSample',
+  list = '/safety/safetyEvaluation/getSafetyEvaluationList',
+  add = '/safety/safetyEvaluation/addSafetyEvaluation',
+  update = '/safety/safetyEvaluation/updateSafetyEvaluation',
+  remove = '/safety/safetyEvaluation/deleteSafetyEvaluation',
+  getByScene = '/safety/safetyEvaluation/getByScene',
+  sceneList = '/safety/ventanalyManageSystem/list',
+}
+
+/** 22 项评价指标、权重和选项字典 */
+export const getSafetyItemOptions = () => defHttp.get({ url: Api.itemOptions });
+
+/** 仅计算,不落库 */
+export const calculateSafetyEvaluation = (params: Record<string, any>) => defHttp.post({ url: Api.calculate, params });
+
+/** 计算并入库 */
+export const saveSafetyEvaluation = (params: Record<string, any>) => defHttp.post({ url: Api.calculateAndSave, params });
+
+/** 用 Excel 示例数据计算并入库,evalType: 1=采前 2=采中 */
+export const calculateSampleSafetyEvaluation = (evalType: number) =>
+  defHttp.post({ url: Api.calculateSample, params: { evalType } }, { joinParamsToUrl: true });
+
+/** 评价记录分页,pageNo/pageSize/evalType/manageSystemId 等走 query 参数 */
+export const getSafetyEvaluationList = (params: Record<string, any>) => defHttp.post({ url: Api.list, params }, { joinParamsToUrl: true });
+
+/** 原样新增(不计算),仅用于数据迁移等特殊场景 */
+export const addSafetyEvaluation = (params: Record<string, any>) => defHttp.post({ url: Api.add, params });
+
+/** 原样修改(不计算),仅用于数据迁移等特殊场景 */
+export const updateSafetyEvaluation = (params: Record<string, any>) => defHttp.post({ url: Api.update, params });
+
+/** 按 id 删除评价记录 */
+export const deleteSafetyEvaluation = (id: string) => defHttp.get({ url: Api.remove, params: { id } });
+
+/** 按场景查询最新采前/采中评价 */
+export const getSafetyEvaluationByScene = (sceneId: string) => defHttp.get({ url: Api.getByScene, params: { sceneId } });
+
+/** 采空区场景列表,用于评价对象绑定 */
+export const getSafetySceneList = () => defHttp.get({ url: Api.sceneList, params: { strtype: 'sys_surface_caimei', pagetype: 'normal' } });