瀏覽代碼

能流关系优化

learshaw 3 月之前
父節點
當前提交
9e5af68f45
共有 3 個文件被更改,包括 1697 次插入264 次删除
  1. 1 1
      ems-ui-cloud/package.json
  2. 58 5
      ems-ui-cloud/src/api/basecfg/flowrel.js
  3. 1638 258
      ems-ui-cloud/src/views/basecfg/flowrel/index.vue

+ 1 - 1
ems-ui-cloud/package.json

@@ -69,7 +69,7 @@
     "three": "^0.156.1",
     "uuidjs": "^5.1.0",
     "vis": "^4.21.0-EOL",
-    "vis-network": "^9.0.1",
+    "vis-network": "^9.1.13",
     "vue": "2.6.12",
     "vue-count-to": "1.0.13",
     "vue-cropper": "0.5.5",

+ 58 - 5
ems-ui-cloud/src/api/basecfg/flowrel.js

@@ -1,7 +1,7 @@
 import request from '@/utils/request'
 
 // 查询能源设施能流关系列表
-export function listRel(query) {
+export function listFlowRel(query) {
   return request({
     url: '/ems/object/flowrel/list',
     method: 'get',
@@ -10,7 +10,7 @@ export function listRel(query) {
 }
 
 // 查询能源设施能流关系详细
-export function getRel(id) {
+export function getFlowRel(id) {
   return request({
     url: '/ems/object/flowrel/' + id,
     method: 'get'
@@ -18,7 +18,7 @@ export function getRel(id) {
 }
 
 // 新增能源设施能流关系
-export function addRel(data) {
+export function addFlowRel(data) {
   return request({
     url: '/ems/object/flowrel',
     method: 'post',
@@ -27,7 +27,7 @@ export function addRel(data) {
 }
 
 // 修改能源设施能流关系
-export function updateRel(data) {
+export function updateFlowRel(data) {
   return request({
     url: '/ems/object/flowrel',
     method: 'put',
@@ -36,9 +36,62 @@ export function updateRel(data) {
 }
 
 // 删除能源设施能流关系
-export function delRel(id) {
+export function delFlowRel(id) {
   return request({
     url: '/ems/object/flowrel/' + id,
     method: 'delete'
   })
 }
+
+// 批量删除能源设施能流关系
+export function delFlowRelBatch(ids) {
+  return request({
+    url: '/ems/object/flowrel/' + ids,
+    method: 'delete'
+  })
+}
+
+// 获取能流拓扑数据
+export function getFlowTopology(query) {
+  return request({
+    url: '/ems/object/flowrel/topology',
+    method: 'get',
+    params: query
+  })
+}
+
+// 验证能流关系是否存在
+export function validateFlowRel(data) {
+  return request({
+    url: '/ems/object/flowrel/validate',
+    method: 'post',
+    data: data
+  })
+}
+
+// 获取对象的上游能流
+export function getUpstreamFlow(objCode, objType) {
+  return request({
+    url: '/ems/object/flowrel/upstream',
+    method: 'get',
+    params: { objCode, objType }
+  })
+}
+
+// 获取对象的下游能流
+export function getDownstreamFlow(objCode, objType) {
+  return request({
+    url: '/ems/object/flowrel/downstream',
+    method: 'get',
+    params: { objCode, objType }
+  })
+}
+
+// 获取指定能源类型的流动关系
+export function getFlowRelByEnergyType(emsClsCode, areaCode) {
+  return request({
+    url: '/ems/object/flowrel/energytype',
+    method: 'get',
+    params: { emsClsCode, areaCode }
+  })
+}

+ 1638 - 258
ems-ui-cloud/src/views/basecfg/flowrel/index.vue

@@ -1,311 +1,1691 @@
 <template>
-  <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
-      <el-form-item label="流动介质" prop="emsCls">
-        <treeselect v-model="queryParams.emsCls" :options="emsClsOptions" :show-count="true" placeholder="请选择流动介质"  :style="{ width: '200px' }"/>
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
-        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
-      </el-form-item>
-    </el-form>
-
-    <el-row :gutter="10" class="mb8">
-      <el-col :span="1.5">
-        <el-button
-          type="primary"
-          plain
-          icon="el-icon-plus"
-          size="mini"
-          @click="handleAdd"
-          v-hasPermi="['basecfg:flowrel:add']"
-        >新增</el-button>
+  <div class="flow-rel-container">
+    <!-- 顶部工具栏 -->
+    <el-card class="toolbar-card" shadow="never">
+      <el-row :gutter="20">
+        <el-col :span="6">
+          <el-select
+            v-model="selectedArea"
+            placeholder="请选择区域"
+            clearable
+            filterable
+            @change="handleAreaChange"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="area in areaOptions"
+              :key="area.id"
+              :label="area.label"
+              :value="area.id"
+            />
+          </el-select>
+        </el-col>
+        <el-col :span="6">
+          <!-- 使用 vue-treeselect 替代 el-cascader -->
+          <treeselect
+            v-model="selectedEnergyType"
+            :options="energyTypeOptions"
+            :normalizer="normalizer"
+            :show-count="true"
+            placeholder="请选择能源类型"
+            clearable
+            searchable
+            :append-to-body="true"
+            :z-index="9999"
+            @input="handleEnergyTypeChange"
+            style="width: 100%"
+          />
+        </el-col>
+        <el-col :span="12" style="text-align: right;">
+          <el-button-group>
+            <el-button
+              type="primary"
+              icon="el-icon-plus"
+              size="small"
+              @click="handleAdd"
+              v-hasPermi="['basecfg:flowrel:add']"
+            >
+              新增能流
+            </el-button>
+            <el-button
+              :type="layoutMode === 'hierarchical' ? 'warning' : 'default'"
+              icon="el-icon-rank"
+              size="small"
+              @click="toggleLayoutMode"
+            >
+              {{ layoutMode === 'hierarchical' ? '层级布局' : '自由布局' }}
+            </el-button>
+            <el-button
+              type="success"
+              icon="el-icon-refresh"
+              size="small"
+              @click="refreshTopology"
+            >
+              刷新拓扑
+            </el-button>
+            <el-button
+              type="info"
+              icon="el-icon-zoom-in"
+              size="small"
+              @click="fitTopology"
+            >
+              适应画布
+            </el-button>
+            <el-button
+              type="warning"
+              icon="el-icon-download"
+              size="small"
+              @click="exportTopology"
+            >
+              导出图片
+            </el-button>
+          </el-button-group>
+          <el-button
+            type="default"
+            :icon="showList ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"
+            size="small"
+            @click="toggleShowList"
+            style="margin-left: 10px"
+          >
+            {{ showList ? '隐藏列表' : '显示列表' }}
+          </el-button>
+        </el-col>
+      </el-row>
+    </el-card>
+
+    <!-- 主内容区 -->
+    <el-row :gutter="20" class="main-content">
+      <!-- 左侧:对象树 -->
+      <el-col :span="5">
+        <el-card class="tree-card" shadow="never">
+          <div slot="header" class="card-header">
+            <span>设施设备树</span>
+            <el-input
+              v-model="treeFilterText"
+              placeholder="搜索设施/设备"
+              size="small"
+              prefix-icon="el-icon-search"
+              clearable
+              style="width: 60%"
+            />
+          </div>
+          <el-tree
+            ref="objectTree"
+            :data="objectTreeData"
+            :props="treeProps"
+            :filter-node-method="filterNode"
+            node-key="id"
+            default-expand-all
+            :expand-on-click-node="false"
+            highlight-current
+            @node-click="handleNodeClick"
+          >
+            <span class="custom-tree-node" slot-scope="{ node, data }">
+              <span class="tree-label">
+                <i :class="getNodeIcon(data)" :style="{ color: getNodeColor(data) }"></i>
+                {{ node.label }}
+              </span>
+              <span class="tree-actions" v-if="data.type !== 'area'">
+                <el-button
+                  type="text"
+                  size="mini"
+                  icon="el-icon-upload2"
+                  @click.stop="handleSetAsSource(data)"
+                  title="设为输出源"
+                />
+                <el-button
+                  type="text"
+                  size="mini"
+                  icon="el-icon-download"
+                  @click.stop="handleSetAsTarget(data)"
+                  title="设为输入目标"
+                />
+              </span>
+            </span>
+          </el-tree>
+        </el-card>
       </el-col>
-      <el-col :span="1.5">
-        <el-button
-          type="success"
-          plain
-          icon="el-icon-edit"
-          size="mini"
-          :disabled="single"
-          @click="handleUpdate"
-          v-hasPermi="['basecfg:flowrel:edit']"
-        >修改</el-button>
+
+      <!-- 中间:拓扑图 -->
+      <el-col :span="19">
+        <el-card class="topology-card" shadow="never">
+          <div slot="header" class="card-header">
+            <span>能流拓扑图</span>
+            <div class="topology-legend">
+              <span class="legend-item" title="电网、配电">
+                <svg width="20" height="20" viewBox="0 0 1024 1024">
+                  <path d="M563.785143 21.211429L639.926857 0l281.819429 968.850286-76.214857 21.211428L563.785143 21.211429zM45.933714 961.828571L320.073143 0l76.141714 21.211429L122.148571 983.04l-76.141714-21.211429z" fill="#1890ff"/>
+                  <path d="M83.968 841.581714l677.814857-360.667428 38.034286 63.634285-677.741714 360.594286zM167.789714 544.548571l38.034286-63.634285 677.814857 360.594285-38.034286 63.634286z" fill="#1890ff"/>
+                  <path d="M0.219429 148.48h951.954285v70.729143H0.219429zM198.217143 325.339429h555.958857v70.656H198.217143z" fill="#1890ff"/>
+                </svg>
+                网
+              </span>
+              <span class="legend-item" title="光伏、风电等发电设施">
+                <svg width="20" height="20" viewBox="0 0 64 64">
+                  <rect x="6" y="24" width="52" height="32" rx="2" fill="#1890ff" transform="skewY(-6)"/>
+                  <circle cx="50" cy="12" r="7" fill="#faad14"/>
+                  <path d="M50 2v4M42 12h4M54 12h4" stroke="#faad14" stroke-width="2"/>
+                </svg>
+                源
+              </span>
+              <span class="legend-item" title="储能电池、蓄电设施">
+                <svg width="20" height="20" viewBox="0 0 64 64">
+                  <rect x="12" y="16" width="40" height="40" rx="4" fill="#722ed1"/>
+                  <rect x="24" y="8" width="16" height="8" rx="2" fill="#722ed1"/>
+                  <rect x="18" y="28" width="28" height="6" rx="1" fill="#d3adf7"/>
+                  <rect x="18" y="40" width="28" height="6" rx="1" fill="#b37feb"/>
+                </svg>
+                储
+              </span>
+              <span class="legend-item" title="照明、空调、充电桩等用能设施">
+                <svg width="20" height="20" viewBox="0 0 64 64">
+                  <rect x="12" y="12" width="40" height="40" rx="4" fill="#fa8c16"/>
+                  <circle cx="32" cy="32" r="10" fill="none" stroke="#fff" stroke-width="3"/>
+                  <path d="M32 22v6M32 38v4M22 32h6M38 32h4" stroke="#fff" stroke-width="2"/>
+                </svg>
+                荷
+              </span>
+              <span class="legend-item" title="能量流动方向">
+                <svg width="20" height="20" viewBox="0 0 64 64">
+                  <defs>
+                    <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
+                      <polygon points="0 0, 10 3.5, 0 7" fill="#E6A23C"/>
+                    </marker>
+                  </defs>
+                  <line x1="8" y1="32" x2="48" y2="32" stroke="#E6A23C" stroke-width="3" marker-end="url(#arrowhead)"/>
+                </svg>
+                能流
+              </span>
+            </div>
+          </div>
+          <div
+            v-loading="topologyLoading"
+            element-loading-text="加载拓扑数据中..."
+            class="topology-container"
+            ref="topologyContainer"
+          >
+            <div v-if="!topologyData.nodes || topologyData.nodes.length === 0" class="empty-topology">
+              <el-empty description="暂无能流数据">
+                <el-button type="primary" size="small" @click="handleAdd">
+                  新增能流关系
+                </el-button>
+              </el-empty>
+            </div>
+            <div id="topology-network" style="width: 100%; height: 100%;"></div>
+          </div>
+        </el-card>
       </el-col>
-      <el-col :span="1.5">
+    </el-row>
+
+    <!-- 底部:能流关系列表 -->
+    <el-card v-show="showList" class="list-card" shadow="never" style="position: relative; z-index: 10;">
+      <div slot="header" class="card-header">
+        <span>能流关系列表</span>
         <el-button
           type="danger"
-          plain
           icon="el-icon-delete"
-          size="mini"
-          :disabled="multiple"
-          @click="handleDelete"
+          size="small"
+          :disabled="selectedRows.length === 0"
+          @click="handleBatchDelete"
           v-hasPermi="['basecfg:flowrel:remove']"
-        >删除</el-button>
-      </el-col>
-      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
-    </el-row>
+        >
+          批量删除
+        </el-button>
+      </div>
+      <el-table
+        v-loading="listLoading"
+        :data="flowRelList"
+        @selection-change="handleSelectionChange"
+        @row-click="handleRowClick"
+        highlight-current-row
+      >
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="序号" type="index" width="60" align="center" />
+        <el-table-column label="输出对象" min-width="200" show-overflow-tooltip>
+          <template slot-scope="scope">
+            <el-tag :type="scope.row.exportObjType === 1 ? 'primary' : 'success'" size="small">
+              {{ scope.row.exportObjType === 1 ? '设施' : '设备' }}
+            </el-tag>
+            {{ scope.row.exportObjName }}
+          </template>
+        </el-table-column>
+        <el-table-column label="流入对象" min-width="200" show-overflow-tooltip>
+          <template slot-scope="scope">
+            <el-tag :type="scope.row.inputObjType === 1 ? 'primary' : 'success'" size="small">
+              {{ scope.row.inputObjType === 1 ? '设施' : '设备' }}
+            </el-tag>
+            {{ scope.row.inputObjName }}
+          </template>
+        </el-table-column>
+        <el-table-column label="流动介质" align="center" width="120" show-overflow-tooltip>
+          <template slot-scope="scope">
+            {{ scope.row.emsClsName }}
+          </template>
+        </el-table-column>
+        <el-table-column label="容量/功率" align="center" width="120">
+          <template slot-scope="scope">
+            <span v-if="scope.row.flowCapacity">
+              {{ scope.row.flowCapacity }} {{ scope.row.flowUnit || 'kW' }}
+            </span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" align="center" width="80">
+          <template slot-scope="scope">
+            <el-switch
+              v-model="scope.row.enableStatus"
+              :active-value="1"
+              :inactive-value="0"
+              @change="handleStatusChange(scope.row)"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="200" fixed="right">
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="text"
+              icon="el-icon-view"
+              @click.stop="handleView(scope.row)"
+            >
+              查看
+            </el-button>
+            <el-button
+              size="mini"
+              type="text"
+              icon="el-icon-edit"
+              @click.stop="handleUpdate(scope.row)"
+              v-hasPermi="['basecfg:flowrel:edit']"
+            >
+              编辑
+            </el-button>
+            <el-button
+              size="mini"
+              type="text"
+              icon="el-icon-delete"
+              class="danger-text"
+              @click.stop="handleDelete(scope.row)"
+              v-hasPermi="['basecfg:flowrel:remove']"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination
+        v-show="total > 0"
+        :total="total"
+        :page.sync="queryParams.pageNum"
+        :limit.sync="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </el-card>
+
+    <!-- 新增/编辑能流关系对话框 -->
+    <el-dialog
+      :title="formTitle"
+      :visible.sync="formDialogVisible"
+      width="700px"
+      :close-on-click-modal="false"
+      @close="handleFormClose"
+    >
+      <el-form
+        ref="flowRelForm"
+        :model="flowRelForm"
+        :rules="formRules"
+        label-width="120px"
+      >
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="输出对象类型" prop="exportObjType">
+              <el-select
+                v-model="flowRelForm.exportObjType"
+                placeholder="请选择"
+                @change="handleExportTypeChange"
+                style="width: 100%"
+              >
+                <el-option label="设施" :value="1" />
+                <el-option label="设备" :value="2" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="输出对象" prop="exportObj">
+              <el-select
+                v-model="flowRelForm.exportObj"
+                placeholder="请选择输出对象"
+                filterable
+                @change="handleExportObjChange"
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="item in exportObjOptions"
+                  :key="item.code"
+                  :label="item.name"
+                  :value="item.code"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="流入对象类型" prop="inputObjType">
+              <el-select
+                v-model="flowRelForm.inputObjType"
+                placeholder="请选择"
+                @change="handleInputTypeChange"
+                style="width: 100%"
+              >
+                <el-option label="设施" :value="1" />
+                <el-option label="设备" :value="2" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="流入对象" prop="inputObj">
+              <el-select
+                v-model="flowRelForm.inputObj"
+                placeholder="请选择流入对象"
+                filterable
+                @change="handleInputObjChange"
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="item in inputObjOptions"
+                  :key="item.code"
+                  :label="item.name"
+                  :value="item.code"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
 
-    <el-table v-loading="loading" :data="relList" @selection-change="handleSelectionChange">
-      <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="输出对象" align="center" prop="exportObjName">
-        <template slot-scope="scope">
-          <span>{{ '(' + getObjType(scope.row.exportObjType) + ') ' +scope.row.exportObjName }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="流入对象" align="center" prop="inputObjName">
-        <template slot-scope="scope">
-          <span>{{ '(' + getObjType(scope.row.inputObjType) + ') ' +scope.row.inputObjName }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="流动介质" align="center" prop="emsClsName" />
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
-        <template slot-scope="scope">
-          <el-button size="mini" type="text"  icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['basecfg:flowrel:edit']">
-            修改</el-button>
-          <el-button size="mini" type="text" icon="el-icon-delete" class="deleteBtn" @click="handleDelete(scope.row)" v-hasPermi="['basecfg:flowrel:remove']">
-            删除</el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <pagination
-      v-show="total>0"
-      :total="total"
-      :page.sync="queryParams.pageNum"
-      :limit.sync="queryParams.pageSize"
-      @pagination="getList"
-    />
-
-    <!-- 添加或修改能源设施能流关系对话框 -->
-    <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
-      <el-form ref="mergeForm" :model="mergeForm" :rules="rules" label-width="80px">
-        <el-form-item label="输出类别" prop="exportObjType">
-          <el-select v-model="mergeForm.exportObjType" @change="setCodePrefix" style="width:100%">
-            <el-option v-for="item in objOptions" :label="item.name" :value="item.code" :key="item.code" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="输出对象" prop="exportObj">
-          <el-input v-model="mergeForm.exportObj"  placeholder="输出对象" />
-        </el-form-item>
-        <el-form-item label="流入类别" prop="inputObjType">
-          <el-select v-model="mergeForm.inputObjType" @change="setCodeCompose" style="width:100%">
-            <el-option v-for="item in objOptions" :label="item.name" :value="item.code" :key="item.code" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="流入对象" prop="inputObj">
-          <el-input v-model="mergeForm.inputObj"  placeholder="流入对象" />
-        </el-form-item>
         <el-form-item label="流动介质" prop="emsCls">
-          <treeselect v-model="mergeForm.emsCls" :options="emsClsOptions" :show-count="true" placeholder="请选择" />
+          <!-- 使用 vue-treeselect 替代 el-cascader -->
+          <treeselect
+            v-model="flowRelForm.emsCls"
+            :options="energyTypeOptions"
+            :normalizer="normalizer"
+            :show-count="true"
+            placeholder="请选择流动介质"
+            clearable
+            searchable
+            :append-to-body="true"
+            :z-index="10000"
+            @input="handleEmsClsChange"
+            style="width: 100%"
+          />
         </el-form-item>
-        <el-form-item label="能流描述" prop="flowDesc">
-          <el-input v-model="mergeForm.flowDesc"  placeholder="能流描述" />
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="流动容量/功率">
+              <el-input-number
+                v-model="flowRelForm.flowCapacity"
+                :min="0"
+                :precision="2"
+                placeholder="请输入"
+                style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="容量单位">
+              <el-select v-model="flowRelForm.flowUnit" placeholder="请选择单位" style="width: 100%">
+                <el-option label="kW" value="kW" />
+                <el-option label="MW" value="MW" />
+                <el-option label="kWh" value="kWh" />
+                <el-option label="MWh" value="MWh" />
+                <el-option label="m³/h" value="m³/h" />
+                <el-option label="t/h" value="t/h" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-form-item label="能流描述">
+          <el-input
+            v-model="flowRelForm.flowDesc"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入能流描述信息"
+          />
+        </el-form-item>
+
+        <el-form-item label="启用状态">
+          <el-switch
+            v-model="flowRelForm.enableStatus"
+            :active-value="1"
+            :inactive-value="0"
+          />
         </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
+        <el-button @click="formDialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="submitForm" :loading="formSubmitting">确 定</el-button>
       </div>
     </el-dialog>
+
+    <!-- 查看详情对话框 -->
+    <el-dialog
+      title="能流关系详情"
+      :visible.sync="viewDialogVisible"
+      width="600px"
+    >
+      <el-descriptions :column="2" border v-if="currentRow">
+        <el-descriptions-item label="输出对象类型">
+          <el-tag :type="currentRow.exportObjType === 1 ? 'primary' : 'success'">
+            {{ currentRow.exportObjType === 1 ? '设施' : '设备' }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="输出对象">
+          {{ currentRow.exportObjName }}
+        </el-descriptions-item>
+        <el-descriptions-item label="流入对象类型">
+          <el-tag :type="currentRow.inputObjType === 1 ? 'primary' : 'success'">
+            {{ currentRow.inputObjType === 1 ? '设施' : '设备' }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="流入对象">
+          {{ currentRow.inputObjName }}
+        </el-descriptions-item>
+        <el-descriptions-item label="流动介质" :span="2">
+          {{ currentRow.emsClsName }}
+        </el-descriptions-item>
+        <el-descriptions-item label="流动容量" :span="2">
+          <span v-if="currentRow.flowCapacity">
+            {{ currentRow.flowCapacity }} {{ currentRow.flowUnit || 'kW' }}
+          </span>
+          <span v-else>-</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="启用状态" :span="2">
+          <el-tag :type="currentRow.enableStatus === 1 ? 'success' : 'info'">
+            {{ currentRow.enableStatus === 1 ? '启用' : '禁用' }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="能流描述" :span="2">
+          {{ currentRow.flowDesc || '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="创建时间" :span="2">
+          {{ currentRow.createTime }}
+        </el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-import { listRel, getRel, delRel, addRel, updateRel } from "@/api/basecfg/flowrel";
-import { listAllFacs } from "@/api/basecfg/emsfacs"
-import { getEmsClsTree } from "@/api/commonApi"
+import {
+  listFlowRel,
+  getFlowRel,
+  addFlowRel,
+  updateFlowRel,
+  delFlowRel,
+  delFlowRelBatch,
+  getFlowTopology
+} from '@/api/basecfg/flowrel'
+import { listAllFacs } from '@/api/basecfg/emsfacs'
+import { listDevice } from '@/api/device/device'
+import { areaTreeSelect } from '@/api/basecfg/area'
+import { getEmsClsTree } from '@/api/commonApi'
+
+// 引入 vue-treeselect 组件
 import Treeselect from '@riophae/vue-treeselect'
-import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+import '@riophae/vue-treeselect/dist/vue-treeselect.css'
+
+// 引入vis-network用于拓扑图展示
+// 需要安装: npm install vis-network --save
+import { Network } from 'vis-network/standalone/esm/vis-network'
+
 export default {
-  name: "Rel",
-  components: { Treeselect },
+  name: 'FlowRel',
+  components: {
+    Treeselect
+  },
   data() {
     return {
-      // 遮罩层
-      loading: true,
-      // 选中数组
-      ids: [],
-      // 非单个禁用
-      single: true,
-      // 非多个禁用
-      multiple: true,
-      // 显示搜索条件
-      showSearch: true,
-      // 总条数
-      total: 0,
-      // 能源设施能流关系表格数据
-      relList: [],
-      // 弹出层标题
-      title: "",
-      // 是否显示弹出层
-      open: false,
+      // 筛选条件
+      selectedArea: null,
+      selectedEnergyType: null,
+      treeFilterText: '',
+
       // 区域选项
-      areaOptions: undefined,
-      // 设施选项
-      facsOptions: undefined,
-      // 能源分类树
-      emsClsOptions: [],
-      // 查询参数
+      areaOptions: [],
+
+      // 能源类型选项
+      energyTypeOptions: [],
+
+      // vue-treeselect 的字段映射
+      normalizer(node) {
+        return {
+          id: node.code || node.id,
+          label: node.name || node.label,
+          children: node.children && node.children.length > 0 ? node.children : undefined
+        }
+      },
+
+      // 对象树数据
+      objectTreeData: [],
+      treeProps: {
+        children: 'children',
+        label: 'label'
+      },
+
+      // 拓扑图
+      topologyLoading: false,
+      topologyData: {
+        nodes: [],
+        edges: []
+      },
+      network: null,
+      layoutMode: 'hierarchical', // 布局模式:'hierarchical'层级布局 | 'force'力导向布局
+
+      // 列表
+      showList: false,
+      listLoading: false,
+      flowRelList: [],
+      selectedRows: [],
+      total: 0,
       queryParams: {
         pageNum: 1,
         pageSize: 10,
-        exportObjType: null,
+        areaCode: null,
+        emsClsCode: null
+      },
+
+      // 表单
+      formDialogVisible: false,
+      viewDialogVisible: false,
+      formTitle: '',
+      formSubmitting: false,
+      flowRelForm: {
+        id: null,
+        exportObjType: 1,
         exportObj: null,
-        inputObjType: null,
+        exportObjName: null,
+        inputObjType: 2,
         inputObj: null,
+        inputObjName: null,
         emsCls: null,
+        emsClsName: null,
+        flowCapacity: null,
+        flowUnit: 'kW',
         flowDesc: null,
+        areaCode: null,
+        enableStatus: 1
       },
-      queryFacsParams: {
-        areaCode: ''
-      },
-      objOptions: [
-        { code: 0, name: "设施"},
-        { code: 2, name: "设备"},
-        { code: 3, name: "部件"}
-      ],
-      // 表单参数
-      mergeForm: {},
-      // 表单校验
-      rules: {
-        exportObj: [
-          { required: true, message: "能源输出对象不能为空", trigger: "blur" }
-        ],
-        inputObj: [
-          { required: true, message: "能源流入对象不能为空", trigger: "blur" }
-        ],
-        emsCls: [
-          { required: true, message: "能源流动介质不能为空", trigger: "blur" }
-        ]
+      formRules: {
+        exportObjType: [{ required: true, message: '请选择输出对象类型', trigger: 'change' }],
+        exportObj: [{ required: true, message: '请选择输出对象', trigger: 'change' }],
+        inputObjType: [{ required: true, message: '请选择流入对象类型', trigger: 'change' }],
+        inputObj: [{ required: true, message: '请选择流入对象', trigger: 'change' }],
+        emsCls: [{ required: true, message: '请选择流动介质', trigger: 'change' }]
       },
 
-    };
+      // 对象选项
+      exportObjOptions: [],
+      inputObjOptions: [],
+
+      // 当前行
+      currentRow: null
+    }
+  },
+  watch: {
+    treeFilterText(val) {
+      this.$refs.objectTree.filter(val)
+    }
   },
   created() {
-    this.getList();
-    this.getAllFacs();
-    this.getEmsCls();
+    this.init()
+  },
+  mounted() {
+    // 监听窗口大小变化,调整拓扑图
+    window.addEventListener('resize', this.handleResize)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.handleResize)
+    if (this.network) {
+      this.network.destroy()
+    }
   },
   methods: {
-    /** 查询能源设施能流关系列表 */
-    getList() {
-      this.loading = true;
-      listRel(this.queryParams).then(response => {
-        this.relList = response.rows;
-        this.total = response.total;
-        this.loading = false;
-      });
-    },
-    // 取消按钮
-    cancel() {
-      this.open = false;
-      this.reset();
-    },
-    // 表单重置
-    reset() {
-      this.mergeForm = {
-        id: null,
-        code: null,
-        exportFacsCode: null,
-        inputFacsCode: null,
-        emsCls: null,
-        state: null,
-        actionType: null,
-        createTime: null,
-        updateTime: null
-      };
-      this.resetForm("mergeForm");
-    },
-    /** 搜索按钮操作 */
-    handleQuery() {
-      this.queryParams.pageNum = 1;
-      this.getList();
-    },
-    /** 重置按钮操作 */
-    resetQuery() {
-      this.resetForm("queryForm");
-      this.handleQuery();
-    },
-    // 多选框选中数据
+    // ==================== 初始化 ====================
+    async init() {
+      await this.getAreaOptions()
+      await this.getEnergyTypeOptions()
+      await this.getObjectTree()
+      await this.getList()
+      await this.loadTopology()
+    },
+
+    // 获取区域选项
+    async getAreaOptions() {
+      try {
+        const res = await areaTreeSelect('0', 2)
+        this.areaOptions = this.flattenTree(res.data)
+      } catch (error) {
+        console.error('获取区域选项失败:', error)
+      }
+    },
+
+    // 扁平化树结构
+    flattenTree(tree, result = []) {
+      tree.forEach(node => {
+        result.push({ id: node.id, label: node.label })
+        if (node.children && node.children.length > 0) {
+          this.flattenTree(node.children, result)
+        }
+      })
+      return result
+    },
+
+    // 获取能源类型选项
+    async getEnergyTypeOptions() {
+      try {
+        const res = await getEmsClsTree()
+        this.energyTypeOptions = res.data || []
+      } catch (error) {
+        console.error('获取能源类型失败:', error)
+      }
+    },
+
+    // 获取对象树(设施+设备)
+    async getObjectTree() {
+      try {
+        const areaRes = await areaTreeSelect('0', 3)
+        const facsRes = await listAllFacs({})
+        const deviceRes = await listDevice({ pageNum: 1, pageSize: 9999 })
+
+        // 构建树结构
+        const tree = this.buildObjectTree(areaRes.data, facsRes.data, deviceRes.rows)
+        this.objectTreeData = tree
+      } catch (error) {
+        console.error('获取对象树失败:', error)
+      }
+    },
+
+    // 构建对象树结构
+    buildObjectTree(areas, facilities, devices) {
+      const tree = []
+      const areaMap = new Map()
+
+      // 构建区域树
+      const buildAreaTree = (nodes, parentId = '0') => {
+        return nodes
+          .filter(node => {
+            const pid = node.parentId || '0'
+            return pid === parentId
+          })
+          .map(node => {
+            const treeNode = {
+              id: node.id,
+              label: node.label,
+              type: 'area',
+              children: []
+            }
+            areaMap.set(node.id, treeNode)
+            treeNode.children = buildAreaTree(nodes, node.id)
+            return treeNode
+          })
+      }
+
+      const areaTree = buildAreaTree(areas)
+
+      // 添加设施到对应区域
+      facilities.forEach(facs => {
+        const areaNode = areaMap.get(facs.refArea)
+        if (areaNode) {
+          const facsNode = {
+            id: `facs_${facs.facsCode}`,
+            label: facs.facsName,
+            code: facs.facsCode,
+            type: 'facility',
+            objType: 1,
+            children: []
+          }
+          areaNode.children.push(facsNode)
+
+          // 添加该设施下的设备
+          const facsDevices = devices.filter(dev => dev.refFacs === facs.facsCode)
+          facsDevices.forEach(dev => {
+            facsNode.children.push({
+              id: `dev_${dev.deviceCode}`,
+              label: dev.deviceName,
+              code: dev.deviceCode,
+              type: 'device',
+              objType: 2
+            })
+          })
+        }
+      })
+
+      return areaTree
+    },
+
+    // ==================== 列表操作 ====================
+    async getList() {
+      this.listLoading = true
+      try {
+        const res = await listFlowRel(this.queryParams)
+        this.flowRelList = res.rows || []
+        this.total = res.total || 0
+      } catch (error) {
+        this.$message.error('获取能流关系列表失败')
+      } finally {
+        this.listLoading = false
+      }
+    },
+
     handleSelectionChange(selection) {
-      this.ids = selection.map(item => item.id)
-      this.single = selection.length!==1
-      this.multiple = !selection.length
+      this.selectedRows = selection
     },
-    /** 新增按钮操作 */
-    handleAdd() {
-      this.reset();
-      this.open = true;
-      this.title = "添加能源设施能流关系";
-      this.getAllFacs();
+
+    handleRowClick(row) {
+      this.currentRow = row
+      // 在拓扑图中高亮显示
+      if (this.network) {
+        this.network.selectNodes([
+          `${row.exportObjType}_${row.exportObj}`,
+          `${row.inputObjType}_${row.inputObj}`
+        ])
+        this.network.selectEdges([`${row.id}`])
+      }
     },
-    /** 修改按钮操作 */
-    handleUpdate(row) {
-      this.reset();
-      const id = row.id || this.ids
-      getRel(id).then(response => {
-        this.mergeForm = response.data;
-        this.open = true;
-        this.title = "修改能源设施能流关系";
-      });
-      this.getAllFacs();
-    },
-    /** 提交按钮 */
-    submitForm() {
-      this.$refs["mergeForm"].validate(valid => {
-        if (valid) {
-          if (this.mergeForm.id != null) {
-            updateRel(this.mergeForm).then(response => {
-              this.$modal.msgSuccess("修改成功");
-              this.open = false;
-              this.getList();
-            });
-          } else {
-            addRel(this.mergeForm).then(response => {
-              this.$modal.msgSuccess("新增成功");
-              this.open = false;
-              this.getList();
-            });
+
+    // ==================== 拓扑图操作 ====================
+    async loadTopology() {
+      this.topologyLoading = true
+      try {
+        const params = {
+          areaCode: this.queryParams.areaCode,
+          emsClsCode: this.queryParams.emsClsCode
+        }
+        const res = await getFlowTopology(params)
+
+        if (res.data) {
+          this.topologyData = res.data
+          this.$nextTick(() => {
+            this.initTopology()
+          })
+        }
+      } catch (error) {
+        console.error('加载拓扑数据失败:', error)
+        this.$message.error('加载拓扑数据失败')
+      } finally {
+        this.topologyLoading = false
+      }
+    },
+
+    initTopology() {
+      const container = document.getElementById('topology-network')
+      if (!container) return
+
+      // 数据验证
+      if (!this.topologyData || !this.topologyData.nodes || !this.topologyData.edges) {
+        console.warn('拓扑数据为空或格式不正确:', this.topologyData)
+        return
+      }
+
+      console.log('拓扑数据:', this.topologyData)
+
+      // ==================== 源网荷储 SVG 图标定义 ====================
+      // 电网/电塔图标 (网) - 使用电塔造型
+      const gridSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
+        <path d="M563.785143 21.211429L639.926857 0l281.819429 968.850286-76.214857 21.211428L563.785143 21.211429zM45.933714 961.828571L320.073143 0l76.141714 21.211429L122.148571 983.04l-76.141714-21.211429z" fill="#1890ff"/>
+        <path d="M83.968 841.581714l677.814857-360.667428 38.034286 63.634285-677.741714 360.594286-38.034286-63.634286z" fill="#1890ff"/>
+        <path d="M167.789714 544.548571l38.034286-63.634285 677.814857 360.594285-38.034286 63.634286-677.814857-360.594286zM678.034286 381.805714l228.425143-212.114285 53.394285 49.444571-228.498285 212.114286-53.394286-49.444572zM0.219429 219.209143l53.321142-49.517714 228.425143 212.187428-53.248 49.517714-228.498285-212.114285zM320.073143 0h319.853714v70.729143H320.073143V0z" fill="#1890ff"/>
+        <path d="M0.219429 148.48h951.954285v70.729143H0.219429V148.48zM198.217143 325.339429h555.958857v70.656H198.217143v-70.656z" fill="#1890ff"/>
+      </svg>`
+
+      // 光伏图标 (源) - 太阳能板 + 太阳
+      const pvSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
+        <defs>
+          <linearGradient id="pvGrad" x1="0%" y1="0%" x2="100%" y2="100%">
+            <stop offset="0%" style="stop-color:#1890ff"/>
+            <stop offset="100%" style="stop-color:#0050b3"/>
+          </linearGradient>
+        </defs>
+        <rect x="6" y="24" width="52" height="32" rx="2" fill="url(#pvGrad)" transform="skewY(-8)"/>
+        <line x1="6" y1="36" x2="58" y2="28" stroke="#fff" stroke-width="1.5" opacity="0.5"/>
+        <line x1="6" y1="48" x2="58" y2="40" stroke="#fff" stroke-width="1.5" opacity="0.5"/>
+        <line x1="22" y1="16" x2="22" y2="56" stroke="#fff" stroke-width="1.5" opacity="0.5"/>
+        <line x1="38" y1="16" x2="38" y2="56" stroke="#fff" stroke-width="1.5" opacity="0.5"/>
+        <circle cx="50" cy="12" r="8" fill="#faad14"/>
+        <path d="M50 0v4M50 20v4M38 12h4M58 12h4M42 4l2.8 2.8M55.2 17.2l2.8 2.8M42 20l2.8-2.8M55.2 6.8l2.8-2.8" stroke="#faad14" stroke-width="2" fill="none"/>
+      </svg>`
+
+      // 风电图标 (源) - 风力发电机
+      const windSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
+        <rect x="30" y="28" width="4" height="34" fill="#8c8c8c"/>
+        <polygon points="32,60 26,62 38,62" fill="#8c8c8c"/>
+        <circle cx="32" cy="24" r="4" fill="#52c41a" stroke="#389e0d" stroke-width="2"/>
+        <path d="M32 24 L32 2 C32 2 44 12 32 24" fill="#73d13d" stroke="#389e0d" stroke-width="1"/>
+        <path d="M32 24 L52 34 C52 34 44 44 32 24" fill="#73d13d" stroke="#389e0d" stroke-width="1"/>
+        <path d="M32 24 L12 34 C12 34 20 44 32 24" fill="#73d13d" stroke="#389e0d" stroke-width="1"/>
+      </svg>`
+
+      // 储能图标 (储) - 电池组
+      const storageSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
+        <rect x="10" y="16" width="44" height="44" rx="4" fill="#722ed1" stroke="#531dab" stroke-width="2"/>
+        <rect x="24" y="8" width="16" height="8" rx="2" fill="#722ed1" stroke="#531dab" stroke-width="2"/>
+        <rect x="16" y="24" width="32" height="8" rx="1" fill="#d3adf7"/>
+        <rect x="16" y="36" width="32" height="8" rx="1" fill="#b37feb"/>
+        <rect x="16" y="48" width="24" height="6" rx="1" fill="#52c41a"/>
+        <path d="M28 27h8M32 25v4" stroke="#722ed1" stroke-width="2"/>
+        <text x="44" y="53" font-size="8" fill="#fff" font-family="Arial">%</text>
+      </svg>`
+
+      // 负荷-照明图标 (荷) - 灯泡
+      const lightSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
+        <path d="M32 6 C20 6 12 16 12 26 C12 34 18 40 22 44 L22 50 L42 50 L42 44 C46 40 52 34 52 26 C52 16 44 6 32 6Z" fill="#ffc53d" stroke="#d48806" stroke-width="2"/>
+        <rect x="22" y="50" width="20" height="4" fill="#8c8c8c"/>
+        <rect x="24" y="54" width="16" height="4" fill="#8c8c8c"/>
+        <rect x="26" y="58" width="12" height="3" rx="1" fill="#8c8c8c"/>
+        <path d="M26 20 Q32 14 38 20" stroke="#fff" stroke-width="2.5" fill="none" opacity="0.6"/>
+        <path d="M24 28 Q32 22 40 28" stroke="#fff" stroke-width="2" fill="none" opacity="0.4"/>
+      </svg>`
+
+      // 负荷-空调图标 (荷) - 空调机组
+      const hvacSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
+        <rect x="6" y="10" width="52" height="32" rx="4" fill="#13c2c2" stroke="#08979c" stroke-width="2"/>
+        <rect x="12" y="16" width="40" height="14" rx="2" fill="#36cfc9"/>
+        <line x1="12" y1="34" x2="52" y2="34" stroke="#08979c" stroke-width="1"/>
+        <line x1="12" y1="36" x2="52" y2="36" stroke="#08979c" stroke-width="1"/>
+        <line x1="12" y1="38" x2="52" y2="38" stroke="#08979c" stroke-width="1"/>
+        <circle cx="46" cy="23" r="3" fill="#fff" opacity="0.5"/>
+        <path d="M20 48 Q24 44 28 48 Q32 52 36 48 Q40 44 44 48" stroke="#87e8de" stroke-width="3" fill="none"/>
+        <path d="M24 54 Q28 50 32 54 Q36 58 40 54" stroke="#87e8de" stroke-width="2" fill="none" opacity="0.6"/>
+      </svg>`
+
+      // 负荷-充电桩图标 (荷)
+      const chargeSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
+        <rect x="14" y="8" width="28" height="48" rx="4" fill="#595959" stroke="#262626" stroke-width="2"/>
+        <rect x="20" y="14" width="16" height="20" rx="2" fill="#1890ff"/>
+        <path d="M26 18 L24 26 L28 26 L26 34" stroke="#fff" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
+        <circle cx="28" cy="44" r="5" fill="#52c41a" stroke="#389e0d" stroke-width="1"/>
+        <rect x="42" y="24" width="10" height="6" rx="1" fill="#262626"/>
+        <path d="M52 27 L58 27 L58 38 Q58 42 54 42 L50 42" stroke="#262626" stroke-width="3" fill="none"/>
+        <circle cx="50" cy="42" r="3" fill="#262626"/>
+      </svg>`
+
+      // 负荷-通用设备图标 (荷)
+      const loadSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
+        <rect x="10" y="10" width="44" height="44" rx="6" fill="#fa8c16" stroke="#d46b08" stroke-width="2"/>
+        <circle cx="32" cy="32" r="14" fill="none" stroke="#fff" stroke-width="3"/>
+        <circle cx="32" cy="32" r="4" fill="#fff"/>
+        <path d="M32 14v8M32 42v8M14 32h8M42 32h8" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
+      </svg>`
+
+      // 水系统图标
+      const waterSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
+        <path d="M32 4 C32 4 52 24 52 38 C52 52 44 58 32 58 C20 58 12 52 12 38 C12 24 32 4 32 4Z" fill="#1890ff" stroke="#0050b3" stroke-width="2"/>
+        <ellipse cx="32" cy="42" rx="14" ry="6" fill="#69c0ff" opacity="0.5"/>
+        <path d="M22 36 Q32 32 42 36" stroke="#fff" stroke-width="2" fill="none" opacity="0.6"/>
+        <path d="M26 28 Q32 24 38 28" stroke="#fff" stroke-width="1.5" fill="none" opacity="0.4"/>
+      </svg>`
+
+      // 默认设施图标
+      const defaultFacilitySvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
+        <path d="M8 28 L32 8 L56 28 L56 56 L8 56 Z" fill="#409EFF" stroke="#2a6dc0" stroke-width="2"/>
+        <rect x="16" y="32" width="12" height="10" fill="#fff" opacity="0.4"/>
+        <rect x="36" y="32" width="12" height="10" fill="#fff" opacity="0.4"/>
+        <rect x="26" y="42" width="12" height="14" fill="#2a6dc0"/>
+      </svg>`
+
+      // 默认设备图标
+      const defaultDeviceSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
+        <circle cx="32" cy="32" r="26" fill="#67C23A" stroke="#4a9e31" stroke-width="2"/>
+        <rect x="22" y="22" width="20" height="20" rx="3" fill="#fff" opacity="0.3"/>
+        <circle cx="32" cy="32" r="6" fill="#fff"/>
+        <circle cx="32" cy="32" r="2" fill="#4a9e31"/>
+      </svg>`
+
+      // 根据节点信息获取对应的SVG图标和类别
+      const getNodeCategory = (node) => {
+        if (!node) return { category: 'default', level: 1 }
+
+        const label = (node.label || '').toLowerCase()
+        const code = (node.code || '').toLowerCase()
+
+        // 电网 (网) - level 0
+        if (label.includes('电网') || label.includes('变压器') || label.includes('配电') ||
+          label.includes('供电') || code.includes('grid') || code.includes('transformer')) {
+          return { category: 'grid', level: 0, svg: gridSvg, color: '#1890ff', name: '电网' }
+        }
+
+        // 光伏 (源) - level 0
+        if (label.includes('光伏') || label.includes('pv') || label.includes('太阳能') ||
+          code.includes('pv') || code.includes('solar')) {
+          return { category: 'pv', level: 0, svg: pvSvg, color: '#faad14', name: '光伏' }
+        }
+
+        // 风电 (源) - level 0
+        if (label.includes('风电') || label.includes('风力') || label.includes('风机') ||
+          code.includes('wind')) {
+          return { category: 'wind', level: 0, svg: windSvg, color: '#52c41a', name: '风电' }
+        }
+
+        // 储能 (储) - level 1
+        if (label.includes('储能') || label.includes('电池') || label.includes('蓄电') ||
+          label.includes('储电') || code.includes('battery') || code.includes('storage') ||
+          code.includes('ess')) {
+          return { category: 'storage', level: 1, svg: storageSvg, color: '#722ed1', name: '储能' }
+        }
+
+        // 照明 (荷) - level 2
+        if (label.includes('照明') || label.includes('灯') || label.includes('路灯') ||
+          code.includes('light') || code.includes('lamp')) {
+          return { category: 'light', level: 2, svg: lightSvg, color: '#ffc53d', name: '照明' }
+        }
+
+        // 空调/暖通 (荷) - level 2
+        if (label.includes('空调') || label.includes('暖通') || label.includes('制冷') ||
+          label.includes('冷水') || label.includes('热水') || label.includes('hvac') ||
+          code.includes('hvac') || code.includes('ac')) {
+          return { category: 'hvac', level: 2, svg: hvacSvg, color: '#13c2c2', name: '暖通' }
+        }
+
+        // 充电桩 (荷) - level 2
+        if (label.includes('充电') || label.includes('充电桩') || label.includes('充电站') ||
+          code.includes('charge') || code.includes('ev')) {
+          return { category: 'charge', level: 2, svg: chargeSvg, color: '#595959', name: '充电' }
+        }
+
+        // 水系统
+        if (label.includes('水') || label.includes('给水') || label.includes('供水') ||
+          code.includes('water')) {
+          return { category: 'water', level: 2, svg: waterSvg, color: '#1890ff', name: '水系统' }
+        }
+
+        // 其他负荷设施 (荷) - level 2
+        if (node.type === 1) {
+          return { category: 'facility', level: 2, svg: defaultFacilitySvg, color: '#409EFF', name: '设施' }
+        }
+
+        // 设备 - level 3
+        return { category: 'device', level: 3, svg: defaultDeviceSvg, color: '#67C23A', name: '设备' }
+      }
+
+      // 将SVG转换为Data URL
+      const svgToDataUrl = (svg) => {
+        return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg)
+      }
+
+      // 节点数据
+      const nodes = this.topologyData.nodes.map(node => {
+        const categoryInfo = getNodeCategory(node)
+
+        return {
+          id: node.id,
+          label: node.label,
+          shape: 'image',
+          image: svgToDataUrl(categoryInfo.svg),
+          size: 30,
+          level: categoryInfo.level,
+          color: {
+            border: categoryInfo.color,
+            highlight: {
+              border: '#F56C6C'
+            }
+          },
+          font: {
+            color: '#303133',
+            size: 12,
+            face: 'Microsoft YaHei, Arial',
+            background: 'rgba(255,255,255,0.8)',
+            strokeWidth: 0
+          },
+          title: `【${categoryInfo.name}】${node.label}`,
+          // 保存原始信息
+          _category: categoryInfo.category,
+          _categoryName: categoryInfo.name
+        }
+      })
+
+      // 边数据
+      const edges = this.topologyData.edges.map(edge => ({
+        id: edge.id,
+        from: edge.from,
+        to: edge.to,
+        label: edge.label,
+        arrows: 'to',
+        color: {
+          color: '#E6A23C',
+          highlight: '#F56C6C'
+        },
+        width: 2,
+        smooth: {
+          type: 'cubicBezier',
+          roundness: 0.4
+        },
+        title: `${edge.label}\n容量: ${edge.capacity || '-'}`
+      }))
+
+      const data = { nodes, edges }
+
+      // 根据布局模式配置不同的选项
+      let layoutConfig, physicsConfig
+
+      if (this.layoutMode === 'hierarchical') {
+        // 层级布局配置
+        layoutConfig = {
+          hierarchical: {
+            enabled: true,
+            direction: 'UD',        // 从上到下(Up-Down)
+            sortMethod: 'directed', // 根据边的方向排序
+            levelSeparation: 200,   // 层级间距
+            nodeSpacing: 150,       // 同层节点间距
+            treeSpacing: 200,       // 树之间的间距
+            blockShifting: true,    // 允许块移动以减少边交叉
+            edgeMinimization: true, // 最小化边交叉
+            parentCentralization: true  // 父节点居中
           }
         }
-      });
+        physicsConfig = {
+          enabled: false  // 层级布局时禁用物理引擎
+        }
+      } else {
+        // 力导向布局配置
+        layoutConfig = {
+          randomSeed: 2  // 固定随机种子使布局稳定
+        }
+        physicsConfig = {
+          enabled: true,
+          barnesHut: {
+            gravitationalConstant: -8000,
+            centralGravity: 0.3,
+            springLength: 200,
+            springConstant: 0.04,
+            damping: 0.09,
+            avoidOverlap: 0.5
+          },
+          stabilization: {
+            iterations: 200,
+            updateInterval: 25
+          }
+        }
+      }
+
+      const options = {
+        nodes: {
+          borderWidth: 2,
+          borderWidthSelected: 3,
+          size: 25,
+          font: {
+            size: 14,
+            color: '#ffffff'
+          }
+        },
+        edges: {
+          width: 2,
+          selectionWidth: 3,
+          font: {
+            size: 12,
+            color: '#606266',
+            background: '#ffffff',
+            strokeWidth: 0
+          }
+        },
+        layout: layoutConfig,
+        physics: physicsConfig,
+        interaction: {
+          hover: true,
+          tooltipDelay: 100,
+          zoomView: true,
+          dragView: true
+        }
+      }
+
+      // 销毁旧实例
+      if (this.network) {
+        this.network.destroy()
+      }
+
+      // 创建新实例
+      this.network = new Network(container, data, options)
+
+      // 绑定事件
+      this.network.on('click', this.handleTopologyClick)
+      this.network.on('doubleClick', this.handleTopologyDoubleClick)
+    },
+
+    handleTopologyClick(params) {
+      if (params.nodes.length > 0) {
+        const nodeId = params.nodes[0]
+        console.log('点击节点:', nodeId)
+      } else if (params.edges.length > 0) {
+        const edgeId = params.edges[0]
+        const edge = this.flowRelList.find(item => String(item.id) === edgeId)
+        if (edge) {
+          this.handleView(edge)
+        }
+      }
+    },
+
+    handleTopologyDoubleClick(params) {
+      if (params.edges.length > 0) {
+        const edgeId = params.edges[0]
+        const edge = this.flowRelList.find(item => String(item.id) === edgeId)
+        if (edge) {
+          this.handleUpdate(edge)
+        }
+      }
+    },
+
+    refreshTopology() {
+      this.loadTopology()
+      this.$message.success('拓扑图已刷新')
     },
-    /** 删除按钮操作 */
+
+    fitTopology() {
+      if (this.network) {
+        this.network.fit({
+          animation: {
+            duration: 500,
+            easingFunction: 'easeInOutQuad'
+          }
+        })
+      }
+    },
+
+    exportTopology() {
+      if (!this.network) return
+
+      const canvas = this.$refs.topologyContainer.querySelector('canvas')
+      if (canvas) {
+        canvas.toBlob(blob => {
+          const url = URL.createObjectURL(blob)
+          const a = document.createElement('a')
+          a.href = url
+          a.download = `能流拓扑图_${new Date().getTime()}.png`
+          a.click()
+          URL.revokeObjectURL(url)
+          this.$message.success('导出成功')
+        })
+      }
+    },
+
+    toggleLayoutMode() {
+      // 切换布局模式
+      this.layoutMode = this.layoutMode === 'hierarchical' ? 'force' : 'hierarchical'
+
+      // 重新初始化拓扑图
+      this.$nextTick(() => {
+        this.initTopology()
+        this.$message.success(`已切换到${this.layoutMode === 'hierarchical' ? '层级' : '自由'}布局`)
+      })
+    },
+
+    handleResize() {
+      if (this.network) {
+        this.network.redraw()
+      }
+    },
+
+    // 切换显示列表
+    toggleShowList() {
+      this.showList = !this.showList
+      if (this.showList) {
+        // 显示列表时,延迟滚动到列表位置
+        this.$nextTick(() => {
+          const listCard = document.querySelector('.list-card')
+          if (listCard) {
+            listCard.scrollIntoView({ behavior: 'smooth', block: 'start' })
+          }
+        })
+      }
+    },
+
+    // ==================== 树操作 ====================
+    filterNode(value, data) {
+      if (!value) return true
+      return data.label.indexOf(value) !== -1
+    },
+
+    getNodeIcon(data) {
+      const iconMap = {
+        area: 'el-icon-office-building',
+        facility: 'el-icon-school',
+        device: 'el-icon-cpu'
+      }
+      return iconMap[data.type] || 'el-icon-folder'
+    },
+
+    getNodeColor(data) {
+      const colorMap = {
+        area: '#909399',
+        facility: '#409EFF',
+        device: '#67C23A'
+      }
+      return colorMap[data.type] || '#909399'
+    },
+
+    handleNodeClick(data) {
+      if (data.type === 'area') return
+      console.log('选中节点:', data)
+    },
+
+    handleSetAsSource(data) {
+      this.flowRelForm.exportObjType = data.objType
+      this.flowRelForm.exportObj = data.code
+      this.flowRelForm.exportObjName = data.label
+      this.handleAdd()
+    },
+
+    handleSetAsTarget(data) {
+      this.flowRelForm.inputObjType = data.objType
+      this.flowRelForm.inputObj = data.code
+      this.flowRelForm.inputObjName = data.label
+      this.handleAdd()
+    },
+
+    // ==================== 筛选操作 ====================
+    handleAreaChange(value) {
+      this.queryParams.areaCode = value
+      this.queryParams.pageNum = 1
+      this.getList()
+      this.loadTopology()
+    },
+
+    handleEnergyTypeChange(value) {
+      this.queryParams.emsClsCode = value
+      this.queryParams.pageNum = 1
+      this.getList()
+      this.loadTopology()
+    },
+
+    // ==================== 表单操作 ====================
+    handleAdd() {
+      this.resetForm()
+      this.formTitle = '新增能流关系'
+      this.formDialogVisible = true
+      this.loadExportObjOptions()
+      this.loadInputObjOptions()
+    },
+
+    handleUpdate(row) {
+      this.resetForm()
+      getFlowRel(row.id).then(res => {
+        this.flowRelForm = { ...res.data }
+        this.formTitle = '编辑能流关系'
+        this.formDialogVisible = true
+        this.loadExportObjOptions()
+        this.loadInputObjOptions()
+      })
+    },
+
+    handleView(row) {
+      this.currentRow = row
+      this.viewDialogVisible = true
+    },
+
     handleDelete(row) {
-      const ids = row.id || this.ids;
-      this.$modal.confirm('是否确认删除能源设施能流关系编号为"' + ids + '"的数据项?').then(function() {
-        return delRel(ids);
+      this.$confirm(`确认删除从"${row.exportObjName}"到"${row.inputObjName}"的能流关系吗?`, '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
       }).then(() => {
-        this.getList();
-        this.$modal.msgSuccess("删除成功");
-      }).catch(() => {});
-    },
-    getObjType(objType) {
-      const objTypeMap = {
-        1: '设施',
-        2: '设备',
-        3: '部件'
-      };
-      return objTypeMap[objType] || '未知';
-    },
-    getAllFacs() {
-      listAllFacs(this.queryFacsParams).then(response =>{
-        this.facsOptions = response.data;
+        delFlowRel(row.id).then(() => {
+          this.$message.success('删除成功')
+          this.getList()
+          this.loadTopology()
+        })
       })
     },
-    /** 查询能源树结构 */
-    getEmsCls() {
-      getEmsClsTree().then(response => {
-        this.emsClsOptions = response.data;
-      });
+
+    handleBatchDelete() {
+      const ids = this.selectedRows.map(item => item.id).join(',')
+      this.$confirm('确认删除选中的能流关系吗?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        delFlowRelBatch(ids).then(() => {
+          this.$message.success('删除成功')
+          this.getList()
+          this.loadTopology()
+        })
+      })
     },
-    setCodePrefix() {
-      this.mergeForm.code = this.mergeForm.exportFacsCode;
+
+    handleStatusChange(row) {
+      updateFlowRel(row).then(() => {
+        this.$message.success('状态更新成功')
+        this.loadTopology()
+      }).catch(() => {
+        row.enableStatus = row.enableStatus === 1 ? 0 : 1
+      })
     },
-    setCodeCompose() {
-      this.mergeForm.code = this.mergeForm.exportFacsCode + '_' +this.mergeForm.inputFacsCode;
+
+    submitForm() {
+      this.$refs.flowRelForm.validate(valid => {
+        if (!valid) return
+
+        this.formSubmitting = true
+
+        const formData = { ...this.flowRelForm }
+
+        // 获取能源类型名称
+        if (formData.emsCls) {
+          formData.emsClsName = this.getEmsClsName(formData.emsCls)
+        }
+
+        const apiMethod = formData.id ? updateFlowRel : addFlowRel
+
+        apiMethod(formData).then(() => {
+          this.$message.success(formData.id ? '修改成功' : '新增成功')
+          this.formDialogVisible = false
+          this.getList()
+          this.loadTopology()
+        }).finally(() => {
+          this.formSubmitting = false
+        })
+      })
     },
+
+    resetForm() {
+      this.flowRelForm = {
+        id: null,
+        exportObjType: 1,
+        exportObj: null,
+        exportObjName: null,
+        inputObjType: 2,
+        inputObj: null,
+        inputObjName: null,
+        emsCls: null,
+        emsClsName: null,
+        flowCapacity: null,
+        flowUnit: 'kW',
+        flowDesc: null,
+        areaCode: null,
+        enableStatus: 1
+      }
+      if (this.$refs.flowRelForm) {
+        this.$refs.flowRelForm.resetFields()
+      }
+    },
+
+    handleFormClose() {
+      this.resetForm()
+    },
+
+    // ==================== 表单联动 ====================
+    handleExportTypeChange() {
+      this.flowRelForm.exportObj = null
+      this.flowRelForm.exportObjName = null
+      this.loadExportObjOptions()
+    },
+
+    handleInputTypeChange() {
+      this.flowRelForm.inputObj = null
+      this.flowRelForm.inputObjName = null
+      this.loadInputObjOptions()
+    },
+
+    async loadExportObjOptions() {
+      if (this.flowRelForm.exportObjType === 1) {
+        // 加载设施
+        const res = await listAllFacs({})
+        this.exportObjOptions = res.data.map(item => ({
+          code: item.facsCode,
+          name: item.facsName
+        }))
+      } else {
+        // 加载设备
+        const res = await listDevice({ pageNum: 1, pageSize: 9999 })
+        this.exportObjOptions = res.rows.map(item => ({
+          code: item.deviceCode,
+          name: item.deviceName
+        }))
+      }
+    },
+
+    async loadInputObjOptions() {
+      if (this.flowRelForm.inputObjType === 1) {
+        // 加载设施
+        const res = await listAllFacs({})
+        this.inputObjOptions = res.data.map(item => ({
+          code: item.facsCode,
+          name: item.facsName
+        }))
+      } else {
+        // 加载设备
+        const res = await listDevice({ pageNum: 1, pageSize: 9999 })
+        this.inputObjOptions = res.rows.map(item => ({
+          code: item.deviceCode,
+          name: item.deviceName
+        }))
+      }
+    },
+
+    handleExportObjChange(value) {
+      const obj = this.exportObjOptions.find(item => item.code === value)
+      if (obj) {
+        this.flowRelForm.exportObjName = obj.name
+      }
+    },
+
+    handleInputObjChange(value) {
+      const obj = this.inputObjOptions.find(item => item.code === value)
+      if (obj) {
+        this.flowRelForm.inputObjName = obj.name
+      }
+    },
+
+    handleEmsClsChange(value) {
+      if (value) {
+        this.flowRelForm.emsClsName = this.getEmsClsName(value)
+      }
+    },
+
+    // ==================== 工具方法 ====================
+    getEmsClsName(code) {
+      // 根据code获取名称(递归查找)
+      const findName = (nodes, targetCode) => {
+        for (const node of nodes) {
+          const nodeCode = node.code || node.id
+          const nodeName = node.name || node.label
+          if (nodeCode === targetCode) {
+            return nodeName
+          }
+          if (node.children && node.children.length > 0) {
+            const result = findName(node.children, targetCode)
+            if (result) return result
+          }
+        }
+        return ''
+      }
+      return findName(this.energyTypeOptions, code)
+    }
   }
-};
+}
 </script>
+
+<style lang="scss" scoped>
+.flow-rel-container {
+  padding: 20px;
+  background: #f0f2f5;
+  min-height: calc(100vh - 84px);
+  overflow: visible; /* 确保内容不被截断 */
+
+  .toolbar-card {
+    margin-bottom: 20px;
+    position: relative;
+    z-index: 100;
+  }
+
+  .main-content {
+    margin-bottom: 20px;
+  }
+
+  .tree-card,
+  .topology-card,
+  .list-card {
+    height: 100%;
+
+    .card-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+
+      .topology-legend {
+        display: flex;
+        gap: 16px;
+        align-items: center;
+
+        .legend-item {
+          display: flex;
+          align-items: center;
+          gap: 4px;
+          font-size: 13px;
+          color: #606266;
+          cursor: default;
+          padding: 4px 8px;
+          border-radius: 4px;
+          transition: background-color 0.2s;
+
+          &:hover {
+            background-color: #f5f7fa;
+          }
+
+          svg {
+            flex-shrink: 0;
+          }
+
+          i {
+            font-size: 16px;
+          }
+        }
+      }
+    }
+  }
+
+  .tree-card {
+    ::v-deep .el-card__body {
+      padding: 10px;
+      max-height: 600px;
+      overflow-y: auto;
+    }
+
+    .custom-tree-node {
+      flex: 1;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      font-size: 14px;
+      padding-right: 8px;
+
+      .tree-label {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+
+        i {
+          font-size: 16px;
+        }
+      }
+
+      .tree-actions {
+        display: none;
+
+        .el-button {
+          padding: 5px;
+        }
+      }
+
+      &:hover .tree-actions {
+        display: flex;
+        gap: 5px;
+      }
+    }
+  }
+
+  .topology-card {
+    ::v-deep .el-card__body {
+      padding: 10px;
+    }
+
+    .topology-container {
+      height: 600px;
+      background: #ffffff;
+      border: 1px solid #e4e7ed;
+      border-radius: 4px;
+      position: relative;
+
+      .empty-topology {
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+    }
+  }
+
+  .list-card {
+    ::v-deep .el-card__body {
+      padding: 20px;
+    }
+
+    /* 确保列表卡片在画布上方正确显示 */
+    position: relative;
+    z-index: 10;
+    background-color: #fff;
+    margin-top: 20px;
+  }
+
+  .danger-text {
+    color: #f56c6c;
+
+    &:hover {
+      color: #f78989;
+    }
+  }
+}
+
+/* vue-treeselect 样式调整 */
+::v-deep .vue-treeselect {
+  .vue-treeselect__control {
+    height: 32px;
+    border-radius: 4px;
+  }
+
+  .vue-treeselect__placeholder,
+  .vue-treeselect__single-value {
+    line-height: 30px;
+  }
+}
+</style>
+
+<!-- 全局样式,用于修复 vue-treeselect 下拉菜单层级问题 -->
+<style lang="scss">
+/* 修复 vue-treeselect 下拉菜单被遮挡的问题 */
+.vue-treeselect__menu-container {
+  z-index: 9999 !important;
+}
+
+.vue-treeselect__menu {
+  z-index: 9999 !important;
+  background-color: #fff !important;
+  border: 1px solid #dcdfe6 !important;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1) !important;
+}
+
+/* 确保下拉框容器有正确的层级 */
+.vue-treeselect {
+  position: relative;
+  z-index: 1;
+}
+
+.vue-treeselect--open {
+  z-index: 9998 !important;
+}
+
+/* 工具栏卡片需要有更高的层级以便下拉菜单能正常显示 */
+.flow-rel-container .toolbar-card {
+  position: relative;
+  z-index: 100;
+}
+</style>