Forráskód Böngészése

[Feat 0000] 掘进工作面二维模型开发及对接

houzekong 3 napja
szülő
commit
4a4714eab9

+ 52 - 0
public/svg-models/tun-face-2d/model.json

@@ -0,0 +1,52 @@
+{
+  "viewBox": "0 0 1920 1080",
+  "css": "[data-anim-id] { transition: transform 3s; }",
+  "animations": [],
+  "elements": {
+    "auto_anim_0": {
+      "key": "auto_anim_0",
+      "frames": [
+        "matrix( 1, 0, 0, 1, 1157.9,431.1)",
+        "matrix( 1, 0, 0, 1, 1148.85,431.1)",
+        "matrix( 1, 0, 0, 1, 1139.75,431.1)",
+        "matrix( 1, 0, 0, 1, 1130.7,431.1)",
+        "matrix( 1, 0, 0, 1, 1121.6,431.1)",
+        "matrix( 1, 0, 0, 1, 1112.55,431.1)",
+        "matrix( 1, 0, 0, 1, 1103.45,431.1)",
+        "matrix( 1, 0, 0, 1, 1094.4,431.1)",
+        "matrix( 1, 0, 0, 1, 1085.3,431.1)",
+        "matrix( 1, 0, 0, 1, 1076.25,431.1)"
+      ]
+    },
+    "auto_anim_1": {
+      "key": "auto_anim_1",
+      "frames": [
+        "matrix( 1, 0, 0, 1, 1019.2,502.15)",
+        "matrix( 1, 0, 0, 1, 1029.8179,502.15)",
+        "matrix( 1, 0, 0, 1, 1040.4822,502.15)",
+        "matrix( 1, 0, 0, 1, 1051.1,502.15)",
+        "matrix( 1, 0, 0, 1, 1061.7179,502.15)",
+        "matrix( 1, 0, 0, 1, 1072.3821,502.15)",
+        "matrix( 1, 0, 0, 1, 1083,502.15)",
+        "matrix( 1, 0, 0, 1, 1093.6178,502.15)",
+        "matrix( 1, 0, 0, 1, 1104.2821,502.15)",
+        "matrix( 1, 0, 0, 1, 1114.9,502.15)"
+      ]
+    },
+    "auto_anim_2": {
+      "key": "auto_anim_2",
+      "frames": [
+        "matrix( 1, 0, 0, 1, 1398.35,474.95)",
+        "matrix( 1, 0, 0, 1, 1405.5,480.2)",
+        "matrix( 1, 0, 0, 1, 1412.65,485.45)",
+        "matrix( 1, 0, 0, 1, 1419.75,490.7)",
+        "matrix( 1, 0, 0, 1, 1426.9,495.95)",
+        "matrix( 1, 0, 0, 1, 1434.05,501.15)",
+        "matrix( 1, 0, 0, 1, 1441.2,506.4)",
+        "matrix( 1, 0, 0, 1, 1448.3,511.65)",
+        "matrix( 1, 0, 0, 1, 1455.45,516.9)",
+        "matrix( 1, 0, 0, 1, 1462.6,522.15)"
+      ]
+    }
+  }
+}

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 587 - 0
public/svg-models/tun-face-2d/model.svg


+ 0 - 184
src/devtest/svg2d-demo/index.vue

@@ -1,184 +0,0 @@
-<template>
-  <div class="demo">
-    <h2>二维 SVG 模型手动测试({{ MODELS.length }} 个模型)</h2>
-    <div class="bar">
-      <select v-model="current">
-        <option v-for="m in MODELS" :key="m.id" :value="m.id">{{ m.label }}({{ m.id }})</option>
-      </select>
-      <button @click="reload">重置模型</button>
-      <span class="status">{{ status }}</span>
-    </div>
-
-    <div class="states">
-      <button v-for="(s, i) in currentModel?.states || []" :key="i" :class="{ active: lastState === i }" @click="trigger(i)">
-        {{ s.label }}
-      </button>
-    </div>
-
-    <div class="stage">
-      <div :id="'host-' + current" class="svg-host">
-        <component :is="currentComp" :key="'m-' + current + '-' + mountKey" ref="drvRef" :identify="'demo'" />
-      </div>
-    </div>
-
-    <pre class="readout">{{ readout }}</pre>
-  </div>
-</template>
-
-<script setup lang="ts">
-  import { computed, defineAsyncComponent, nextTick, onMounted, ref } from 'vue';
-  import { MODELS } from './models';
-
-  defineOptions({ name: 'Svg2dDemoPage' });
-
-  const current = ref(MODELS[0].id);
-  const mountKey = ref(0);
-  const status = ref('就绪');
-  const readout = ref('');
-  const lastState = ref(-1);
-  const drvRef = ref<any>(null);
-
-  const currentModel = computed(() => MODELS.find((m) => m.id === current.value));
-  const currentComp = computed(() => {
-    const m = currentModel.value;
-    return m ? defineAsyncComponent(m.comp) : null;
-  });
-
-  const hostEl = () => document.getElementById('host-' + current.value);
-  const svgOf = () => hostEl()?.querySelector('svg') as SVGSVGElement | undefined;
-
-  async function waitForSvg(timeout = 15000) {
-    const t0 = performance.now();
-    while (performance.now() - t0 < timeout) {
-      if (svgOf()) return true;
-      await new Promise((r) => setTimeout(r, 30));
-    }
-    return false;
-  }
-
-  function info() {
-    const svg = svgOf();
-    if (!svg) return '模型未加载';
-    const anims = svg.getAnimations ? svg.getAnimations() : [];
-    const playing = anims.filter((a) => a.playState === 'running').length;
-    const loopEls = svg.querySelectorAll('[class*="_Loop"], [data-zj-anim]');
-    const cls = [...svg.querySelectorAll('[class]')]
-      .map((el) => el.getAttribute('class') || '')
-      .filter((c) => /block|hidden|animate-play|scan/.test(c))
-      .slice(0, 6);
-    return [
-      `模型:${currentModel.value?.label}(${current.value})`,
-      `元素:data-anim-id ${svg.querySelectorAll('[data-anim-id]').length} 个,svg 子节点 ${svg.querySelectorAll('*').length} 个`,
-      `循环元素:${loopEls.length} 个;运行中动画:${playing}/${anims.length}`,
-      cls.length ? `状态类:${cls.join(' | ')}` : '状态类:无',
-    ].join('\n');
-  }
-
-  async function trigger(i: number) {
-    const model = currentModel.value;
-    if (!model) return;
-    const s = model.states[i];
-    lastState.value = i;
-    status.value = `已触发:${s.label}`;
-    try {
-      drvRef.value?.animate?.(...s.args);
-    } catch (e: any) {
-      status.value = `执行出错:${e?.message || e}`;
-    }
-    await new Promise((r) => setTimeout(r, 120));
-    readout.value = info();
-  }
-
-  async function reload() {
-    lastState.value = -1;
-    status.value = '重新加载…';
-    mountKey.value++;
-    await nextTick();
-    const ok = await waitForSvg();
-    status.value = ok ? '就绪' : '加载失败';
-    readout.value = info();
-  }
-
-  async function onModelChange() {
-    lastState.value = -1;
-    status.value = '加载中…';
-    await nextTick();
-    const ok = await waitForSvg();
-    status.value = ok ? '就绪' : '加载失败';
-    readout.value = info();
-  }
-
-  onMounted(onModelChange);
-</script>
-
-<style scoped>
-  .demo {
-    padding: 14px;
-    background: #0f1420;
-    color: #d7e0ee;
-    min-height: 100vh;
-    font-size: 13px;
-  }
-  h2 {
-    font-size: 15px;
-    margin: 0 0 8px;
-  }
-  .bar {
-    display: flex;
-    gap: 8px;
-    align-items: center;
-    margin-bottom: 8px;
-  }
-  select,
-  button {
-    padding: 4px 10px;
-    border-radius: 4px;
-    border: 1px solid #3d4b63;
-    background: #1b2434;
-    color: #cfd9e6;
-    font-size: 12px;
-  }
-  button {
-    cursor: pointer;
-  }
-  button.active {
-    background: #2d4b7a;
-    border-color: #4f7cc4;
-  }
-  .status {
-    color: #7fd18b;
-    font-size: 12px;
-  }
-  .states {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 8px;
-    margin-bottom: 10px;
-  }
-  .stage {
-    width: 960px;
-    height: 540px;
-    background: #0b101a;
-    border: 1px solid #24324a;
-    overflow: hidden;
-  }
-  .svg-host {
-    width: 100%;
-    height: 100%;
-  }
-  .svg-host :deep(svg) {
-    width: 100% !important;
-    height: 100% !important;
-    margin: 0 !important;
-    display: block;
-  }
-  .readout {
-    background: #111a28;
-    padding: 8px;
-    font-size: 12px;
-    line-height: 1.6;
-    color: #9fd0a8;
-    white-space: pre-wrap;
-    min-height: 70px;
-  }
-</style>

+ 0 - 3
src/devtest/svg2d-demo/main.ts

@@ -1,3 +0,0 @@
-import { createApp } from 'vue';
-import Page from './index.vue';
-createApp(Page).mount('#app');

+ 0 - 280
src/devtest/svg2d-demo/models.ts

@@ -1,280 +0,0 @@
-/* 全部已迁移模型 + 每个模型的手动动画状态按钮 */
-export interface DemoState {
-  label: string;
-  args: any[];
-}
-export interface DemoModel {
-  id: string;
-  label: string;
-  comp: () => Promise<any>;
-  states: DemoState[];
-}
-
-const gateStates: DemoState[] = [
-  { label: '全开', args: [true, true, true] },
-  { label: '全关', args: [false, false, false] },
-  { label: '前门开', args: [true, false, false] },
-  { label: '后门开', args: [false, false, true] },
-];
-const boolStates: DemoState[] = [
-  { label: '全开', args: [true, true, true] },
-  { label: '全关', args: [false, false, false] },
-];
-const progressStates = (field = 'frontArea', max = 90): DemoState[] =>
-  [0, 25, 50, 75, 100].map((p) => ({ label: `${field} ${p}%`, args: [{ [field]: (max * p) / 100 }, max] }));
-const degreeStates = (max = 180): DemoState[] => [0, 45, 90, 135, 180].map((d) => ({ label: `开度 ${d}`, args: [{ OpenDegree: d }, max] }));
-const fanStates = (v: any = '1'): DemoState[] => [
-  { label: '1号风机开', args: [{ Fan1StartStatus: v }] },
-  { label: '2号风机开', args: [{ Fan2StartStatus: v }] },
-  { label: '双机开', args: [{ Fan1StartStatus: v, Fan2StartStatus: v }] },
-  { label: '全停', args: [{}] },
-];
-const mainFanStates: DemoState[] = [
-  { label: '1号主扇开', args: [{ Fan1StartStatus: 1 }] },
-  { label: '2号主扇开', args: [{ Fan2StartStatus: 1 }] },
-  { label: '双机开', args: [{ Fan1StartStatus: 1, Fan2StartStatus: 1 }] },
-  { label: '全停', args: [{}] },
-];
-const gasStates: DemoState[] = [
-  { label: '泵1开', args: [{ pumps: { 1: true } }] },
-  { label: '泵2开', args: [{ pumps: { 2: true } }] },
-  { label: '双泵开', args: [{ pumps: { 1: true, 2: true } }] },
-  { label: '全停', args: [{ pumps: {} }] },
-  { label: '不传数据(保持)', args: [] },
-];
-const noopStates: DemoState[] = [{ label: '触发(无动画)', args: [] }];
-const scanStates: DemoState[] = [
-  { label: '向下扫描', args: ['down'] },
-  { label: '向上扫描', args: ['up'] },
-  { label: '停止', args: [''] },
-];
-const junyaStates: DemoState[] = [
-  { label: '门窗开', args: [{ windowOpen: true, airflowOpen: false }] },
-  { label: '门窗关', args: [{ windowOpen: false, airflowOpen: false }] },
-  { label: '风流开', args: [{ windowOpen: false, airflowOpen: true }] },
-  { label: '门窗+风流开', args: [{ windowOpen: true, airflowOpen: true }] },
-];
-const zhijiangStates: DemoState[] = [
-  { label: '开车', args: [{ running: true }] },
-  { label: '停车', args: [{ running: false }] },
-];
-
-export const MODELS: DemoModel[] = [
-  {
-    id: 'gas-pump-2d',
-    label: '瓦斯抽采泵站',
-    comp: () => import('/@/views/vent/monitorManager/gasPumpMonitor/components/gasPumpSVG.vue'),
-    states: gasStates,
-  },
-  {
-    id: 'gate-dual-2d',
-    label: '双扇风门',
-    comp: () => import('/@/views/vent/monitorManager/gateMonitor/components/gateDualSVG.vue'),
-    states: gateStates,
-  },
-  { id: 'gate-2d', label: '风门', comp: () => import('/@/views/vent/monitorManager/gateMonitor/components/gateSVG.vue'), states: gateStates },
-  {
-    id: 'gate-triple-2d',
-    label: '风门三道',
-    comp: () => import('/@/views/vent/monitorManager/gateMonitor/components/gateTripleSVG.vue'),
-    states: gateStates,
-  },
-  {
-    id: 'gate-xr-2d',
-    label: '风门(斜巷)',
-    comp: () => import('/@/views/vent/monitorManager/gateMonitor/components/gateXrSVG.vue'),
-    states: gateStates,
-  },
-  {
-    id: 'gate-liang-lie-2d',
-    label: '风门两列',
-    comp: () => import('/@/views/vent/monitorManager/gateMonitor/components/gateLiangLieSVG.vue'),
-    states: gateStates,
-  },
-  {
-    id: 'gate-san-dao-shen-suo-2d',
-    label: '风门三道伸缩',
-    comp: () => import('/@/views/vent/monitorManager/gateMonitor/components/gateSanDaoShenSuoSVG.vue'),
-    states: gateStates,
-  },
-  {
-    id: 'gate-ye-ya-2d',
-    label: '液压风门',
-    comp: () => import('/@/views/vent/monitorManager/gateMonitor/components/GateYeYaSVG.vue'),
-    states: gateStates,
-  },
-  {
-    id: 'gate-shrink-2d',
-    label: '风门收缩',
-    comp: () => import('/@/views/vent/monitorManager/gateMonitor/components/GateShrinkSVG.vue'),
-    states: gateStates,
-  },
-  {
-    id: 'gate-dual-aio-2d',
-    label: '风门(一体化)',
-    comp: () => import('/@/views/vent/monitorManager/gateMonitor/components/gateDualAIO.vue'),
-    states: gateStates,
-  },
-  {
-    id: 'gate-ssl-2d',
-    label: '风门(ssl)',
-    comp: () => import('/@/views/vent/monitorManager/airDoor/components/gateSVG.ssl.vue'),
-    states: gateStates,
-  },
-  {
-    id: 'gate-dual-ssl-2d',
-    label: '双扇风门(ssl)',
-    comp: () => import('/@/views/vent/monitorManager/airDoor/components/gateDualSVG.ssl.vue'),
-    states: gateStates,
-  },
-  {
-    id: 'fire-door-2d',
-    label: '防火门',
-    comp: () => import('/@/views/vent/monitorManager/fireDoorMonitor/components/fireDoorSVG.vue'),
-    states: boolStates,
-  },
-  {
-    id: 'pai-dai-hang-2d',
-    label: '皮带密闭门',
-    comp: () => import('/@/views/vent/monitorManager/fireDoorMonitor/components/pidaihangSVG.vue'),
-    states: boolStates,
-  },
-  {
-    id: 'window-small-2d',
-    label: '小风窗',
-    comp: () => import('/@/views/vent/monitorManager/windowMonitor/components/SmallWindowSVG.vue'),
-    states: progressStates(),
-  },
-  {
-    id: 'window-2d',
-    label: '风窗',
-    comp: () => import('/@/views/vent/monitorManager/windowMonitor/components/windowSVG.vue'),
-    states: progressStates(),
-  },
-  {
-    id: 'window-da-liu-ta-2d',
-    label: '大风窗(塔)',
-    comp: () => import('/@/views/vent/monitorManager/windowMonitor/components/windowDaLiuTaSVG.vue'),
-    states: progressStates(),
-  },
-  {
-    id: 'window-dai-xiao-men-2d',
-    label: '带小门风窗',
-    comp: () => import('/@/views/vent/monitorManager/windowMonitor/components/windowDaiXiaoMenSVG.vue'),
-    states: progressStates(),
-  },
-  {
-    id: 'window-door-2d',
-    label: '风窗门',
-    comp: () => import('/@/views/vent/monitorManager/windowMonitor/components/windowDoorSVG.vue'),
-    states: progressStates(),
-  },
-  {
-    id: 'window-door-blt-2d',
-    label: '风窗门(blt)',
-    comp: () => import('/@/views/vent/monitorManager/windowMonitor/components/windowDoorBltSVG.vue'),
-    states: noopStates,
-  },
-  {
-    id: 'window-dual-2d',
-    label: '双扇风窗',
-    comp: () => import('/@/views/vent/monitorManager/windowMonitor/components/windowDualSVG.vue'),
-    states: degreeStates(90),
-  },
-  {
-    id: 'window-dual-adjust-2d',
-    label: '双扇可调风窗',
-    comp: () => import('/@/views/vent/monitorManager/windowMonitor/components/windowDualAdjustSVG.vue'),
-    states: progressStates(),
-  },
-  {
-    id: 'window-dual-zd-2d',
-    label: '双扇自动风窗',
-    comp: () => import('/@/views/vent/monitorManager/windowMonitor/components/windowDualZDSVG.vue'),
-    states: degreeStates(),
-  },
-  {
-    id: 'window-liang-lie-bd-2d',
-    label: '两列(bd)风窗',
-    comp: () => import('/@/views/vent/monitorManager/windowMonitor/components/windowLiangLieBDSVG.vue'),
-    states: progressStates(),
-  },
-  {
-    id: 'window-triple-2d',
-    label: '三扇风窗',
-    comp: () => import('/@/views/vent/monitorManager/windowMonitor/components/windowTripleSVG.vue'),
-    states: progressStates(),
-  },
-  {
-    id: 'window-yi-lie-2d',
-    label: '一列风窗',
-    comp: () => import('/@/views/vent/monitorManager/windowMonitor/components/windowYiLieSVG.vue'),
-    states: progressStates(),
-  },
-  {
-    id: 'window-zd-2d',
-    label: '自动风窗',
-    comp: () => import('/@/views/vent/monitorManager/windowMonitor/components/windowZDSVG.vue'),
-    states: degreeStates(),
-  },
-  {
-    id: 'fan-local-2d',
-    label: '局部风机',
-    comp: () => import('/@/views/vent/monitorManager/fanLocalMonitor/components/fanLocalSVG.vue'),
-    states: fanStates(),
-  },
-  {
-    id: 'fan-tunnel-2d',
-    label: '风筒风机',
-    comp: () => import('/@/views/vent/monitorManager/fanLocalMonitor/components/fanTunnelSVG.vue'),
-    states: fanStates(),
-  },
-  {
-    id: 'fan-tunnel-dual-2d',
-    label: '双风筒风机',
-    comp: () => import('/@/views/vent/monitorManager/fanLocalMonitor/components/fanTunnelDualSVG.vue'),
-    states: fanStates(),
-  },
-  {
-    id: 'main-fan-2d',
-    label: '主扇',
-    comp: () => import('/@/views/vent/monitorManager/mainFanMonitor/components/mainFanSVG.vue'),
-    states: mainFanStates,
-  },
-  {
-    id: 'main-fan-sz-2d',
-    label: '主扇(sz)',
-    comp: () => import('/@/views/vent/monitorManager/mainFanMonitor/components/mainFanSZSVG.vue'),
-    states: mainFanStates,
-  },
-  {
-    id: 'main-fan-xj-2d',
-    label: '主扇(xj)',
-    comp: () => import('/@/views/vent/monitorManager/mainFanMonitor/components/mainFanXJSVG.vue'),
-    states: mainFanStates,
-  },
-  {
-    id: 'windrect-fixed-2d',
-    label: '风向(固定)',
-    comp: () => import('/@/views/vent/monitorManager/windrectMonitor/components/fixedSVG.vue'),
-    states: noopStates,
-  },
-  {
-    id: 'windrect-scan-2d',
-    label: '风向(扫描)',
-    comp: () => import('/@/views/vent/monitorManager/windrectMonitor/components/scanSVG.vue'),
-    states: scanStates,
-  },
-  {
-    id: 'junya-2d',
-    label: '均压',
-    comp: () => import('/@/views/vent/monitorManager/balancePressMonitor/components/junyaSVG.vue'),
-    states: junyaStates,
-  },
-  {
-    id: 'zhijiang-2d',
-    label: '制浆',
-    comp: () => import('/@/views/vent/monitorManager/groutMonitor/components/zhijiangSVG.vue'),
-    states: zhijiangStates,
-  },
-];

+ 15 - 6
src/views/vent/monitorManager/dedustMonitor/components/DedustHome.vue

@@ -59,7 +59,7 @@
 
 <script setup lang="ts">
   import FourBorderBg from '/@/views/vent/comment/components/fourBorderBg.vue';
-  import { onBeforeMount, ref, onMounted, onUnmounted, computed } from 'vue';
+  import { onBeforeMount, ref, inject, onMounted, onUnmounted, computed } from 'vue';
   import { list } from '../dedust.api';
   import ventBox1 from '/@/components/vent/ventBox1.vue';
   import { mountedThree, destroy, setModelType } from '../dedust.threejs';
@@ -88,6 +88,8 @@
   });
 
   const loading = ref(false);
+  /** 全局配置(is2DModel 决定二维 / 三维模型) */
+  const globalConfig: any = inject('globalConfig');
   const deviceInfo = ref<any>({});
 
   // 将列表配置项转换为列表可用的prop
@@ -154,18 +156,25 @@
 
   onMounted(() => {
     loading.value = true;
-    mountedThree('#dedust3D', ['#dedustCss', '#dedustEnvA', '#dedustEnvB']).then(() => {
-      setModelType('dedust').finally(() => {
-        loading.value = false;
+    // 二维模式:不初始化三维,模型由 dedustSVG 装配(箭头隐藏)
+    if (!globalConfig?.is2DModel) {
+      mountedThree('#dedust3D', ['#dedustCss', '#dedustEnvA', '#dedustEnvB']).then(() => {
+        setModelType('dedust').finally(() => {
+          loading.value = false;
+        });
       });
-    });
+    } else {
+      loading.value = false;
+    }
     timer = setInterval(() => {
       getDataSource(props.deviceId);
     }, 1000);
   });
   onUnmounted(() => {
     clearInterval(timer);
-    destroy();
+    if (!globalConfig?.is2DModel) {
+      destroy();
+    }
   });
 </script>
 <style lang="less" scoped>

+ 15 - 6
src/views/vent/monitorManager/dedustMonitor/components/DedustHomeBet.vue

@@ -64,7 +64,7 @@
 
 <script setup lang="ts">
   import FourBorderBg from '/@/views/vent/comment/components/fourBorderBg.vue';
-  import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
+  import { ref, inject, onMounted, onUnmounted, computed, watch } from 'vue';
   import { list } from '../dedust.api';
   import ventBox1 from '/@/components/vent/ventBox1.vue';
   import { mountedThree, destroy, setModelType } from '../dedust.threejs';
@@ -83,6 +83,8 @@
   });
 
   const loading = ref(false);
+  /** 全局配置(is2DModel 决定二维 / 三维模型) */
+  const globalConfig: any = inject('globalConfig');
   const deviceInfo = ref<any>({});
   const deviceType = computed(() => {
     return deviceInfo.value.deviceType;
@@ -169,18 +171,25 @@
 
   onMounted(() => {
     loading.value = true;
-    mountedThree('#dedust3D', ['#dedustCss', '#dedustEnvA', '#dedustEnvB']).then(() => {
-      setModelType('dedust').finally(() => {
-        loading.value = false;
+    // 二维模式:不初始化三维,模型由 dedustSVG 装配(箭头隐藏)
+    if (!globalConfig?.is2DModel) {
+      mountedThree('#dedust3D', ['#dedustCss', '#dedustEnvA', '#dedustEnvB']).then(() => {
+        setModelType('dedust').finally(() => {
+          loading.value = false;
+        });
       });
-    });
+    } else {
+      loading.value = false;
+    }
     timer = setInterval(() => {
       getDataSource(props.deviceId);
     }, 1000);
   });
   onUnmounted(() => {
     clearInterval(timer);
-    destroy();
+    if (!globalConfig?.is2DModel) {
+      destroy();
+    }
   });
 </script>
 <style lang="less" scoped>

+ 101 - 0
src/views/vent/monitorManager/dedustMonitor/components/dedustSVG.vue

@@ -0,0 +1,101 @@
+<template>
+  <!-- 二维模型:除尘风机(复用掘进工作面模型包 tun-face-2d);本组件只保留业务驱动 -->
+  <SvgModelViewer ref="viewerRef" model-id="tun-face-2d" :identify="identify" />
+</template>
+
+<script setup lang="ts">
+  import { onBeforeUnmount, ref, watch } from 'vue';
+  import SvgModelViewer from '/@/components/vent/SvgModelViewer.vue';
+  import { useEngineAnim } from '/@/utils/svg-model/useEngineAnim';
+
+  withDefaults(
+    defineProps<{
+      /** 组件唯一标识符,提供后可以确保多个组件复用时的元素定位依旧准确 */
+      identify?: string;
+    }>(),
+    { identify: '' }
+  );
+
+  /**
+   * 除尘风机页面复用掘进工作面模型:模型整体照常显示,但 3 个流动箭头不播放、直接隐藏。
+   * 驱动保留 animate(running) 开关:true → 箭头显示并循环流动,false(默认)→ 箭头隐藏。
+   */
+  const ARROW_IDS = ['auto_anim_0', 'auto_anim_1', 'auto_anim_2'];
+  /** 一轮循环时长(10 帧,按观感可调) */
+  const LOOP_PERIOD_MS = 2200;
+
+  const viewerRef = ref<any>(null);
+  const { engine } = useEngineAnim(viewerRef);
+
+  let anims: Animation[] = [];
+  let running = false;
+
+  function stop() {
+    anims.forEach((a) => {
+      try {
+        a.cancel();
+      } catch (e) {
+        /* ignore */
+      }
+    });
+    anims = [];
+  }
+
+  function setVisible(visible: boolean) {
+    const eng = engine.value;
+    if (!eng) return;
+    ARROW_IDS.forEach((id) => {
+      const el = eng.getElement(id) as HTMLElement | undefined;
+      if (el) el.style.display = visible ? '' : 'none';
+    });
+  }
+
+  /** 开始循环流动:帧序列本身就是一段完整的小动画,直接循环播放 */
+  function start() {
+    stop();
+    const eng = engine.value;
+    if (!eng) return;
+    ARROW_IDS.forEach((id) => {
+      const el = eng.getElement(id);
+      const frames = eng.getElementInfo(id)?.frames;
+      if (!el || !frames || frames.length < 2) return;
+      // model.json 里这排箭头的首尾位移 = 相邻箭头图案间距,循环回绕时正好落到相邻箭头上
+      const keyframes = frames.map((transform) => ({ transform }));
+      const anim = (el as HTMLElement).animate(keyframes, {
+        duration: LOOP_PERIOD_MS,
+        iterations: Infinity,
+        easing: 'linear',
+        fill: 'both',
+      });
+      anims.push(anim);
+    });
+  }
+
+  /**
+   * 除尘风机二维模型开关
+   *
+   * @param flag 是否播放箭头:false(默认)→ 箭头隐藏;true → 箭头显示并循环流动
+   */
+  function animate(flag: boolean = false) {
+    running = flag;
+    setVisible(flag);
+    if (flag) start();
+    else stop();
+  }
+
+  watch(engine, (eng) => {
+    if (!eng) return;
+    if (running) {
+      setVisible(true);
+      start();
+    } else {
+      setVisible(false);
+    }
+  });
+
+  onBeforeUnmount(() => {
+    stop();
+  });
+
+  defineExpose({ animate });
+</script>

+ 20 - 0
src/views/vent/monitorManager/dedustMonitor/dedust.data.ts

@@ -1,3 +1,5 @@
+import { defineAsyncComponent } from 'vue';
+
 /** 故障相关状态配置 0 正常 1 故障 */
 export const statusConfigA = [
   { value: '0', label: '' },
@@ -195,3 +197,21 @@ export const dedustStatusConfigC = [
 //     label: '停止控制',
 //   },
 // ];
+
+/** 二维 SVG 模型组件缓存(切换模型时不重复加载) */
+const componentsCaches = new Map<string, any>();
+
+/**
+ * 获取除尘风机二维模型组件(复用掘进工作面模型包 tun-face-2d)
+ *
+ * @param is2DModel 是否为二维模型;三维模型由 dedust.threejs 的 mountedThree 负责,此处仅返回二维组件
+ */
+export function getModelComponent(is2DModel: boolean = false) {
+  if (!is2DModel) return null;
+  const name = 'dedustSVG';
+  if (!componentsCaches.has(name)) {
+    componentsCaches.set(name, import(`./components/${name}.vue`));
+  }
+  // @ts-ignore
+  return defineAsyncComponent(() => componentsCaches.get(name));
+}

+ 23 - 2
src/views/vent/monitorManager/dedustMonitor/index.vue

@@ -1,7 +1,22 @@
 <!-- eslint-disable vue/multi-word-component-names -->
 <template>
+  <!-- 二维 SVG 模型(复用掘进工作面模型包,箭头隐藏不播放) -->
+  <div
+    v-if="globalConfig?.is2DModel"
+    v-show="activeKey == 'monitor'"
+    class="dedust-2d"
+    style="width: 100%; height: 100%; position: absolute; overflow: hidden; display: flex; justify-content: center; align-items: center"
+  >
+    <component :is="modelComponent" style="z-index: 2" />
+  </div>
   <!-- 把 dedust3D 放到这里则模型可以正常旋转 -->
-  <div id="dedust3D" v-show="activeKey == 'monitor'" style="width: 100%; height: 100%; position: absolute; overflow: hidden"> </div>
+  <div
+    v-if="!globalConfig?.is2DModel"
+    id="dedust3D"
+    v-show="activeKey == 'monitor'"
+    style="width: 100%; height: 100%; position: absolute; overflow: hidden"
+  >
+  </div>
   <div class="scene-box">
     <customHeader
       :fieldNames="{ label: 'systemname', value: 'id', options: 'children' }"
@@ -57,7 +72,7 @@
 </template>
 
 <script setup lang="ts">
-  import { onBeforeMount, ref, onMounted, computed, defineAsyncComponent } from 'vue';
+  import { onBeforeMount, ref, shallowRef, inject, onMounted, computed, defineAsyncComponent } from 'vue';
   import customHeader from '/@/components/vent/customHeader.vue';
   import BottomMenu from '/@/views/vent/comment/components/bottomMenu.vue';
   import DedustHistory from './components/DedustHistory.vue';
@@ -65,6 +80,7 @@
   import { useRouter } from 'vue-router';
   import { useSystemSelect } from '/@/hooks/vent/useSystemSelect';
   import { useGlobSetting } from '/@/hooks/setting';
+  import { getModelComponent } from './dedust.data';
 
   const { sysOrgCode } = useGlobSetting();
   const DedustHomeComponent = computed(() => {
@@ -91,6 +107,11 @@
   const { currentRoute } = useRouter();
   const activeKey = ref('monitor');
 
+  /** 全局配置(is2DModel 决定二维 / 三维模型) */
+  const globalConfig: any = inject('globalConfig');
+  /** 二维模型组件(三维由 DedustHome 的 mountedThree 负责,此处仅二维有值) */
+  const modelComponent = shallowRef(getModelComponent(globalConfig?.is2DModel));
+
   const historyTable = ref();
   const alarmHistoryTable = ref();
   const handlerHistoryTable = ref();

+ 103 - 0
src/views/vent/monitorManager/tunFaceMonitor/components/tunFaceSVG.vue

@@ -0,0 +1,103 @@
+<template>
+  <!-- 二维模型:掘进工作面(模型包 public/svg-models/tun-face-2d/);本组件只保留业务驱动 -->
+  <SvgModelViewer ref="viewerRef" model-id="tun-face-2d" :identify="identify" />
+</template>
+
+<script setup lang="ts">
+  import { onBeforeUnmount, ref, watch } from 'vue';
+  import SvgModelViewer from '/@/components/vent/SvgModelViewer.vue';
+  import { useEngineAnim } from '/@/utils/svg-model/useEngineAnim';
+
+  withDefaults(
+    defineProps<{
+      /** 组件唯一标识符,提供后可以确保多个组件复用时的元素定位依旧准确 */
+      identify?: string;
+    }>(),
+    { identify: '' }
+  );
+
+  /**
+   * 掘进工作面流动箭头(与 model.json 的 elements 一一对应):
+   * 模型里 3 个箭头元素共用一个开关,运行 → 循环流动,停止 → 隐藏。
+   */
+  const ARROW_IDS = ['auto_anim_0', 'auto_anim_1', 'auto_anim_2'];
+  /** 一轮循环时长(10 帧,按观感可调) */
+  const LOOP_PERIOD_MS = 2200;
+
+  const viewerRef = ref<any>(null);
+  const { engine } = useEngineAnim(viewerRef);
+
+  /** 每个箭头元素上的 WAAPI 循环动画 */
+  let anims: Animation[] = [];
+  let running = true;
+
+  function stop() {
+    anims.forEach((a) => {
+      try {
+        a.cancel();
+      } catch (e) {
+        /* ignore */
+      }
+    });
+    anims = [];
+  }
+
+  function setVisible(visible: boolean) {
+    const eng = engine.value;
+    if (!eng) return;
+    ARROW_IDS.forEach((id) => {
+      const el = eng.getElement(id) as HTMLElement | undefined;
+      if (el) el.style.display = visible ? '' : 'none';
+    });
+  }
+
+  /** 开始循环流动:帧序列本身就是一段完整的小动画,直接循环播放 */
+  function start() {
+    stop();
+    const eng = engine.value;
+    if (!eng) return;
+    ARROW_IDS.forEach((id) => {
+      const el = eng.getElement(id);
+      const frames = eng.getElementInfo(id)?.frames;
+      if (!el || !frames || frames.length < 2) return;
+      // model.json 里这排箭头的首尾位移 = 相邻箭头图案间距,循环回绕时正好落到相邻箭头上
+      const keyframes = frames.map((transform) => ({ transform }));
+      const anim = (el as HTMLElement).animate(keyframes, {
+        duration: LOOP_PERIOD_MS,
+        iterations: Infinity,
+        easing: 'linear',
+        fill: 'both',
+      });
+      anims.push(anim);
+    });
+  }
+
+  /**
+   * 掘进工作面二维模型开关
+   *
+   * @param flag 是否运行:true(默认)→ 箭头显示并循环流动;false → 箭头隐藏(停止态)
+   */
+  function animate(flag: boolean = true) {
+    running = flag;
+    setVisible(flag);
+    if (flag) start();
+    else stop();
+  }
+
+  watch(engine, (eng) => {
+    if (!eng) return;
+    if (running) {
+      setVisible(true);
+      start();
+    } else {
+      setVisible(false);
+    }
+  });
+
+  onBeforeUnmount(() => {
+    stop();
+  });
+
+  /** 页面通过 modelRef.value?.animate?.(running) 控制(当前页面打开即常驻循环) */
+  defineExpose({ animate });
+</script>

+ 24 - 8
src/views/vent/monitorManager/tunFaceMonitor/index.vue

@@ -4,14 +4,17 @@
     class="bg"
     style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; overflow: hidden">
     <a-spin :spinning="loading" />
+    <!-- 二维 SVG 模型(掘进工作面:箭头常驻循环流动) -->
+    <component v-if="globalConfig?.is2DModel" ref="modelRef" :is="modelComponent" style="z-index: 2" />
     <div
+        v-else
         id="tunFace3DCSS"
         class="threejs-Object-CSS"
         v-show="!loading"
         style="width: 100%; height: 100%; position: absolute; pointer-events: none; overflow: hidden; z-index: 1; top: 0"
       >
     </div>
-    <div id="tunFace3D" style="width: 100%; height: 100%; position: absolute; overflow: hidden"> </div>
+    <div v-if="!globalConfig?.is2DModel" id="tunFace3D" style="width: 100%; height: 100%; position: absolute; overflow: hidden"> </div>
   </div>
   <div class="scene-box">
     <customHeader :fieldNames="{ label: 'systemname', value: 'id', options: 'children' }" :options = 'options' @change="getSelectRow" :optionValue="optionValue">掘进工作面智能管控</customHeader>
@@ -45,10 +48,10 @@
 </template>
 
 <script setup lang="ts">
-import { onBeforeMount, ref, onMounted, onUnmounted, reactive, toRaw } from 'vue';
+import { onBeforeMount, ref, shallowRef, inject, onMounted, onUnmounted, reactive, toRaw } from 'vue';
 import { mountedThree, destroy, setModelType } from './tunFace.threejs';
 import { systemList, getTableList } from './tunFace.api';
-import { monitorNav } from './tunFace.data'
+import { monitorNav, getModelComponent } from './tunFace.data'
 import customHeader from '/@/components/vent/customHeader.vue';
 import BottomMenu from '/@/views/vent/comment/components/bottomMenu.vue';
 import tunFaceVentHome from './components/tunFaceVentHome.vue';
@@ -67,6 +70,12 @@ const { currentRoute } = useRouter();
 const activeKey = ref('monitor');
 const loading = ref(false);
 
+/** 全局配置(is2DModel 决定二维 / 三维模型) */
+const globalConfig: any = inject('globalConfig');
+/** 二维模型容器(三维由 mountedThree 负责,此处仅二维有值) */
+const modelRef = ref();
+const modelComponent = shallowRef(getModelComponent(globalConfig?.is2DModel));
+
 const monitorNavData = ref(monitorNav)
 const monitorActive = ref(0)
 
@@ -92,7 +101,7 @@ const selectData = reactive({});
 function changeActive(activeValue) {
   activeKey.value = activeValue
   loading.value = true
-  if(activeKey.value === 'monitor'){
+  if(activeKey.value === 'monitor' && !globalConfig?.is2DModel){
     setModelType('tunFace')
     setTimeout(() =>{
       loading.value = false
@@ -169,10 +178,15 @@ onBeforeMount(() => {
 
 onMounted(async() => { 
   loading.value = true;
-  mountedThree().then(async () => {
-    setModelType('tunFace')
+  // 二维模式:不初始化三维,模型由 tunFaceSVG 自行装配
+  if (globalConfig?.is2DModel) {
     loading.value = false;
-  });
+  } else {
+    mountedThree().then(async () => {
+      setModelType('tunFace')
+      loading.value = false;
+    });
+  }
   if (currentRoute.value && currentRoute.value['query'] && currentRoute.value['query']['id']) {
     optionValue.value = currentRoute.value['query']['id']
   }
@@ -182,7 +196,9 @@ onMounted(async() => {
 });
 
 onUnmounted(() => {
-  destroy();
+  if (!globalConfig?.is2DModel) {
+    destroy();
+  }
 });
 
 </script>

+ 19 - 1
src/views/vent/monitorManager/tunFaceMonitor/tunFace.data.ts

@@ -1,4 +1,4 @@
-import { reactive, ref } from 'vue';
+import { defineAsyncComponent, reactive, ref } from 'vue';
 import { BasicColumn } from '/@/components/Table';
 import echarts from '/@/utils/lib/echarts';
 
@@ -847,3 +847,21 @@ export const gasPumpCtr = reactive({
   gasPump1Open: '0',
   gasPump2Open: '0',
 });
+
+/** 二维 SVG 模型组件缓存(切换模型时不重复加载) */
+const componentsCaches = new Map<string, any>();
+
+/**
+ * 获取掘进工作面二维模型组件
+ *
+ * @param is2DModel 是否为二维模型;三维模型由 tunFace.threejs 的 mountedThree 负责,此处仅返回二维组件
+ */
+export function getModelComponent(is2DModel: boolean = false) {
+  if (!is2DModel) return null;
+  const name = 'tunFaceSVG';
+  if (!componentsCaches.has(name)) {
+    componentsCaches.set(name, import(`./components/${name}.vue`));
+  }
+  // @ts-ignore
+  return defineAsyncComponent(() => componentsCaches.get(name));
+}

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott