Преглед изворни кода

tab页面添加bom维护功能

lipenghui пре 2 месеци
родитељ
комит
dea6de2943
30 измењених фајлова са 715 додато и 22 уклоњено
  1. 4 0
      src/api/pms/devicebom/index.ts
  2. 6 6
      src/router/modules/remaining.ts
  3. 9 7
      src/views/pms/device/DeviceInfo.vue
  4. 3 3
      src/views/pms/device/IotDeviceForm.vue
  5. 130 0
      src/views/pms/device/MaterialListDrawerDevice.vue
  6. 0 0
      src/views/pms/device/allotlog/AllotLogList.vue
  7. 0 0
      src/views/pms/device/allotlog/ConfigDeviceAllot.vue
  8. 1 1
      src/views/pms/device/allotlog/DeviceAllot.vue
  9. 0 0
      src/views/pms/device/allotlog/DeviceAllotLogDrawer.vue
  10. 0 0
      src/views/pms/device/base/BrandList.vue
  11. 0 0
      src/views/pms/device/base/CustomerList.vue
  12. 0 0
      src/views/pms/device/base/ModelList.vue
  13. 213 0
      src/views/pms/device/bom/BomFormDevice.vue
  14. 1 1
      src/views/pms/device/bom/BomInfo.vue
  15. 344 0
      src/views/pms/device/bom/BomList.vue
  16. 0 0
      src/views/pms/device/bom/BomTree.vue
  17. 0 0
      src/views/pms/device/maintenance/MaintenanceDetail.vue
  18. 0 0
      src/views/pms/device/maintenance/MaintenanceList.vue
  19. 0 0
      src/views/pms/device/personlog/ConfigDevicePerson.vue
  20. 1 1
      src/views/pms/device/personlog/DevicePerson.vue
  21. 0 0
      src/views/pms/device/personlog/DevicePersonLogDrawer.vue
  22. 0 0
      src/views/pms/device/personlog/PersonList.vue
  23. 0 0
      src/views/pms/device/record/RecordInfo.vue
  24. 0 0
      src/views/pms/device/record/RecordList.vue
  25. 0 0
      src/views/pms/device/statuslog/ConfigDeviceStatus.vue
  26. 1 1
      src/views/pms/device/statuslog/DeviceStatus.vue
  27. 0 0
      src/views/pms/device/statuslog/DeviceStatusLogDrawer.vue
  28. 0 0
      src/views/pms/device/statuslog/DeviceStatusLogList.vue
  29. 1 1
      src/views/pms/maintain/IotMaintain.vue
  30. 1 1
      src/views/pms/model/IotModelForm.vue

+ 4 - 0
src/api/pms/devicebom/index.ts

@@ -25,6 +25,10 @@ export const IotDeviceBomApi = {
     return await request.get({ url: `/pms/iot-device-bom/page`, params })
   },
 
+  getIotDeviceBomSimple: async (params: any) => {
+    return await request.get({ url: `/pms/iot-device-bom/simple-list`, params })
+  },
+
   // 查询PMS 设备BOM 关系详情
   getIotDeviceBom: async (id: number) => {
     return await request.get({ url: `/pms/iot-device-bom/get?id=` + id })

+ 6 - 6
src/router/modules/remaining.ts

@@ -136,7 +136,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: 'template/detail/:id/:deviceid/:status/:deptid/:createtime',
-        component: () => import('@/views/pms/device/RecordInfo.vue'),
+        component: () => import('@/views/pms/device/record/RecordInfo.vue'),
         name: 'FillOrderInfoDevice',
         meta: {
           title: '详情',
@@ -232,7 +232,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },{
         path: 'device/bom/:id',
-        component: () => import('@/views/pms/device/BomInfo.vue'),
+        component: () => import('@/views/pms/device/bom/BomInfo.vue'),
         name: 'DeviceBom',
         meta: {
           noCache: false,
@@ -245,7 +245,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: 'device/person',
-        component: () => import('@/views/pms/device/ConfigDevicePerson.vue'),
+        component: () => import('@/views/pms/device/personlog/ConfigDevicePerson.vue'),
         name: 'ConfigDevicePerson',
         meta: {
           noCache: false,
@@ -258,7 +258,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: 'device/status',
-        component: () => import('@/views/pms/device/ConfigDeviceStatus.vue'),
+        component: () => import('@/views/pms/device/statuslog/ConfigDeviceStatus.vue'),
         name: 'ConfigDeviceStatus',
         meta: {
           noCache: false,
@@ -271,7 +271,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: 'device/allot',
-        component: () => import('@/views/pms/device/ConfigDeviceAllot.vue'),
+        component: () => import('@/views/pms/device/allotlog/ConfigDeviceAllot.vue'),
         name: 'ConfigDeviceAllot',
         meta: {
           noCache: false,
@@ -510,7 +510,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: 'mainworkorder/device/detail/:orderId/:deviceId(\\d+)',
-        component: () => import('@/views/pms/device/MaintenanceDetail.vue'),
+        component: () => import('@/views/pms/device/maintenance/MaintenanceDetail.vue'),
         name: 'IotDeviceMainWorkOrderDetail',
         meta: {
           noCache: false,

+ 9 - 7
src/views/pms/device/DeviceInfo.vue

@@ -147,7 +147,8 @@
         <DeviceUpload ref="fileRef" v-model:activeName="activeName" />
       </el-tab-pane>
       <el-tab-pane label="设备BOM" name="bom">
-        <BomInfo ref="bomRef" v-model:activeName="activeName" />
+<!--        <BomInfo ref="bomRef" v-model:activeName="activeName" />-->
+        <BomList ref="bomRef" v-model:activeName="activeName" :deviceId="id" />
       </el-tab-pane>
       <el-tab-pane label="运行记录" name="record">
         <RecordList ref="recordRef" v-model:activeName="activeName" :deviceId="id" />
@@ -201,15 +202,16 @@ import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
 import { DICT_TYPE, getDictLabel } from '@/utils/dict'
 import { formatDate } from '../../../utils/formatTime'
 import DeviceUpload from '@/views/pms/device/DeviceUpload.vue'
-import BomInfo from '@/views/pms/device/BomInfo.vue'
+import BomInfo from '@/views/pms/device/bom/BomInfo.vue'
+import BomList from '@/views/pms/device/bom/BomList.vue'
 import FailureList from '@/views/pms/device/FailureList.vue'
 import MaintainList from '@/views/pms/device/MaintainList.vue'
 import InspectList from '@/views/pms/device/InspectList.vue'
-import MaintenanceList from '@/views/pms/device/MaintenanceList.vue'
-import AllotLogList from '@/views/pms/device/AllotLogList.vue'
-import DeviceStatusLogList from "@/views/pms/device/DeviceStatusLogList.vue";
-import PersonList from '@/views/pms/device/PersonList.vue'
-import RecordList from '@/views/pms/device/RecordList.vue'
+import MaintenanceList from '@/views/pms/device/maintenance/MaintenanceList.vue'
+import AllotLogList from '@/views/pms/device/allotlog/AllotLogList.vue'
+import DeviceStatusLogList from "@/views/pms/device/statuslog/DeviceStatusLogList.vue";
+import PersonList from '@/views/pms/device/personlog/PersonList.vue'
+import RecordList from '@/views/pms/device/record/RecordList.vue'
 import { createImageViewer } from '@/components/ImageViewer'
 
 const defaultPicUrl = ref(

+ 3 - 3
src/views/pms/device/IotDeviceForm.vue

@@ -348,9 +348,9 @@
 </template>
 <script setup lang="ts">
 import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
-import BrandList from '@/views/pms/device/BrandList.vue'
-import ModelList from '@/views/pms/device/ModelList.vue'
-import CustomerList from '@/views/pms/device/CustomerList.vue'
+import BrandList from '@/views/pms/device/base/BrandList.vue'
+import ModelList from '@/views/pms/device/base/ModelList.vue'
+import CustomerList from '@/views/pms/device/base/CustomerList.vue'
 import { defaultProps, handleTree } from '@/utils/tree'
 import * as DeptApi from '@/api/system/dept'
 import * as ProductClassifyApi from '@/api/pms/productclassify'

+ 130 - 0
src/views/pms/device/MaterialListDrawerDevice.vue

@@ -0,0 +1,130 @@
+<template>
+  <el-drawer
+    title="物料详情"
+    :append-to-body="true"
+    :model-value="modelValue"
+    @update:model-value="$emit('update:modelValue', $event)"
+    :show-close="false"
+    direction="rtl"
+    :size="computedSize"
+    :before-close="handleClose"
+  >
+    <template v-if="nodeId">
+      <div v-loading="loading" style="height: 100%">
+        <el-table :data="materials" style="width: 100%">
+          <el-table-column prop="name" label="物料名称" width="180" />
+          <el-table-column prop="code" label="物料编码" width="180" />
+          <el-table-column prop="model" label="规格型号" width="180" />
+          <el-table-column prop="unit" label="单位" width="180" />
+          <el-table-column
+            label="创建时间"
+            align="center"
+            prop="createTime"
+            width="180"
+            :formatter="dateFormatter"
+          />
+          <el-table-column label="操作" align="right">
+            <template #default="scope">
+              <el-button size="small" type="danger" @click="emit('delete', scope.row)"
+                >删除</el-button
+              >
+            </template>
+          </el-table-column>
+        </el-table>
+        <!-- 分页 -->
+        <Pagination
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="loadMaterials(props.nodeId)"
+        />
+      </div>
+    </template>
+  </el-drawer>
+</template>
+<script setup lang="ts">
+import { defineEmits, defineOptions, ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import * as DeviceMaterialApi from '@/api/pms/iotdevicematerial'
+import { dateFormatter } from '@/utils/formatTime'
+
+const drawerVisible = ref<boolean>(false)
+const emit = defineEmits(['update:modelValue', 'add', 'delete'])
+
+defineOptions({
+  name: 'MaterialListDrawer'
+})
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  createTime: [],
+  bomNodeId: '',
+  name: '',
+  code: ''
+})
+
+const windowWidth = ref(window.innerWidth)
+// 动态计算百分比
+const computedSize = computed(() => {
+  return windowWidth.value > 1200 ? '60%' : '80%'
+})
+
+const loading = ref(false)
+const total = ref(0) // 列表的总页数
+const materials = ref([])
+
+const props = defineProps({
+  modelValue: Boolean,
+  nodeId: Number,
+  deviceId: Number,
+})
+
+// 监听bom树节点ID变化
+watch(
+  () => props.nodeId,
+  async (newVal) => {
+    if (newVal) {
+      await loadMaterials(newVal)
+    }
+  }
+)
+
+// 加载指定bom节点下的物料数据
+const loadMaterials = async (nodeId) => {
+  queryParams.bomNodeId = nodeId
+  queryParams.deviceId = props.deviceId
+  queryParams.pageNo = 1
+  try {
+    loading.value = true
+    // API调用
+    const data = await DeviceMaterialApi.IotDeviceMaterialApi.getIotDeviceMaterialPage(queryParams)
+    materials.value = data.list
+    total.value = data.total
+  } catch (error) {
+    ElMessage.error('数据加载失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 打开抽屉
+const openDrawer = () => {
+  drawerVisible.value = true
+}
+
+// 关闭抽屉
+const closeDrawer = () => {
+  drawerVisible.value = false
+}
+
+// 关闭抽屉
+const handleClose = () => {
+  emit('update:modelValue', false)
+  materials.value = []
+}
+
+defineExpose({ openDrawer, closeDrawer, loadMaterials }) // 暴露方法给父组件
+</script>
+
+<style lang="scss" scoped></style>

+ 0 - 0
src/views/pms/device/AllotLogList.vue → src/views/pms/device/allotlog/AllotLogList.vue


+ 0 - 0
src/views/pms/device/ConfigDeviceAllot.vue → src/views/pms/device/allotlog/ConfigDeviceAllot.vue


+ 1 - 1
src/views/pms/device/DeviceAllot.vue → src/views/pms/device/allotlog/DeviceAllot.vue

@@ -193,7 +193,7 @@ import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import DeptTree from '@/views/system/user/DeptTree.vue'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import DeviceAllotLogDrawer from "@/views/pms/device/DeviceAllotLogDrawer.vue";
+import DeviceAllotLogDrawer from "@/views/pms/device/allotlog/DeviceAllotLogDrawer.vue";
 
 /** 设备调拨 列表 */
 defineOptions({ name: 'IotDeviceAllot' })

+ 0 - 0
src/views/pms/device/DeviceAllotLogDrawer.vue → src/views/pms/device/allotlog/DeviceAllotLogDrawer.vue


+ 0 - 0
src/views/pms/device/BrandList.vue → src/views/pms/device/base/BrandList.vue


+ 0 - 0
src/views/pms/device/CustomerList.vue → src/views/pms/device/base/CustomerList.vue


+ 0 - 0
src/views/pms/device/ModelList.vue → src/views/pms/device/base/ModelList.vue


+ 213 - 0
src/views/pms/device/bom/BomFormDevice.vue

@@ -0,0 +1,213 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+    >
+      <el-form-item label="上级BOM" prop="parentId">
+        <el-tree-select
+          v-model="formData.parentId"
+          :data="bomTree"
+          :props="defaultProps"
+          check-strictly
+          default-expand-all
+          placeholder="请选择上级BOM"
+          value-key="bomId"
+        />
+      </el-form-item>
+      <el-form-item label="BOM节点名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入BOM节点名称" />
+      </el-form-item>
+      <el-form-item label="BOM节点属性" prop="type">
+        <el-checkbox-group v-model="formData.type" class="w-80">
+          <el-checkbox
+            v-for="dict in getIntDictOptions(DICT_TYPE.PMS_BOM_NODE_EXT_ATTR)"
+            :key="dict.value"
+            :value="dict.value"
+          >
+            {{ dict.label }}
+          </el-checkbox>
+        </el-checkbox-group>
+      </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">
+        <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>
+    <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 * as BomApi from '@/api/pms/devicebom'
+import { CommonStatusEnum } from '@/utils/constants'
+import { useTreeStore } from '@/store/modules/treeStore'
+import * as DeviceCategoryApi from '@/api/pms/productclassify'
+import { FormRules } from 'element-plus'
+import { defineEmits, ref } from 'vue'
+
+defineOptions({ name: 'BomFormDevice' })
+const props = defineProps({
+  deviceId: Number
+})
+// 从store中共享设备分类id
+const treeStore = useTreeStore()
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const localCategoryId = ref(null) // 通过store存储的设备分类id 由父组件传递过来
+const selfDeviceCategoryId = ref(null) // 当前页面定义的 设备分类id
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  deviceCategoryId: localCategoryId.value,
+  deviceId: props.deviceId,
+  title: '',
+  parentId: undefined,
+  name: undefined,
+  sort: 0,
+  type: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive<FormRules>({
+  // deviceCategoryId: [{ required: true, message: '设备分类不能为空', trigger: 'blur' }],
+  parentId: [{ required: true, message: '上级Bom不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: 'Bom节点名称不能为空', trigger: 'blur' }],
+  sort: [{ required: true, message: '显示排序不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const bomTree = ref() // BOM 树形结构
+const deviceCategoryTree = ref() // 设备分类树
+const firstLevelKeys = ref([])
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  status: undefined,
+  deviceId: undefined,
+  deviceCategoryId: localCategoryId.value
+})
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  // 获取store中的设备分类id
+
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 获得 设备分类树
+  // await getDeviceCategoryTree()
+  // 获得 bom 树
+  await getTree()
+  console.log('open方法执行了!')
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await BomApi.IotDeviceBomApi.getIotDeviceBom(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+const emit = defineEmits(['success', 'node-click']) // 定义 success 树点击 事件,用于操作成功后的回调
+
+/** 处理 设备分类 树 被点击 */
+const handleDeviceCategoryTreeNodeClick = async (row: { [key: string]: any }) => {
+  emit('node-click', row)
+  // 清空设备分类bom树的选择,重新查询筛选bom树
+  selfDeviceCategoryId.value = row.id
+  await getTree()
+}
+
+/** 提交表单 */
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef.value) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    // 赋值设备分类id
+    //formData.value.deviceCategoryId = selfDeviceCategoryId.value
+    const data = formData.value as unknown as BomApi.IotDeviceBomVO
+    if (formType.value === 'create') {
+      data.deviceId = props.deviceId
+      await BomApi.IotDeviceBomApi.createIotDeviceBom(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await BomApi.IotDeviceBomApi.updateIotDeviceBom(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    deviceCategoryId: localCategoryId.value,
+    title: '',
+    parentId: undefined,
+    name: undefined,
+    sort: 0,
+    type: undefined,
+    status: CommonStatusEnum.ENABLE
+  }
+  formRef.value?.resetFields()
+}
+
+/** 获得 设备分类 树 **/
+// const getDeviceCategoryTree = async () => {
+//   deviceCategoryTree.value = []
+//   const res = await DeviceCategoryApi.IotProductClassifyApi.getSimpleProductClassifyList()
+//   let device: Tree = { id: 0, name: '顶级设备分类', children: [] }
+//   device.children = handleTree(res)
+//   deviceCategoryTree.value.push(device)
+// }
+
+/** 获得 Bom 树 */
+const getTree = async () => {
+  bomTree.value = []
+  queryParams.deviceId = props.deviceId
+  const data = await BomApi.IotDeviceBomApi.getIotDeviceBomSimple(queryParams)
+  let bom: Tree = { id: 0, name: '顶级Bom', children: [] }
+  bom.children = handleTree(data)
+  bomTree.value.push(bom)
+  firstLevelKeys.value = bomTree.value.map((node) => node.id)
+}
+
+/** 初始化 **/
+onMounted(() => {
+  formData.value.sort = 0
+})
+</script>

+ 1 - 1
src/views/pms/device/BomInfo.vue → src/views/pms/device/bom/BomInfo.vue

@@ -72,7 +72,7 @@ import { IotDeviceVO } from '@/api/pms/device'
 import { dateFormatter } from '@/utils/formatTime'
 import IotInfoForm from '@/views/pms/iotinfo/IotInfoForm.vue'
 import { onMounted, ref } from 'vue'
-import BomTree from '@/views/pms/device/BomTree.vue'
+import BomTree from '@/views/pms/device/bom/BomTree.vue'
 import * as PmsMaterialApi from '@/api/pms/material'
 
 defineOptions({ name: 'DeviceUpload' })

+ 344 - 0
src/views/pms/device/bom/BomList.vue

@@ -0,0 +1,344 @@
+<template>
+  <doc-alert title="用户体系" url="https://doc.iocoder.cn/user-center/" />
+  <doc-alert title="三方登陆" url="https://doc.iocoder.cn/social-user/" />
+  <doc-alert title="Excel 导入导出" url="https://doc.iocoder.cn/excel-import-and-export/" />
+
+  <!-- 搜索 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="110px"
+    >
+      <el-form-item label="BOM节点名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入BOM节点名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="BOM节点" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择BOM节点"
+          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', null)"
+          v-hasPermi="['rq:iot-bom: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-button @click="handleAllQuery"><Icon icon="ep:search" 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"
+      style="width: 100%"
+    >
+      <el-table-column prop="name" label="BOM节点">
+        <template #default="scope">
+          <!-- 使用 el-tooltip 包裹内容 -->
+          <el-tooltip
+            effect="dark"
+            :content="`设备分类:${scope.row.deviceCategoryName || '暂无'}`"
+            placement="top-start"
+            :disabled="!scope.row.deviceCategoryName"
+          >
+            <!-- 原有显示名称 -->
+            <span class="bom-node-name">
+              {{ scope.row.name }}
+            </span>
+          </el-tooltip>
+        </template>
+      </el-table-column>
+      <el-table-column prop="deviceCategoryName" 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="materials" label="物料数量" />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row)"
+            v-hasPermi="['rq:iot-bom:update']"
+          >
+            修改
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            @click="openSelectMaterialForm(scope.row.id, scope.row.deviceCategoryId)"
+            v-hasPermi="['rq:iot-bom:update']"
+          >
+            添加物料
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            @click="handleView(scope.row.id)"
+            v-hasPermi="['rq:iot-bom:update']"
+          >
+            物料详情
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['rq:iot-bom:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </ContentWrap>
+
+  <!-- 添加或修改 Bom树节点 对话框 -->
+  <BomFormDevice ref="formRef"  :deviceId="props.deviceId" @success="getList" />
+  <!-- 添加物料列表 -->
+  <MaterialList ref="materialListRef" @choose="chooseMaterial" />
+  <!-- 抽屉组件 -->
+  <MaterialListDrawerDevice
+    :model-value="drawerVisible"
+    @update:model-value="(val) => (drawerVisible = val)"
+    :node-id="currentBomNodeId"
+    :deviceId = "props.deviceId"
+    ref="showDrawer"
+  />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as BomApi from '@/api/pms/devicebom'
+import * as MaterialApi from '@/api/pms/iotdevicematerial'
+import BomFormDevice from './BomFormDevice.vue'
+import { useTreeStore } from '@/store/modules/treeStore'
+import { computed, ref } from 'vue'
+import { handleTree } from '@/utils/tree'
+import MaterialList from '@/views/pms/bom/MaterialList.vue'
+import MaterialListDrawerDevice from '@/views/pms/device/MaterialListDrawerDevice.vue'
+
+defineOptions({ name: 'BomDevice' })
+
+const showDrawer = ref()
+const drawerVisible = ref<boolean>(false)
+const treeStore = useTreeStore()
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const isExpandAll = ref(true) // 是否展开,默认全部展开
+const loading = ref(true) // 列表的加载中
+const currentBomNodeId = ref() // 当前选中的bom节点
+const refreshTable = ref(true) // 重新渲染表格状态
+const list = ref() // 列表的数据
+const props = defineProps<{ deviceId?: number }>()
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  status: undefined,
+  deviceId: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+const CommonBomMaterialData = ref({
+  id: undefined,
+  deviceCategoryId: undefined,
+  bomNodeId: undefined,
+  name: undefined,
+  code: undefined,
+  materialId: undefined,
+  quantity: undefined,
+  deviceId: undefined,
+})
+
+// 从 Store 中获取左侧设备分类树选中的 节点ID
+let selectedId = computed(() => treeStore.selectedId)
+
+/** 查询 BOM树 列表 */
+const getList = async () => {
+  loading.value = true
+  queryParams.deviceId = props.deviceId
+  try {
+    const data = await BomApi.IotDeviceBomApi.getIotDeviceBomPage(queryParams)
+    if (data&&data.list) {
+      list.value = handleTree(data.list)
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 选择物料操作 */
+const materialListRef = ref()
+const openSelectMaterialForm = (id?: number, deviceCategoryId?: number) => {
+  materialListRef.value.open(id)
+  currentBomNodeId.value = id
+  // 保存当前BOM节点的deviceCategoryId
+  CommonBomMaterialData.value.deviceCategoryId = deviceCategoryId
+}
+
+/** 查看物料详情 */
+const handleView = async (nodeId) => {
+  currentBomNodeId.value = nodeId
+  drawerVisible.value = true
+  showDrawer.value.openDrawer()
+  // 强制刷新物料数据
+  await showDrawer.value.loadMaterials(nodeId)
+}
+
+const chooseMaterial = async (row) => {
+  // 将物料关联到bom节点
+  try {
+    // CommonBomMaterialData.value.deviceCategoryId = row.deviceCategoryId
+    CommonBomMaterialData.value.bomNodeId = currentBomNodeId.value
+    CommonBomMaterialData.value.materialId = row.id
+    CommonBomMaterialData.value.name = row.name
+    CommonBomMaterialData.value.code = row.code
+    CommonBomMaterialData.value.deviceId = props.deviceId
+    const data = CommonBomMaterialData.value as unknown as CommonBomMaterialVO
+    debugger
+    await MaterialApi.IotDeviceMaterialApi.createIotDeviceMaterial(data)
+    message.success(t('common.createSuccess'))
+    // 保存成功后立即刷新抽屉数据
+    showDrawer.value.loadMaterials(currentBomNodeId.value)
+    await getList()
+  } finally {
+    // formLoading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  queryParams.deviceCategoryId = selectedId.value
+  getList()
+}
+
+/** 查询所有数据 */
+const handleAllQuery = () => {
+  queryParams.pageNo = 1
+  queryParams.deviceCategoryId = ''
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 处理 设备分类 被点击 */
+// const handleDeviceCategoryTreeNodeClick = async (row) => {
+//   queryParams.deviceCategoryId = row.id
+//   await getList()
+// }
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, row) => {
+  // 如果是没有点击左侧设备树 直接在初始化的列表页面点击某个 BOM节点的修改 也要保存当前BOM关联的设备分类ID
+  if (row != null) {
+    treeStore.setSelectedId(row.deviceCategoryId)
+    formRef.value.open(type, row.id)
+    return
+  }
+  formRef.value.open(type, null)
+}
+
+/** 展开/折叠操作 */
+const toggleExpandAll = () => {
+  refreshTable.value = false
+  isExpandAll.value = !isExpandAll.value
+  nextTick(() => {
+    refreshTable.value = true
+  })
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await BomApi.IotDeviceBomApi.deleteIotDeviceBom(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+
+// 自定义箭头展开位置
+const toggleRowExpansion = (row) => {
+  const $table = document.querySelector('.el-table__body-wrapper') as any
+  if ($table) {
+    $table.__vue__.toggleRowExpansion(row)
+  }
+}
+
+const isExpanded = (row) => {
+  const $table = document.querySelector('.el-table__body-wrapper') as any
+  return $table?.__vue__.store.states.expandRows.value.includes(row)
+}
+</script>
+
+<style scoped>
+/* 确保表格容器正确继承宽度 */
+:deep(.el-table) {
+  width: 100% !important;
+}
+
+/* 操作按钮换行优化 */
+.flex-wrap {
+  flex-wrap: wrap;
+}
+.gap-4px {
+  gap: 4px;
+}
+
+/* BOM节点名称样式 */
+.bom-node-name {
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+</style>

+ 0 - 0
src/views/pms/device/BomTree.vue → src/views/pms/device/bom/BomTree.vue


+ 0 - 0
src/views/pms/device/MaintenanceDetail.vue → src/views/pms/device/maintenance/MaintenanceDetail.vue


+ 0 - 0
src/views/pms/device/MaintenanceList.vue → src/views/pms/device/maintenance/MaintenanceList.vue


+ 0 - 0
src/views/pms/device/ConfigDevicePerson.vue → src/views/pms/device/personlog/ConfigDevicePerson.vue


+ 1 - 1
src/views/pms/device/DevicePerson.vue → src/views/pms/device/personlog/DevicePerson.vue

@@ -182,7 +182,7 @@ import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import DeptTree from '@/views/system/user/DeptTree.vue'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import DevicePersonLogDrawer from "@/views/pms/device/DevicePersonLogDrawer.vue";
+import DevicePersonLogDrawer from "@/views/pms/device/personlog/DevicePersonLogDrawer.vue";
 
 /** 设备台账 列表 */
 defineOptions({ name: 'IotDevicePerson' })

+ 0 - 0
src/views/pms/device/DevicePersonLogDrawer.vue → src/views/pms/device/personlog/DevicePersonLogDrawer.vue


+ 0 - 0
src/views/pms/device/PersonList.vue → src/views/pms/device/personlog/PersonList.vue


+ 0 - 0
src/views/pms/device/RecordInfo.vue → src/views/pms/device/record/RecordInfo.vue


+ 0 - 0
src/views/pms/device/RecordList.vue → src/views/pms/device/record/RecordList.vue


+ 0 - 0
src/views/pms/device/ConfigDeviceStatus.vue → src/views/pms/device/statuslog/ConfigDeviceStatus.vue


+ 1 - 1
src/views/pms/device/DeviceStatus.vue → src/views/pms/device/statuslog/DeviceStatus.vue

@@ -193,7 +193,7 @@ import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import DeptTree from '@/views/system/user/DeptTree.vue'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import DeviceStatusLogDrawer from "@/views/pms/device/DeviceStatusLogDrawer.vue";
+import DeviceStatusLogDrawer from "@/views/pms/device/statuslog/DeviceStatusLogDrawer.vue";
 
 /** 设备台账 列表 */
 defineOptions({ name: 'IotDeviceStatus' })

+ 0 - 0
src/views/pms/device/DeviceStatusLogDrawer.vue → src/views/pms/device/statuslog/DeviceStatusLogDrawer.vue


+ 0 - 0
src/views/pms/device/DeviceStatusLogList.vue → src/views/pms/device/statuslog/DeviceStatusLogList.vue


+ 1 - 1
src/views/pms/maintain/IotMaintain.vue

@@ -274,7 +274,7 @@ import * as UserApi from '@/api/system/user'
 import { ref } from 'vue'
 import { IotMaintainMaterialVO } from '@/api/pms/maintain/material'
 import { useTagsViewStore } from '@/store/modules/tagsView'
-import CustomerList from '@/views/pms/device/CustomerList.vue'
+import CustomerList from '@/views/pms/device/base/CustomerList.vue'
 import WorkOrderMaterial from '@/views/pms/iotmainworkorder/WorkOrderMaterial.vue'
 import { IotMainWorkOrderBomMaterialVO } from '@/api/pms/iotmainworkorderbommaterial'
 import MaterialListDrawer from '@/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue'

+ 1 - 1
src/views/pms/model/IotModelForm.vue

@@ -62,7 +62,7 @@
 <script setup lang="ts">
 import { IotModelApi, IotModelVO } from '@/api/pms/model'
 import {DICT_TYPE, getIntDictOptions, getStrDictOptions} from '@/utils/dict'
-import BrandList from '@/views/pms/device/BrandList.vue'
+import BrandList from '@/views/pms/device/base/BrandList.vue'
 
 /** PMS 功能优化 规格型号 表单 */
 defineOptions({ name: 'IotModelForm' })