| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818 | <template>  <el-container>    <splitpanes class="default-theme diysplitpanes">      <pane min-size="10" size="20" max-size="50" class="custom-aside">        <el-tabs          v-model="activeName"          type="border-card"          class="full-height-tabs"          @tab-click="handleTabclick"        >          <!-- 组件区域 -->          <el-tab-pane label="Library" name="Liberal">            <el-collapse v-model="colactiveNames">              <el-collapse-item                v-for="item in collapseData"                :key="item.ftypecode"                :title="item.ftypedesc"                :name="String(item.ftypecode)"              >                <div class="coms-container">                  <div                    v-for="com in item.coms"                    :key="com.comId"                    class="com-item"                    @dragstart="onDragStart($event, 'default', com)"                    :draggable="true"                  >                    <img                      :src="getImage(com.image) || getImgPath('temp.png')"                      alt="component image"                      class="com-image"                    />                    <div class="com-name">{{ com.name }}</div>                  </div>                </div>              </el-collapse-item>            </el-collapse>          </el-tab-pane>          <el-tab-pane label="Project" name="Project">            <!-- 使用自定义图标和垂直虚线 -->            <ProjectTree              :data="treeData"              :useCustomExpandIcon="true"              :nodeIcon="systemIcon"              :expandIcon="expandIcon"              :collapseIcon="collapseIcon"              :treeProps="{                label: 'label',                children: 'children',                isLeaf: 'isLeaf'              }"              @node-click="handleNodeClick"            />            <!-- 使用 Element Plus 默认箭头(无前置图标和虚线) -->            <!-- <ProjectTree :data="treeData" :useCustomExpandIcon="false" @node-click="handleNodeClick" /> -->          </el-tab-pane>          <el-tab-pane label="Result" name="Result">            <el-empty v-if="activities.length === 0" description="暂无数据" />            <el-timeline v-else>              <el-timeline-item                v-for="(activity, index) in activities"                center                :key="index"                :color="selectedIndex === index ? '#67C23A' : ''"                size="normal"                :timestamp="activity.timestamp"                @click="handleTimelineItemClick(index)"              >                {{ activity.content }}              </el-timeline-item>            </el-timeline>          </el-tab-pane>        </el-tabs>      </pane>      <!-- canvas区域 -->      <pane class="custom-main">        <splitpanes class="default-theme" horizontal>          <pane min-size="50" size="75" max-size="100" class="flow-pane">            <div class="main-header">              <div class="header-content">                <div class="image-container">                  <img :src="topoIcon" alt="Topology Image" />                </div>                <div class="text-container">                  <span>Topology</span>                </div>              </div>              <TopoButtonBar               @run-button-click="btnfunc"              @open-simulationData-dialog="handleOpenSimulationDataDialog"              @button-click="handleTopoButtonClick"              @mode-switch="handleTopoModeSwitch"              />            </div>            <!-- <div class="main-header">              <div class="topologyStyle">Topology</div> -->              <!-- <el-tabs type="border-card">                <el-tab-pane label="Topology">                  <el-space :size="spacesize" class="spaceclass">                    <el-button                      title="放大"                      @click="zoomIn"                      class="custom-icon-button"                    >                      <el-icon><ZoomIn /></el-icon>                    </el-button>                    <el-button                      title="缩小"                      @click="zoomOut"                      class="custom-icon-button"                    >                      <el-icon><ZoomOut /></el-icon>                    </el-button>                    <el-button                      title="自适应"                      @click="fitView"                      class="custom-icon-button"                    >                      <el-icon><FullScreen /></el-icon>                    </el-button>                    <el-tooltip content="重置" placement="top">                      <el-button                        title="重置"                        @click="resetTransform"                        class="custom-icon-button"                      >                        <changebak name="reset" />                      </el-button>                    </el-tooltip>                    <el-tooltip content="背景切换" placement="top">                      <el-button                        title="背景切换"                        @click="toggleDarkMode"                        class="custom-icon-button"                      >                        <changebak v-if="dark" name="sun" />                        <changebak v-else name="moon" />                      </el-button>                    </el-tooltip>                    <el-tooltip content="保存" placement="top">                      <el-button                        @click="saveproject"                        class="custom-icon-button"                      >                        <el-icon class="btn-icon" :color="iconcolor"                          ><UploadFilled                        /></el-icon>                      </el-button>                    </el-tooltip>                  </el-space>                </el-tab-pane>              </el-tabs> -->            <!-- </div> -->            <div class="flow-content" ref="flowContentRef">              <vueflow                 ref="vueflowRef"                 :jobId="jobId"                :showGrid="buttonStates.showGrid"                :fixedGrid="buttonStates.fixedGrid"                :isSelectMode="modeSwitch"                @save="handleSave"                @copy="handleCopy"                @cut="handleCut"                @paste="handlePaste"              />              <Ruler :visible="buttonStates.showRuler" :container-ref="flowContentRef" />            </div>          </pane>          <pane min-size="0" size="25" max-size="50">            <el-tabs type="border-card" style="height: 100%">              <el-tab-pane label="日志" style="height: 100%">                <el-input                  v-model="logContent"                  type="textarea"                  id="textarea_id"                  spellcheck="false"                  style="height: 100%; font-size: 12px"                  :autosize="false"                  resize="none"                />              </el-tab-pane>            </el-tabs>          </pane>        </splitpanes>      </pane>    </splitpanes>  </el-container>    <!-- 模拟数据弹窗 -->    <SLDataDialog ref="SLdatadialogref" @selectRunType="handleSelectRunType" />    <!-- 运行弹窗 -->    <RunDialog ref="RunDialogRef" :runData="arrobj" />    <!-- 单位系统弹窗 -->    <SystemUnitDialog      :visible="showSystemUnitDialog"      @update:visible="showSystemUnitDialog = $event"      @confirm="handleSystemUnitConfirm"    />    <!-- 个人单位系统弹窗 -->    <PersonUnitDialog      :visible="showPersonUnitDialog"      @update:visible="showPersonUnitDialog = $event"      @confirm="handlePersonUnitConfirm"    /></template><script setup>import { Splitpanes, Pane } from "splitpanes"import "splitpanes/dist/splitpanes.css"import { request, getImage } from "@/utils/request"import emitter from '@/utils/emitter';import { ElMessage } from "element-plus"import { useProjectStore } from "@/store/project"import { useI18n } from "vue-i18n"import vueflow from "./vueflow/index.vue"import useDragAndDrop from "./vueflow/useDnD"import { ProjectTree } from "@/components/ProjectTree"import TopoButtonBar from "@/components/layout/TopoButtonBar.vue"import Ruler from '@/components/Ruler.vue'import {  ZoomIn,  ZoomOut,  FullScreen,  Lock,  Unlock,  DocumentDelete,  Delete,  UploadFilled,  Histogram,  DeleteFilled,  Crop,  Close} from "@element-plus/icons-vue"import changebak from "./vueflow/changebak.vue"import SLDataDialog from "./dialog/SLDataDialog.vue"import RunDialog from "./dialog/RunDialog.vue"import { useVueFlow } from "@vue-flow/core"import SystemUnitDialog from "@/components/SystemUnitDialog.vue"import PersonUnitDialog from "@/components/PersonUnitDialog.vue"import systemIcon from "@/assets/img/treeSystemIcon.png"import topoIcon from "@/assets/icons/topo.png"import expandIcon from "@/assets/img/treeExpand.png"import collapseIcon from "@/assets/img/treeCollapse.png"const { zoomIn, zoomOut, fitView, setInteractive } = useVueFlow()const { t, locale } = useI18n()const projectStore = useProjectStore()const { onDragStart, onDragLeave, treeobj, onDrop } = useDragAndDrop()// 树中单元系统的弹窗显示控制const showSystemUnitDialog = ref(false)const showPersonUnitDialog = ref(false)// 存储 SystemUnitDialog 的 tableDataconst systemUnitData = ref([])const flowContentRef = ref(null)// 项目树数据const treeData = ref([  {    id: "root",    label: t("treeData.system"),    children: [      {        id: "systemSetup",        label: t("treeData.systemSetup"),        children: [          {            id: "systemUnit",            label: t("treeData.systemUnitView"),            children: []          },          {            id: "personUnit",            label: t("treeData.personUnitSetting"),            children: []          }        ]      }    ]  }])// 项目树节点点击事件const handleNodeClick = ({ node, data, event }) => {  console.log("Node clicked:", node, data, event)  if (node.id === "systemUnit") {    console.log("System Unit clicked")    showSystemUnitDialog.value = true  } else if (node.id === "personUnit") {    showPersonUnitDialog.value = true  }}// 单元系统弹窗确认事件const handleSystemUnitConfirm = () => {  showSystemUnitDialog.value = false}// 个人单元弹窗确认事件const handlePersonUnitConfirm = () => {  showPersonUnitDialog.value = false}const getImgPath = (url) => {  return new URL(`../../assets/img/${url}`, import.meta.url).href}let pid = computed(() => projectStore.pid || "")let spacesize = ref(10)let colactiveNames = ref(["1"])let collapseData = ref()const vueflowRef = ref()const dark = ref(false)let iconcolor = ref("#000")const activeName = ref("Liberal")const selectedIndex = ref(null)const activities = ref([])const jobId = ref()const SLdatadialogref = ref(null)const RunDialogRef = ref(null)const modeSwitch = ref(true) // 连接模式开关const buttonStates = ref({ // 拓扑按钮状态  showGrid: false,  fixedGrid: false})const headerbuttons = ref([  { type: "button", img: "newproject.png", name: "temp" },  { type: "button", img: "save.png", name: "temp" },  { type: "button", img: "importpro.png", name: "temp" },  { type: "button", img: "exportpro.png", name: "temp" },  { type: "divider" },  { type: "button", img: "back.png", name: "temp" },  { type: "button", img: "goon.png", name: "temp" },  { type: "divider" },  { type: "button", img: "temp.png", name: "temp" },  { type: "divider" },  { type: "button", img: "monidata.png", name: "SLdata" },  { type: "divider" },  { type: "button", img: "run.png", name: "run" },  { type: "button", img: "stop.png", name: "stop" },  { type: "button", img: "monitor.png", name: "monitor" },  { type: "button", img: "temp.png", name: "temp" },  { type: "button", img: "temp.png", name: "temp" },  { type: "button", img: "temp.png", name: "temp" },  { type: "button", img: "temp.png", name: "temp" },  { type: "divider" },  { type: "button", img: "text.png", name: "temp" },  { type: "button", img: "text2.png", name: "temp" },  { type: "button", img: "text3.png", name: "temp" },  { type: "divider" },  { type: "button", img: "temp.png", name: "temp" }])let mainbuttons = ref([  { img: "temp.png" },  { img: "temp.png" },  { img: "temp.png" },  { img: "temp.png" },  { img: "temp.png" },  { img: "temp.png" },  { img: "temp.png" },  { img: "temp.png" },  { img: "temp.png" }])const logContent = ref("")const arrobj = ref([])let websock = ref(null)let times = ref({  lockReconnect: false, //是否真正建立连接  timeout: 28 * 1000, //30秒一次心跳  timeoutObj: null, //心跳倒计时  serverTimeoutObj: null, //  timeoutnum: null //断开重连倒计时})// 用于分开不同求解运行const runtype = ref("")// 获取组件数据const getComponent = () => {  const params = {    transCode: "ES1001",    pid: pid.value  }  request(params)    .then((res) => {      console.log(res)      collapseData.value = res.rows      console.log("zujian:", collapseData.value)    })    .catch((err) => {      ElMessage.error(err.returnMsg)    })}const resetTransform = () => {  if (vueflowRef.value) {    vueflowRef.value.resetTransform()  }}const toggleDarkMode = () => {  if (vueflowRef.value) {    vueflowRef.value.toggleDarkMode()  }}const saveproject = () => {  if (vueflowRef.value) {    vueflowRef.value.saveproject()  }}const removeNode = () => {  if (vueflowRef.value) {    vueflowRef.value.removeNode()  }}const removeEdge = () => {  if (vueflowRef.value) {    vueflowRef.value.removeEdge()  }}const confirmDelete = () => {  if (vueflowRef.value) {    vueflowRef.value.confirmDelete()  }}// 处理 topoButtonBar 触发的 open-data-dialog 事件const handleOpenSimulationDataDialog = (pid) => {  if (pid) {    SLdatadialogref.value?.openDialog?.(pid);  }};const btnfunc = (name) => {  if (name === "run") {    if (runtype.value === "") {      ElMessage.error("请先设置模拟类型")      return    } else if (runtype.value === "Incompressible Transient") {      // 处理不可压缩瞬态的逻辑      RunDialogRef.value?.openDialog?.()    } else if (runtype.value === "Incompressible Steady State") {      // 处理不可压缩稳态的逻辑      runProject()    } else if (runtype.value === "Compressible Transient") {      // 处理可压缩瞬态的逻辑    } else if (runtype.value === "Compressible Steady State") {      // 处理可压缩稳态的逻辑      runProject()    } else {      ElMessage.error("未知的模拟类型")      return    }  } else if (name === "SLdata") {    nextTick(() => {      SLdatadialogref.value?.openDialog?.(pid.value)    })  }}const runProject = () => {  const params = {    transCode: "ES0013",    pid: pid.value  }  request(params)    .then((res) => {      ElMessage.success("开始运行")    })    .catch((err) => {      ElMessage.error(err.returnMsg)    })}const handleSelectRunType = (type) => {  runtype.value = type  projectStore.setruntype(type)}// 获取结果列表const getresultlist = () => {  const params = {    transCode: "ES0014",    pid: pid.value  }  request(params)    .then((res) => {      console.log("获取结果列表成功", res)      // 处理结果列表数据      activities.value = res.rows.map((item) => ({        jobId: item.jobId,        content: "结果" + item.ser,        timestamp: item.startTime.split(" +")[0]      }))    })    .catch((err) => {      console.error("获取结果列表失败", err)      ElMessage.error("获取结果列表失败")    })}const handleTabclick = (tab, event) => {  console.log("tab clicked:", tab.props.name)  if (tab.props.name === "Result") {    getresultlist()  }}const handleTimelineItemClick = (index) => {  selectedIndex.value = index  console.log("Timeline item clicked:", activities.value[index])  jobId.value = activities.value[index].jobId  vueflowRef.value?.asideDataref?.getresultData(jobId.value)}// 处理 topoButtonBar 触发的 button-click 事件const handleTopoButtonClick = ({ action, isActive }) => {  buttonStates.value[action] = isActive !== undefined ? isActive : buttonStates.value[action]  console.log(`Button clicked: ${action}, isActive: ${buttonStates.value[action]}`)}// 处理 topoButtonBar 触发的 mode-switch 事件const handleTopoModeSwitch = (value) => {  modeSwitch.value = value  // setInteractive(!value)  console.log(value ? "切换到选择模式" : "切换到连接模式")}// 新增事件处理函数const handleSave = () => {  if (vueflowRef.value) {    vueflowRef.value.saveproject();  }};const handleCopy = () => {  if (vueflowRef.value) {    vueflowRef.value.copyNodes();  }};const handleCut = () => {  if (vueflowRef.value) {    vueflowRef.value.cutNodes();  }};const handlePaste = () => {  if (vueflowRef.value) {    vueflowRef.value.pasteNodes();  }};//websockct的连接function initWebSocket() {  //初始化weosocket  const wsurl = import.meta.env.VITE_WEBSOCKET_URL + pid.value  websock = new WebSocket(wsurl)  websock.onopen = websocketonopen  websock.onmessage = websocketonmessage  websock.onerror = websocketonerror  websock.onclose = websocketclose}// Websoket连接成功事件const websocketonopen = (res) => {  console.log("WebSocket连接成功", res)  start()}// Websoket接收消息事件const websocketonmessage = (res) => {  console.log("websocket接受消息:", res.data)  if (res.data.indexOf("{") !== -1) {  } else {    if (res.data.indexOf("——开始") !== -1) {      arrobj.value = [] // 清空数据点数组    }    if (res.data.indexOf("——成功") !== -1) {      getresultlist() //刷新结果列表      const timer = setTimeout(function () {        console.log("关闭定时器")      }, 10000)    }    if (res.data.indexOf("msg=heartChec") == -1) {      // 去除空行      const cleanedLog = res.data        .split("\n")        .filter((line) => line.trim() !== "")        .join("\n")      logContent.value = logContent.value + "\n" + cleanedLog      let textarea = document.getElementById("textarea_id")      textarea.scrollTop = textarea.scrollHeight    }    // 提取数据点    const lines = res.data.split("\n")    for (const line of lines) {      const match = line.match(/value:\s*([\d.]+);\s*([\d.]+);/)      if (match) {        const step = parseFloat(match[1])        const value = parseFloat(match[2])        arrobj.value.push({ step, value })      }    }    // console.log('arrobjvalue',arrobj.value)  }  reset()}// Websoket连接错误事件const websocketonerror = (res) => {  console.log("连接错误", res)  websock.close()  reconnect()}// Websoket断开事件const websocketclose = (res) => {  console.log("断开连接", res)}// 心跳包const reconnect = () => {  if (times.value.lockReconnect) return  times.value.lockReconnect = true  //没连接上会一直重连,设置延迟避免请求过多  times.value.timeoutnum && clearTimeout(times.value.timeoutnum)  times.value.timeoutnum = setTimeout(function () {    //新连接    initWebSocket()    times.value.lockReconnect = false  }, 10000)}const reset = () => {  //重置心跳  clearTimeout(times.value.timeoutObj)  clearTimeout(times.value.serverTimeoutObj)  start()}const start = () => {  //开启心跳  times.value.timeoutObj && clearTimeout(times.value.timeoutObj)  times.value.serverTimeoutObj && clearTimeout(times.value.serverTimeoutObj)  times.value.timeoutObj = setTimeout(function () {    //这里发送一个心跳,后端收到后,返回一个心跳消息    if (websock.readyState == 1) {      //如果连接正常      websock.send("heartCheck")    } else {      //否则重连      reconnect()    }    times.value.serverTimeoutObj = setTimeout(function () {      // 超时关闭      websock.close() //如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次    }, times.value.timeout)  }, times.value.timeout)}const getlogs = () => {  const params = {    transCode: "ES0017",    pid: pid.value  }  request(params)    .then((res) => {      // console.log("获取日志成功", res)      logContent.value = res.logs        .split("\n")        .filter((line) => line.trim() !== "")        .join("\n")      // 自动滚动日志到底部      let textarea = document.getElementById("textarea_id")      textarea.scrollTop = textarea.scrollHeight    })    .catch((err) => {      console.error("获取日志失败", err)      ElMessage.error("获取日志失败")    })}onMounted(() => {  runtype.value = projectStore.runtype || ""  getComponent()  setTimeout(function () {    initWebSocket()    getlogs()  }, 1500)  emitter.on('save', handleSave);  emitter.on('copy', handleCopy);  emitter.on('cut', handleCut);  emitter.on('paste', handlePaste);});onUnmounted(() => {  emitter.off('save', handleSave);  emitter.off('copy', handleCopy);  emitter.off('cut', handleCut);  emitter.off('paste', handlePaste);});// onBeforeUnmount(() => {//   websock?.close()//   clearTimeout(times.value.timeoutObj)//   clearTimeout(times.value.serverTimeoutObj)// })</script><style scoped>.flow-pane {  display: flex;  flex-direction: column;  height: 100%;}.main-header {  background: #edf2fa;  border-radius: 0px 0px 0px 0px;  border: 2px solid #eeeeee;}.header-content {  display: flex;  align-items: center;  justify-content: space-between;  max-width: 110px;  background-color: #ffffff;  padding: 3px;  margin: 0;  border-top: #075679 3px solid;}.image-container {  flex: 1;  max-width: 45%;  padding-right: 5px;}.text-container{  padding: 0 5px 0 0;  font-size: 16px;}.diysplitpanes {  overflow: auto;}.main-header :deep(.el-tabs--border-card > .el-tabs__content) {  padding: 5px 15px;}.main-header :deep(.el-tabs__content) {  background: #ffffff;  border-radius: 0px 0px 0px 0px;  border: 1px solid #d8d8d8;}.coms-container {  display: flex;  flex-wrap: wrap;  gap: 10px;}.com-item {  display: flex;  justify-content: start;  align-items: center;  width: 130px;}.com-image {  width: 24px;  height: 24px;  object-fit: contain;}.com-name {  font-size: 11px;}.flow-content {  width: 100%;  flex: 1;  background: #ffffff;  position: relative;  overflow: hidden;}.btn-icon {  font-size: 16px;}.topologyStyle {  width: 80px;  height: 30px;  font-size: 18px;  border-top: 3px solid #77c5e6;  background-color: #ffffff;}</style>
 |