yanghao 3 дней назад
Родитель
Сommit
c6b570e6f5

+ 1 - 1
.env.local

@@ -4,7 +4,7 @@ NODE_ENV=development
 VITE_DEV=true
 
 # 请求路径  http://192.168.188.149:48080  https://iot.deepoil.cc  http://172.26.0.56:48080
-VITE_BASE_URL='https://iot.deepoil.cc'
+VITE_BASE_URL='http://172.26.0.56:48080'
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
 VITE_UPLOAD_TYPE=server

+ 56 - 0
src/api/pms/qhse/index.ts

@@ -287,3 +287,59 @@ export const IotHiddenApi = {
     return await request.put({ url: `/rq/iot-hazard/rectify`, data })
   }
 }
+
+// SOC数据源分析
+export const IotSocApi = {
+  // 获得SOC数据源分析分页
+  getSocList: async (params) => {
+    return await request.get({ url: `/rq/iot-soc-source/page`, params })
+  },
+  // 删除SOC数据源分析
+  deleteSoc: async (id) => {
+    return await request.delete({ url: `/rq/iot-soc-source/delete?id=` + id })
+  },
+  // 获取上级分类
+  getParentCategory: async () => {
+    return await request.get({ url: `/rq/iot-soc-source/simple-list` })
+  },
+  // 查询SOC分类详情
+  getSocClassify: async (id: number) => {
+    return await request.get({ url: `/rq/iot-soc-source/get?id=` + id })
+  },
+  // 添加SOC数据源分析
+  createSoc: async (data) => {
+    return await request.post({ url: `/rq/iot-soc-source/create`, data })
+  },
+  // 修改SOC数据源分析
+  updateSoc: async (data) => {
+    return await request.put({ url: `/rq/iot-soc-source/update`, data })
+  }
+}
+
+// SOC卡汇总
+export const IotSocSummaryApi = {
+  // 获得SOC卡汇总分页
+  getIotSocSummaryPage: async (params) => {
+    return await request.get({ url: `/rq/iot-soc-summary/page`, params })
+  },
+  // 删除SOC卡汇总
+  deleteIotSocSummary: async (id) => {
+    return await request.delete({ url: `/rq/iot-soc-summary/delete?id=` + id })
+  },
+  // 添加SOC卡汇总
+  createIotSocSummary: async (data) => {
+    return await request.post({ url: `/rq/iot-soc-summary/create`, data })
+  },
+  // 修改SOC卡汇总
+  updateIotSocSummary: async (data) => {
+    return await request.put({ url: `/rq/iot-soc-summary/update`, data })
+  },
+  // 导出SOC卡汇总 Excel
+  exportIotSocSummary: async (params) => {
+    return await request.download({ url: `/rq/iot-soc-summary/export-excel`, params })
+  },
+  // 获取详情
+  getIotSocSummary: async (id) => {
+    return await request.get({ url: `/rq/iot-soc-summary/get?id=` + id })
+  }
+}

+ 26 - 2
src/views/pms/qhse/certificate.vue

@@ -56,8 +56,9 @@
           v-loading="loading"
           :data="list"
           :stripe="true"
-          height="calc(85vh - 130px)"
+          height="calc(85vh - 203px)"
           :show-overflow-tooltip="true"
+          :row-style="tableRowStyle"
         >
           <el-table-column :label="t('monitor.serial')" width="70" align="center">
             <template #default="scope">
@@ -110,7 +111,7 @@
             :label="t('devicePerson.operation')"
             align="center"
             fixed="right"
-            min-width="150px"
+            min-width="180px"
           >
             <template #default="scope">
               <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
@@ -135,6 +136,14 @@
           @pagination="getList"
         />
       </ContentWrap>
+
+      <ContentWrap style="margin-top: -5px">
+        <el-alert title="证书已过期红色预警" type="error" show-icon :closable="false">
+          <template #icon>
+            <Bell />
+          </template>
+        </el-alert>
+      </ContentWrap>
     </el-col>
   </el-row>
 
@@ -517,6 +526,13 @@ const closeDialog = () => {
   resetForm()
 }
 
+const tableRowStyle = ({ row }) => {
+  if (row.expired) {
+    return { backgroundColor: '#ffe6e6' }
+  }
+  return {}
+}
+
 // 提交表单
 const submitForm = async () => {
   if (!formRef.value) return
@@ -610,4 +626,12 @@ onMounted(async () => {
   display: flex;
   justify-content: center;
 }
+
+::deep(.el-table__body tr.expired-row) {
+  background-color: #ffe6e6 !important;
+}
+
+::deep(.el-table__body tr.expired-row:hover td) {
+  background-color: #ffcccc !important;
+}
 </style>

+ 1 - 1
src/views/pms/qhse/faultReport/index.vue

@@ -93,7 +93,7 @@
             min-width="150"
             show-overflow-tooltip
           />
-          <el-table-column prop="deptName" label="部门名称" align="center" />
+          <el-table-column prop="deptName" label="部门名称" align="center" width="100" />
           <el-table-column prop="dutyPerson" label="现场负责人" align="center" width="100" />
           <el-table-column prop="actualTime" label="创建时间" align="center" min-width="150">
             <template #default="{ row }">

+ 264 - 242
src/views/pms/qhse/hazard/index.vue

@@ -1,268 +1,283 @@
 <template>
-  <div class="hazard-table-container">
-    <ContentWrap style="border: 0">
-      <!-- 搜索工作栏 -->
-      <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true">
-        <el-form-item label="风险等级" prop="riskGrade">
-          <el-select
-            v-model="queryParams.riskGrade"
-            placeholder="请选择风险等级"
-            clearable
-            style="width: 200px"
-          >
-            <el-option
-              v-for="dict in getStrDictOptions(DICT_TYPE.DANGER_GRADE)"
-              :key="dict.value"
-              :label="dict.label"
-              :value="dict.value"
-            />
-          </el-select>
-        </el-form-item>
-
-        <el-form-item>
-          <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-          <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-          <el-button type="primary" @click="openForm('create')" color="#626aef">
-            <Icon icon="ep:plus" class="mr-5px" /> 新增
-          </el-button>
-          <el-button type="success" plain @click="handleExport" :loading="exportLoading">
-            <Icon icon="ep:download" class="mr-5px" /> 导出
-          </el-button>
-        </el-form-item>
-      </el-form>
-    </ContentWrap>
-
-    <ContentWrap style="border: 0">
-      <el-table
-        :data="tableData"
-        border
-        style="width: 100%"
-        :header-cell-style="{ background: '#f5f7fa', color: '#333' }"
-        :cell-style="{ padding: '12px 8px' }"
-        height="70vh"
-      >
-        <!-- 区域/位置 列(已合并) -->
-        <el-table-column prop="region" label="区域/位置" width="150" align="center" fixed="left" />
-
-        <!-- 其他列保持不变 -->
-        <el-table-column label="序号" width="70" align="center">
-          <template #default="scope">
-            {{ scope.$index + 1 }}
-          </template>
-        </el-table-column>
-        <el-table-column
-          prop="elementDescription"
-          label="危害因素描述"
-          width="200"
-          align="center"
-        />
-        <el-table-column prop="maybeResult" min-width="320" label="可导致的后果" align="center" />
-
-        <!-- 风险评价列保持不变 -->
-        <el-table-column label="风险评价" width="320" align="center">
-          <el-table-column prop="evalKn" label="可能性 (L)" width="80" align="center">
-            <template #default="{ row }">
-              {{ row.evalKn }}
-            </template>
-          </el-table-column>
-          <el-table-column prop="evalYz" label="严重性 (S)" width="80" align="center">
-            <template #default="{ row }">
-              {{ row.evalYz }}
-            </template>
-          </el-table-column>
-          <el-table-column prop="evalFxz" label="风险值 (R)" width="80" align="center">
-            <template #default="{ row }">
-              {{ row.evalFxz }}
+  <el-row :gutter="20">
+    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
+    <el-col :span="isLeftContentCollapsed ? 24 : 20" :xs="24">
+      <ContentWrap style="border: 0">
+        <!-- 搜索工作栏 -->
+        <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true">
+          <el-form-item label="风险等级" prop="riskGrade">
+            <el-select
+              v-model="queryParams.riskGrade"
+              placeholder="请选择风险等级"
+              clearable
+              style="width: 200px"
+            >
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.DANGER_GRADE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item>
+            <el-button @click="handleQuery"
+              ><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button
+            >
+            <el-button @click="resetQuery"
+              ><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button
+            >
+            <el-button type="primary" @click="openForm('create')" color="#626aef">
+              <Icon icon="ep:plus" class="mr-5px" /> 新增
+            </el-button>
+            <el-button type="success" plain @click="handleExport" :loading="exportLoading">
+              <Icon icon="ep:download" class="mr-5px" /> 导出
+            </el-button>
+          </el-form-item>
+        </el-form>
+      </ContentWrap>
+
+      <ContentWrap style="border: 0">
+        <el-table
+          :data="tableData"
+          border
+          style="width: 100%"
+          :header-cell-style="{ background: '#f5f7fa', color: '#333' }"
+          :cell-style="{ padding: '12px 8px' }"
+          height="70vh"
+        >
+          <!-- 区域/位置 列(已合并) -->
+          <el-table-column
+            prop="region"
+            label="区域/位置"
+            width="150"
+            align="center"
+            fixed="left"
+          />
+
+          <!-- 其他列保持不变 -->
+          <el-table-column label="序号" width="70" align="center">
+            <template #default="scope">
+              {{ scope.$index + 1 }}
             </template>
           </el-table-column>
-          <el-table-column prop="riskGrade" label="风险等级" width="100" align="center">
-            <template #default="scope">
-              <div class="bg-[#ffff00] w-full rounded-md" v-if="scope.row.riskGrade === 'normal'">
+          <el-table-column
+            prop="elementDescription"
+            label="危害因素描述"
+            width="200"
+            align="center"
+          />
+          <el-table-column prop="maybeResult" min-width="320" label="可导致的后果" align="center" />
+
+          <!-- 风险评价列保持不变 -->
+          <el-table-column label="风险评价" width="320" align="center">
+            <el-table-column prop="evalKn" label="可能性 (L)" width="80" align="center">
+              <template #default="{ row }">
+                {{ row.evalKn }}
+              </template>
+            </el-table-column>
+            <el-table-column prop="evalYz" label="严重性 (S)" width="80" align="center">
+              <template #default="{ row }">
+                {{ row.evalYz }}
+              </template>
+            </el-table-column>
+            <el-table-column prop="evalFxz" label="风险值 (R)" width="80" align="center">
+              <template #default="{ row }">
+                {{ row.evalFxz }}
+              </template>
+            </el-table-column>
+            <el-table-column prop="riskGrade" label="风险等级" width="100" align="center">
+              <template #default="scope">
+                <!-- <div class="bg-[#ffff00] w-full rounded-md" v-if="scope.row.riskGrade === 'normal'">
                 一般风险
               </div>
               <div class="bg-[#ffc000] w-full rounded-md" v-else-if="scope.row.riskGrade === 'big'">
                 较大风险
               </div>
-              <div class="bg-[#0070c0] w-full text-white rounded-md" v-else>低风险</div>
+              <div class="bg-[#0070c0] w-full text-white rounded-md" v-else>低风险</div> -->
+                <dict-tag :type="DICT_TYPE.DANGER_GRADE" :value="scope.row.riskGrade" />
+              </template>
+            </el-table-column>
+          </el-table-column>
+
+          <el-table-column
+            prop="controlMethod"
+            label="控制措施"
+            min-width="200"
+            show-overflow-tooltip
+            align="center"
+          />
+          <el-table-column prop="charge" label="责任人 " min-width="100" align="center" />
+          <el-table-column label="操作" width="150" align="center" fixed="right">
+            <template #default="{ row }">
+              <div class="flex gap-3 justify-center">
+                <el-link
+                  :underline="false"
+                  size="small"
+                  type="primary"
+                  @click="openForm('edit', row)"
+                >
+                  编辑
+                </el-link>
+                <el-link :underline="false" size="small" type="danger" @click="deleteRow(row)">
+                  删除
+                </el-link>
+              </div>
             </template>
           </el-table-column>
-        </el-table-column>
-
-        <el-table-column
-          prop="controlMethod"
-          label="控制措施"
-          min-width="200"
-          show-overflow-tooltip
-          align="center"
-        />
-        <el-table-column prop="charge" label="责任人 " min-width="100" align="center" />
-        <el-table-column label="操作" width="150" align="center" fixed="right">
-          <template #default="{ row }">
-            <div class="flex gap-3 justify-center">
-              <el-link
-                :underline="false"
-                size="small"
-                type="primary"
-                @click="openForm('edit', row)"
-              >
-                编辑
-              </el-link>
-              <el-link :underline="false" size="small" type="danger" @click="deleteRow(row)">
-                删除
-              </el-link>
-            </div>
-          </template>
-        </el-table-column>
-      </el-table>
-
-      <!-- 分页 -->
-      <div class="mt-2 flex justify-right">
-        <el-pagination
-          v-model:current-page="pagination.pageNo"
-          v-model:page-size="pagination.pageSize"
-          :total="total"
-          layout="total, sizes, prev, pager, next, jumper"
-          @size-change="handleSizeChange"
-          @current-change="handleCurrentChange"
-          background
-        />
-      </div>
-    </ContentWrap>
-
-    <!-- 新增/编辑弹窗 -->
-    <!-- 新增/编辑弹窗 -->
-    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="50%" @close="resetForm">
-      <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
-        <el-row :gutter="20">
-          <!-- 第一行 -->
-          <el-col :span="12">
-            <el-form-item label="区域/位置" prop="region">
-              <el-input v-model="formData.region" placeholder="请输入区域/位置" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="危害因素描述" prop="elementDescription">
-              <el-input v-model="formData.elementDescription" placeholder="请输入危害因素描述" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-row :gutter="20">
-          <!-- 第二行 -->
-          <el-col :span="12">
-            <el-form-item label="可能导致的后果" prop="maybeResult">
-              <el-input
-                v-model="formData.maybeResult"
-                placeholder="请输入可能导致的后果"
-                type="textarea"
-                :rows="1"
-              />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="风险评价可能性" prop="evalKn">
-              <el-input-number
-                v-model="formData.evalKn"
-                controls-position="right"
-                style="width: 100%"
-              />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-row :gutter="20">
-          <!-- 第三行 -->
-          <el-col :span="12">
-            <el-form-item label="风险评价严重性" prop="evalYz">
-              <el-input-number
-                v-model="formData.evalYz"
-                controls-position="right"
-                style="width: 100%"
-              />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="风险评价风险值" prop="evalFxz">
-              <el-input-number v-model="formData.evalFxz" disabled style="width: 100%" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-row :gutter="20">
-          <!-- 第四行 -->
-          <el-col :span="12">
-            <el-form-item label="风险等级" prop="riskGrade">
-              <el-select
-                v-model="formData.riskGrade"
-                placeholder="请选择风险等级"
-                clearable
-                style="width: 100%"
-              >
-                <el-option
-                  v-for="dict in getStrDictOptions(DICT_TYPE.DANGER_GRADE)"
-                  :key="dict.value"
-                  :label="dict.label"
-                  :value="dict.value"
-                />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="责任人" prop="charge">
-              <el-input v-model="formData.charge" placeholder="请输入责任人" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-row :gutter="20">
-          <el-col :span="24">
-            <el-form-item label="备注" prop="remark">
-              <el-input
-                v-model="formData.remark"
-                type="textarea"
-                placeholder="请输入备注"
-                :rows="1"
-              />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <!-- 控制措施单独一行(占满) -->
-        <el-row :gutter="20">
-          <el-col :span="24">
-            <el-form-item label="控制措施" prop="controlMethod">
-              <el-input
-                v-model="formData.controlMethod"
-                type="textarea"
-                :rows="4"
-                placeholder="请输入控制措施"
+        </el-table>
+
+        <!-- 分页 -->
+        <div class="mt-2 flex justify-right">
+          <el-pagination
+            v-model:current-page="pagination.pageNo"
+            v-model:page-size="pagination.pageSize"
+            :total="total"
+            layout="total, sizes, prev, pager, next, jumper"
+            @size-change="handleSizeChange"
+            @current-change="handleCurrentChange"
+            background
+          />
+        </div>
+      </ContentWrap>
+    </el-col>
+  </el-row>
+  <!-- 新增/编辑弹窗 -->
+  <!-- 新增/编辑弹窗 -->
+  <el-dialog v-model="dialogVisible" :title="dialogTitle" width="50%" @close="resetForm">
+    <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
+      <el-row :gutter="20">
+        <!-- 第一行 -->
+        <el-col :span="12">
+          <el-form-item label="区域/位置" prop="region">
+            <el-input v-model="formData.region" placeholder="请输入区域/位置" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="危害因素描述" prop="elementDescription">
+            <el-input v-model="formData.elementDescription" placeholder="请输入危害因素描述" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <!-- 第二行 -->
+        <el-col :span="12">
+          <el-form-item label="可能导致的后果" prop="maybeResult">
+            <el-input
+              v-model="formData.maybeResult"
+              placeholder="请输入可能导致的后果"
+              type="textarea"
+              :rows="1"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="风险评价可能性" prop="evalKn">
+            <el-input-number
+              v-model="formData.evalKn"
+              controls-position="right"
+              style="width: 100%"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <!-- 第三行 -->
+        <el-col :span="12">
+          <el-form-item label="风险评价严重性" prop="evalYz">
+            <el-input-number
+              v-model="formData.evalYz"
+              controls-position="right"
+              style="width: 100%"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="风险评价风险值" prop="evalFxz">
+            <el-input-number v-model="formData.evalFxz" disabled style="width: 100%" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <!-- 第四行 -->
+        <el-col :span="12">
+          <el-form-item label="风险等级" prop="riskGrade">
+            <el-select
+              v-model="formData.riskGrade"
+              placeholder="请选择风险等级"
+              clearable
+              style="width: 100%"
+            >
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.DANGER_GRADE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
               />
-            </el-form-item>
-          </el-col>
-        </el-row>
-      </el-form>
-
-      <template #footer>
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="submitForm">确定</el-button>
-      </template>
-    </el-dialog>
-  </div>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="责任人" prop="charge">
+            <el-input v-model="formData.charge" placeholder="请输入责任人" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="备注" prop="remark">
+            <el-input
+              v-model="formData.remark"
+              type="textarea"
+              placeholder="请输入备注"
+              :rows="1"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <!-- 控制措施单独一行(占满) -->
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="控制措施" prop="controlMethod">
+            <el-input
+              v-model="formData.controlMethod"
+              type="textarea"
+              :rows="4"
+              placeholder="请输入控制措施"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="dialogVisible = false">取消</el-button>
+      <el-button type="primary" @click="submitForm">确定</el-button>
+    </template>
+  </el-dialog>
 </template>
 
 <script setup>
 import { ref, reactive, watch, onMounted } from 'vue'
 import { IotDangerApi } from '@/api/pms/qhse/index'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import DeptTree from '@/views/system/user/HazardTree.vue'
 
 // 查询参数
 const queryParams = reactive({
-  riskGrade: ''
+  riskGrade: '',
+  deptId: ''
 })
 
 // 表格数据
 const tableData = ref([])
-
+const isLeftContentCollapsed = ref(false)
 // 弹窗控制
 const dialogVisible = ref(false)
 const dialogTitle = ref('新增')
@@ -306,6 +321,12 @@ const handleQuery = () => {
   loadTableData()
 }
 
+const handleDeptNodeClick = async (row) => {
+  queryParams.deptId = row.id
+  pagination.pageNo = 1
+  loadTableData()
+}
+
 const downloadFile = (response) => {
   // 创建 blob 对象
   const blob = new Blob([response], {
@@ -493,7 +514,8 @@ const loadTableData = async () => {
     const params = {
       pageNo: pagination.pageNo,
       pageSize: pagination.pageSize,
-      riskGrade: queryParams.riskGrade // 添加搜索参数
+      riskGrade: queryParams.riskGrade, // 添加搜索参数
+      deptId: queryParams.deptId
     }
     const res = await IotDangerApi.getDangerList(params)
     tableData.value = res.list || []

+ 15 - 3
src/views/pms/qhse/index.vue

@@ -40,8 +40,9 @@
           v-loading="loading"
           :data="list"
           :stripe="true"
-          height="calc(85vh - 130px)"
+          height="calc(78vh - 145px)"
           :show-overflow-tooltip="true"
+          :row-style="tableRowStyle"
         >
           <el-table-column :label="t('monitor.serial')" width="70" align="center" fixed="left">
             <template #default="scope">
@@ -104,6 +105,13 @@
           @pagination="getList"
         />
       </ContentWrap>
+      <ContentWrap style="margin-top: -5px">
+        <el-alert title="台账已过期红色预警" type="error" show-icon :closable="false">
+          <template #icon>
+            <Bell />
+          </template>
+        </el-alert>
+      </ContentWrap>
     </el-col>
   </el-row>
 
@@ -288,8 +296,12 @@ const queryParams = reactive({
 })
 const queryFormRef = ref(null) // 搜索的表单
 
-const contentSpan = ref(20)
-const treeShow = ref(true)
+const tableRowStyle = ({ row }) => {
+  if (row.expired) {
+    return { backgroundColor: '#ffe6e6' }
+  }
+  return {}
+}
 
 // 对话框相关
 const dialogVisible = ref(false)

+ 161 - 0
src/views/pms/qhse/socData/SOCClassifyForm.vue

@@ -0,0 +1,161 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+    >
+      <el-form-item label="上级分类" prop="parentId">
+        <el-tree-select
+          v-model="formData.parentId"
+          :data="deptTree"
+          :props="defaultProps"
+          check-strictly
+          placeholder="请选择上级分类"
+          value-key="id"
+          :default-expanded-keys="firstLevelKeys"
+        />
+      </el-form-item>
+      <el-form-item label="分类名称" prop="name">
+        <lang-input v-model="formData.name" placeholder="请输入分类名称" />
+      </el-form-item>
+
+      <el-form-item label="显示排序" prop="sort">
+        <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status" v-if="formType === 'update'">
+        <el-select v-model="formData.status" clearable placeholder="请选择状态">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input
+          v-model="formData.remark"
+          maxlength="11"
+          placeholder="请输入备注"
+          type="textarea"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { defaultProps, handleTree } from '@/utils/tree'
+import { IotSocApi } from '@/api/pms/qhse'
+// import * as UserApi from '@/api/system/user'
+import { CommonStatusEnum } from '@/utils/constants'
+import { FormRules } from 'element-plus'
+
+defineOptions({ name: 'IotProductClassifyForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  code: undefined,
+  parentId: undefined,
+  name: undefined,
+  sort: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive<FormRules>({
+  parentId: [{ required: true, message: '上级分类不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '分类名称不能为空', trigger: 'blur' }],
+  sort: [{ required: true, message: '显示排序不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const deptTree = ref() // 树形结构
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number, parentId: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  formData.value.sort = 0
+  formData.value.parentId = parentId
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await IotSocApi.getSocClassify(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+  // 获得分类树
+  await getTree()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await IotSocApi.createSoc(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await IotSocApi.updateSoc(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    title: '',
+    parentId: undefined,
+    name: undefined,
+    sort: undefined,
+    leaderUserId: undefined,
+    phone: undefined,
+    email: undefined,
+    status: CommonStatusEnum.ENABLE
+  }
+  formRef.value?.resetFields()
+}
+
+const firstLevelKeys = ref([])
+/** 获得分类树 */
+const getTree = async () => {
+  deptTree.value = []
+  const data = await IotSocApi.getParentCategory()
+  let dept: Tree = { id: 0, name: '性质', children: [] }
+  dept.children = handleTree(data)
+  deptTree.value.push(dept)
+  firstLevelKeys.value = deptTree.value.map((node) => node.id)
+}
+</script>

+ 200 - 0
src/views/pms/qhse/socData/index.vue

@@ -0,0 +1,200 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="分类名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入分类名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="分类状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择分类状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['iot:product-classify:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button type="danger" plain @click="toggleExpandAll">
+          <Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      row-key="id"
+      :default-expand-all="isExpandAll"
+      v-if="refreshTable"
+      @row-click="handleClick"
+    >
+      <el-table-column prop="name" label="分类名称" width="280" :show-overflow-tooltip="true" />
+      <!--      <el-table-column prop="code" label="分类编码" />-->
+      <el-table-column prop="sort" label="排序" />
+      <el-table-column prop="status" label="状态">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column prop="remark" label="备注" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['iot:product-classify:update']"
+          >
+            修改
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['iot:product-classify:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ProductClassifyForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { handleTree } from '@/utils/tree'
+import { IotSocApi } from '@/api/pms/qhse/index'
+import ProductClassifyForm from './SOCClassifyForm.vue'
+
+defineOptions({ name: 'IotProductClassify' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref() // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 100,
+  name: undefined,
+  status: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+const isExpandAll = ref(true) // 是否展开,默认全部展开
+const refreshTable = ref(true) // 重新渲染表格状态
+const parentId = ref('')
+const handleClick = (node: {}) => {
+  parentId.value = node.id
+}
+/** 查询分类列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await IotSocApi.getSocList(queryParams)
+    list.value = handleTree(data)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 展开/折叠操作 */
+const toggleExpandAll = () => {
+  refreshTable.value = false
+  isExpandAll.value = !isExpandAll.value
+  nextTick(() => {
+    refreshTable.value = true
+  })
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryParams.pageNo = 1
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number, parent: number) => {
+  parent = parentId.value
+  formRef.value.open(type, id, parent)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await IotSocApi.deleteSoc(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  // 获取用户列表
+  //userList.value = await UserApi.getSimpleUserList()
+})
+</script>
+<style scoped>
+/* 全局样式或 scoped 穿透 */
+:deep(.el-table__body tr) {
+  cursor: pointer; /* 手型光标 */
+}
+
+:deep(.el-table__body tr:hover) {
+  cursor: pointer;
+}
+</style>

+ 171 - 0
src/views/pms/qhse/socSummary/IotSocSummaryForm.vue

@@ -0,0 +1,171 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="队伍" prop="deptId">
+        <el-tree-select
+          clearable
+          v-model="formData.deptId"
+          :data="deptList2"
+          :props="defaultProps"
+          :check-strictly="false"
+          node-key="id"
+          filterable
+          placeholder="请选择所在队伍"
+          @node-click="handleNodeClick"
+        />
+      </el-form-item>
+
+      <el-form-item label="观察日期" prop="observationDate">
+        <el-date-picker
+          v-model="formData.observationDate"
+          type="date"
+          value-format="x"
+          placeholder="选择观察日期"
+          style="width: 100%"
+        />
+      </el-form-item>
+      <el-form-item label="soc类型" prop="socClass">
+        <el-tree-select
+          clearable
+          v-model="formData.socClass"
+          :data="socList"
+          :props="defaultProps"
+          check-strictly
+          node-key="id"
+          filterable
+          placeholder="请选择soc类型"
+        />
+      </el-form-item>
+
+      <el-form-item label="姓名" prop="userName">
+        <el-input v-model="formData.userName" placeholder="请输入姓名" />
+      </el-form-item>
+      <el-form-item label="岗位" prop="post">
+        <el-input v-model="formData.post" placeholder="请输入岗位" />
+      </el-form-item>
+
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" placeholder="请输入备注" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { IotSocSummaryApi } from '@/api/pms/qhse/index'
+import { IotSocApi } from '@/api/pms/qhse/index'
+import { defaultProps } from '@/utils/tree'
+import { handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+/** SOC卡汇总 表单 */
+defineOptions({ name: 'IotSocSummaryForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const deptList2 = ref<Tree[]>([]) // 树形结构
+const socList = ref<Tree[]>([]) // soc类型列表
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  project: undefined,
+  observationDate: undefined,
+  socClass: undefined,
+  userName: undefined,
+  post: undefined,
+  deptId: undefined,
+  remark: undefined
+})
+const formRules = reactive({
+  observationDate: [{ required: true, message: '观察日期不能为空', trigger: 'blur' }],
+  socClass: [{ required: true, message: 'soc类型不能为空', trigger: 'blur' }],
+  className: [{ required: true, message: '类型名称不能为空', trigger: 'blur' }],
+  deptId: [{ required: true, message: '队伍不能为空', trigger: 'change' }],
+  userName: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
+  post: [{ required: true, message: '岗位不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await IotSocSummaryApi.getIotSocSummary(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await IotSocSummaryApi.createIotSocSummary(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await IotSocSummaryApi.updateIotSocSummary(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+const handleNodeClick = (data: Tree) => {
+  if (data.type !== '3') {
+    ElMessage.warning('只能选择队伍')
+    return
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    project: undefined,
+    observationDate: undefined,
+    socClass: undefined,
+    className: undefined,
+    userName: undefined,
+    post: undefined,
+    deptId: undefined,
+    remark: undefined,
+    deptName: undefined
+  }
+  formRef.value?.resetFields()
+}
+
+onMounted(async () => {
+  deptList2.value = handleTree(await DeptApi.getSimpleDeptList())
+
+  const data = await IotSocApi.getSocList({})
+  socList.value = handleTree(data)
+})
+</script>

+ 233 - 0
src/views/pms/qhse/socSummary/index.vue

@@ -0,0 +1,233 @@
+<template>
+  <el-row :gutter="20">
+    <!-- 左侧部门树 -->
+    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
+
+    <el-col :span="isLeftContentCollapsed ? 24 : 20" :xs="24">
+      <ContentWrap>
+        <!-- 搜索工作栏 -->
+        <el-form
+          class="-mb-15px"
+          :model="queryParams"
+          ref="queryFormRef"
+          :inline="true"
+          label-width="68px"
+        >
+          <el-form-item label="姓名" prop="userName">
+            <el-input
+              v-model="queryParams.userName"
+              placeholder="请输入姓名"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-240px"
+            />
+          </el-form-item>
+
+          <el-form-item label="队伍名称" prop="deptName">
+            <el-input
+              v-model="queryParams.deptName"
+              placeholder="请输入队伍名称"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button @click="handleQuery"
+              ><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button
+            >
+            <el-button @click="resetQuery"
+              ><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button
+            >
+            <el-button
+              type="primary"
+              plain
+              @click="openForm('create')"
+              v-hasPermi="['rq:iot-soc-summary:create']"
+            >
+              <Icon icon="ep:plus" class="mr-5px" /> 新增
+            </el-button>
+            <el-button
+              type="success"
+              plain
+              @click="handleExport"
+              :loading="exportLoading"
+              v-hasPermi="['rq:iot-soc-summary:export']"
+            >
+              <Icon icon="ep:download" class="mr-5px" /> 导出
+            </el-button>
+          </el-form-item>
+        </el-form>
+      </ContentWrap>
+
+      <!-- 列表 -->
+      <ContentWrap>
+        <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+          <el-table-column :label="t('monitor.serial')" width="70" align="center">
+            <template #default="scope">
+              {{ scope.$index + 1 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="项目部" align="center" prop="projectName" width="120" />
+          <!-- <el-table-column label="部门名称" align="center" prop="deptName" /> -->
+          <el-table-column label="队伍名称" align="center" prop="deptName" />
+          <el-table-column
+            label="观察日期"
+            align="center"
+            prop="observationDate"
+            :formatter="dateFormatter"
+            width="180px"
+          />
+
+          <el-table-column label="类型名称" align="center" prop="className" />
+          <el-table-column label="姓名" align="center" prop="userName" />
+          <el-table-column label="岗位" align="center" prop="post" />
+
+          <el-table-column label="备注" align="center" prop="remark" />
+          <el-table-column
+            label="创建时间"
+            align="center"
+            prop="createTime"
+            :formatter="dateFormatter"
+            width="180px"
+          />
+
+          <el-table-column label="操作" align="center" min-width="120px">
+            <template #default="scope">
+              <el-button
+                link
+                type="primary"
+                @click="openForm('update', scope.row.id)"
+                v-hasPermi="['rq:iot-soc-summary:update']"
+              >
+                编辑
+              </el-button>
+              <el-button
+                link
+                type="danger"
+                @click="handleDelete(scope.row.id)"
+                v-hasPermi="['rq:iot-soc-summary:delete']"
+              >
+                删除
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <!-- 分页 -->
+        <Pagination
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </ContentWrap>
+    </el-col>
+  </el-row>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <IotSocSummaryForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { IotSocSummaryApi } from '@/api/pms/qhse/index'
+import IotSocSummaryForm from './IotSocSummaryForm.vue'
+import DeptTree from '@/views/system/user/DeptTree2.vue'
+
+/** SOC卡汇总 列表 */
+defineOptions({ name: 'IotSocSummary' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const isLeftContentCollapsed = ref(false)
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  project: undefined,
+  observationDate: [],
+  socClass: undefined,
+  className: undefined,
+  userName: undefined,
+  post: undefined,
+  deptId: undefined,
+  remark: undefined,
+  createTime: [],
+  deptName: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await IotSocSummaryApi.getIotSocSummaryPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleDeptNodeClick = async (row) => {
+  queryParams.deptId = row.id
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await IotSocSummaryApi.deleteIotSocSummary(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await IotSocSummaryApi.exportIotSocSummary(queryParams)
+    download.excel(data, 'SOC卡汇总.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 301 - 0
src/views/system/user/HazardTree.vue

@@ -0,0 +1,301 @@
+<template>
+  <el-col
+    :class="{ leftcontent: true, collapsed: isCollapsed }"
+    :span="isCollapsed ? 0 : 4"
+    :xs="24"
+  >
+    <ContentWrap class="h-[85vh]">
+      <div
+        class="dept-tree"
+        :class="{ 'is-collapsed': isCollapsed }"
+        style="overflow-y: auto; overflow-x: auto"
+      >
+        <div class="head-container" style="display: flex; flex-direction: row">
+          <el-input
+            v-model="deptName"
+            class="mb-18px"
+            style="height: 35px"
+            clearable
+            placeholder="请输入部门名称"
+          >
+            <template #prefix>
+              <Icon icon="ep:search" />
+            </template>
+          </el-input>
+        </div>
+        <div class="tree-container">
+          <el-tree
+            v-show="!isCollapsed"
+            ref="treeRef"
+            :data="deptList"
+            :expand-on-click-node="false"
+            :filter-node-method="filterNode"
+            :props="defaultProps"
+            :default-expanded-keys="firstLevelKeys"
+            highlight-current
+            node-key="id"
+            @node-click="handleNodeClick"
+            @node-contextmenu="handleRightClick"
+          />
+        </div>
+      </div>
+      <div
+        v-show="menuVisible"
+        class="custom-menu"
+        :style="{ left: menuX + 'px', top: menuY + 'px' }"
+      >
+        <ul>
+          <li @click="handleMenuClick('add')">新增子节点</li>
+          <li @click="handleMenuClick('edit')">重命名</li>
+          <li @click="handleMenuClick('delete')">删除</li>
+        </ul>
+      </div>
+    </ContentWrap>
+  </el-col>
+
+  <!-- 切换按钮移到外部,始终可见 -->
+  <button
+    :class="isCollapsed ? 'tree-toggle--outside' : 'tree-toggle--outside2'"
+    type="button"
+    :aria-label="isCollapsed ? '展开组织树' : '收起组织树'"
+    @click="toggleCollapsed"
+    :title="isCollapsed ? '展开' : '收起'"
+  >
+    <img class="tree-toggle__img" :src="isCollapsed ? hideimage : showimage" alt="" />
+  </button>
+</template>
+
+<script lang="ts" setup>
+import { ElTree } from 'element-plus'
+import * as DeptApi from '@/api/system/dept'
+import { defaultProps, handleTree } from '@/utils/tree'
+import { useTreeStore } from '@/store/modules/usersTreeStore'
+import hideimage from '@/assets/imgs/leftTree-hide.png'
+import showimage from '@/assets/imgs/leftTree-show.png'
+
+defineOptions({ name: 'SystemUserDeptTree' })
+
+type Props = {
+  collapsed?: boolean
+  collapsible?: boolean
+}
+
+const props = defineProps<Props>()
+const emits = defineEmits<{
+  (e: 'node-click', row: any): void
+  (e: 'update:collapsed', value: boolean): void
+  (e: 'toggle', value: boolean): void
+}>()
+
+const collapsible = computed(() => props.collapsible !== false)
+const collapsedLocal = ref(false)
+const isCollapsed = computed(() => (props.collapsed ?? collapsedLocal.value) === true)
+
+const deptName = ref('')
+const deptList = ref<Tree[]>([]) // 树形结构
+const treeRef = ref<InstanceType<typeof ElTree>>()
+const menuVisible = ref(false)
+const menuX = ref(0)
+const menuY = ref(0)
+const firstLevelKeys = ref([])
+let selectedNode = null
+const treeStore = useTreeStore()
+
+watch(
+  () => props.collapsed,
+  (val) => {
+    if (typeof val === 'boolean') collapsedLocal.value = val
+  },
+  { immediate: true }
+)
+
+const toggleCollapsed = () => {
+  const next = !isCollapsed.value
+  collapsedLocal.value = next
+  emits('update:collapsed', next)
+  emits('toggle', next)
+}
+
+const handleRightClick = (event, { node, data }) => {
+  event.preventDefault()
+  menuX.value = event.clientX
+  menuY.value = event.clientY
+  selectedNode = data // 存储当前操作的节点数据 ‌:ml-citation{ref="7" data="citationList"}
+  //menuVisible.value = true;
+}
+const treeContainer = ref(null)
+const setHeight = () => {
+  if (!treeContainer.value) return
+  const windowHeight = window.innerHeight
+
+  treeContainer.value.style.height = `${windowHeight * 0.78}px` // 60px 底部预留
+}
+const handleMenuClick = (action) => {
+  switch (action) {
+    case 'add':
+      // 调用新增节点逻辑 ‌:ml-citation{ref="4" data="citationList"}
+      break
+    case 'edit':
+      // 调用编辑节点逻辑 ‌:ml-citation{ref="7" data="citationList"}
+      break
+    case 'delete':
+      // 调用删除节点逻辑 ‌:ml-citation{ref="4" data="citationList"}
+      break
+  }
+  menuVisible.value = false
+}
+/** 获得部门树 */
+const getTree = async () => {
+  const res = await DeptApi.getSimpleDeptList()
+  const resData = res.filter((item) => item.name === '科瑞石油技术' || item.type === '1')
+  deptList.value = []
+  deptList.value.push(...handleTree(resData))
+  firstLevelKeys.value = deptList.value.map((node) => node.id)
+}
+
+/** 基于名字过滤 */
+const filterNode = (name: string, data: Tree) => {
+  if (!name) return true
+  return data.name.includes(name)
+}
+
+/** 处理部门被点击 */
+const handleNodeClick = async (row: { [key: string]: any }) => {
+  emits('node-click', row)
+  treeStore.setSelectedId(row.id)
+}
+
+/** 监听deptName */
+watch(deptName, (val) => {
+  treeRef.value!.filter(val)
+})
+
+/** 初始化 */
+onMounted(async () => {
+  await getTree()
+  // setHeight()
+  // window.addEventListener('resize', setHeight)
+})
+onUnmounted(() => {
+  window.removeEventListener('resize', setHeight)
+})
+</script>
+<style lang="scss" scoped>
+.dept-tree {
+  height: 85vh;
+  overflow-y: auto;
+
+  &.is-collapsed {
+    overflow-y: hidden;
+  }
+}
+.custom-menu {
+  position: fixed;
+  background: white;
+  border: 1px solid #ccc;
+  box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
+  z-index: 1000;
+}
+.custom-menu ul {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+.custom-menu li {
+  padding: 8px 20px;
+  cursor: pointer;
+}
+.custom-menu li:hover {
+  background: #f5f5f5;
+}
+.tree-container {
+  overflow-y: auto;
+  max-height: calc(85vh - 100px);
+  min-width: 100%;
+  // border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  position: relative;
+  overflow-x: visible;
+}
+
+// 外部按钮样式 - 定位到左侧边缘
+.tree-toggle--outside {
+  position: absolute;
+  top: 30vh;
+  left: 0;
+  transform: translate(-50%, 0%);
+  z-index: 10;
+  background: transparent;
+  border: none;
+  cursor: pointer;
+  padding: 0;
+  line-height: 0;
+  transition: color 0.3s ease;
+}
+
+.tree-toggle--outside2 {
+  position: absolute;
+  top: 38%;
+  left: 16.5%;
+  transform: translate(-50%, 0%);
+  z-index: 10;
+  background: transparent;
+  border: none;
+  cursor: pointer;
+  padding: 0;
+  line-height: 0;
+  transition: color 0.3s ease;
+}
+
+.tree-toggle--outside:hover {
+  filter: brightness(0.9);
+  transition: color 0.3s ease;
+}
+
+.tree-toggle--outside2:hover {
+  filter: brightness(0.9);
+  transition: color 0.3s ease;
+}
+
+.tree-toggle__img {
+  display: block;
+  width: auto;
+  height: auto;
+  max-width: 100%;
+  max-height: 100%;
+  user-select: none;
+}
+
+.dept-tree.is-collapsed .tree-container {
+  overflow-y: auto;
+}
+
+.leftcontent {
+  transition: width 0.3s ease;
+  position: relative;
+}
+
+.leftcontent.collapsed {
+  width: 0 !important;
+  overflow: hidden;
+}
+
+// 小屏幕下隐藏按钮
+@media (max-width: 768px) {
+  .tree-toggle--outside,
+  .tree-toggle--outside2 {
+    display: none;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 5px; /* 设置滚动条宽度 */
+}
+::-webkit-scrollbar-thumb {
+  background-color: darkgrey; /* 设置滚动条颜色 */
+  border-radius: 10px; /* 设置圆角 */
+}
+::-webkit-scrollbar-track {
+  background: transparent; /* 设置轨道颜色 */
+}
+</style>