index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. <template>
  2. <doc-alert title="用户体系" url="https://doc.iocoder.cn/user-center/" />
  3. <doc-alert title="三方登陆" url="https://doc.iocoder.cn/social-user/" />
  4. <doc-alert title="Excel 导入导出" url="https://doc.iocoder.cn/excel-import-and-export/" />
  5. <el-row :gutter="20">
  6. <!-- 左侧 设备分类 树 -->
  7. <el-col :span="4" :xs="24">
  8. <ContentWrap class="h-1/1">
  9. <DeviceCategoryTree @node-click="handleDeviceCategoryTreeNodeClick" />
  10. </ContentWrap>
  11. </el-col>
  12. <el-col :span="20" :xs="24">
  13. <!-- 搜索 -->
  14. <ContentWrap>
  15. <el-form
  16. class="-mb-15px"
  17. :model="queryParams"
  18. ref="queryFormRef"
  19. :inline="true"
  20. label-width="110px"
  21. >
  22. <el-form-item label="BOM节点名称" prop="name">
  23. <el-input
  24. v-model="queryParams.name"
  25. placeholder="请输入BOM节点名称"
  26. clearable
  27. @keyup.enter="handleQuery"
  28. class="!w-240px"
  29. />
  30. </el-form-item>
  31. <el-form-item label="BOM节点" prop="status">
  32. <el-select
  33. v-model="queryParams.status"
  34. placeholder="请选择BOM节点"
  35. clearable
  36. class="!w-240px"
  37. >
  38. <el-option
  39. v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
  40. :key="dict.value"
  41. :label="dict.label"
  42. :value="dict.value"
  43. />
  44. </el-select>
  45. </el-form-item>
  46. <el-form-item>
  47. <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
  48. <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
  49. <el-button
  50. type="primary"
  51. plain
  52. @click="openForm('create', null)"
  53. v-hasPermi="['rq:iot-bom:create']"
  54. >
  55. <Icon icon="ep:plus" class="mr-5px" /> 新增
  56. </el-button>
  57. <el-button type="danger" plain @click="toggleExpandAll">
  58. <Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
  59. </el-button>
  60. <!--
  61. <el-button @click="handleAllQuery"><Icon icon="ep:search" class="mr-5px" /> 查询所有</el-button> -->
  62. </el-form-item>
  63. </el-form>
  64. </ContentWrap>
  65. <ContentWrap>
  66. <el-table
  67. v-loading="loading"
  68. :data="list"
  69. row-key="id"
  70. :default-expand-all="isExpandAll"
  71. v-if="refreshTable"
  72. style="width: 100%"
  73. @row-click="handleClick"
  74. >
  75. <el-table-column prop="name" label="BOM节点" >
  76. <template #default="scope">
  77. <!-- 使用 el-tooltip 包裹内容 -->
  78. <el-tooltip
  79. effect="dark"
  80. :content="`设备分类:${scope.row.deviceCategoryName || '暂无'}`"
  81. placement="top-start"
  82. :disabled="!scope.row.deviceCategoryName"
  83. >
  84. <!-- 原有显示名称 -->
  85. <span class="bom-node-name">
  86. {{ scope.row.name }}
  87. </span>
  88. </el-tooltip>
  89. </template>
  90. </el-table-column>
  91. <el-table-column prop="deviceCategoryName" label="设备分类" />
  92. <el-table-column label="维修" width="100">
  93. <template #default="scope">
  94. <el-switch
  95. :model-value="scope.row.type?.includes(1)"
  96. active-value
  97. inactive-value
  98. disabled
  99. />
  100. </template>
  101. </el-table-column>
  102. <el-table-column label="保养" width="100">
  103. <template #default="scope">
  104. <el-switch
  105. :model-value="scope.row.type?.includes(2)"
  106. active-value
  107. inactive-value
  108. disabled
  109. />
  110. </template>
  111. </el-table-column>
  112. <el-table-column prop="sort" label="排序" width="80"/>
  113. <!-- <el-table-column prop="status" label="状态" width="80">
  114. <template #default="scope">
  115. <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
  116. </template>
  117. </el-table-column> -->
  118. <!--
  119. <el-table-column
  120. label="创建时间"
  121. align="center"
  122. prop="createTime"
  123. :formatter="dateFormatter"
  124. /> -->
  125. <el-table-column prop="materials" label="物料数量" width="80"/>
  126. <el-table-column label="操作" align="center" width="300">
  127. <template #default="scope">
  128. <el-button
  129. link
  130. type="primary"
  131. @click="openForm('update', scope.row)"
  132. v-hasPermi="['rq:iot-bom:update']"
  133. >
  134. 修改
  135. </el-button>
  136. <el-button
  137. link
  138. type="primary"
  139. @click="openSelectMaterialForm(scope.row)"
  140. v-hasPermi="['rq:iot-bom:update']"
  141. >
  142. 添加物料
  143. </el-button>
  144. <el-button
  145. link
  146. type="primary"
  147. @click="handleView(scope.row)"
  148. v-hasPermi="['rq:iot-bom:update']"
  149. >
  150. 物料详情
  151. </el-button>
  152. <el-button
  153. link
  154. type="danger"
  155. @click="handleDelete(scope.row.id)"
  156. v-hasPermi="['rq:iot-bom:delete']"
  157. >
  158. 删除
  159. </el-button>
  160. </template>
  161. </el-table-column>
  162. </el-table>
  163. </ContentWrap>
  164. </el-col>
  165. </el-row>
  166. <!-- 添加或修改 Bom树节点 对话框 -->
  167. <BomForm ref="formRef" :category_id="selectedId" @success="getList" />
  168. <!-- 添加物料列表 -->
  169. <MaterialList ref="materialListRef" @choose="chooseMaterial" />
  170. <!-- 抽屉组件 -->
  171. <MaterialListDrawer
  172. :model-value="drawerVisible"
  173. @update:model-value="val => drawerVisible = val"
  174. :node-id="currentBomNodeId"
  175. ref="showDrawer"
  176. :row-info="currentRowInfo"
  177. @refresh="handleDrawerClosed"
  178. />
  179. </template>
  180. <script lang="ts" setup>
  181. import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
  182. import * as BomApi from '@/api/pms/bom'
  183. import {CommonBomMaterialApi, CommonBomMaterialVO} from '@/api/pms/commonbommaterial'
  184. import BomForm from './BomForm.vue'
  185. import DeviceCategoryTree from './DeviceCategoryTree.vue'
  186. import { useTreeStore } from '@/store/modules/treeStore';
  187. import { ref, computed } from 'vue';
  188. import { handleTree } from '@/utils/tree'
  189. import MaterialList from "@/views/pms/bom/MaterialList.vue";
  190. import MaterialListDrawer from "@/views/pms/bom/MaterialListDrawer.vue";
  191. defineOptions({ name: 'Bom' })
  192. const showDrawer = ref()
  193. const drawerVisible = ref<boolean>(false)
  194. const treeStore = useTreeStore();
  195. const message = useMessage() // 消息弹窗
  196. const { t } = useI18n() // 国际化
  197. const isExpandAll = ref(true) // 是否展开,默认全部展开
  198. const loading = ref(true) // 列表的加载中
  199. const currentBomNodeId = ref() // 当前选中的bom节点
  200. const refreshTable = ref(true) // 重新渲染表格状态
  201. const list = ref() // 列表的数据
  202. // 添加存储当前行信息的变量 抽屉页面使用
  203. const currentRowInfo = ref({
  204. deviceCategoryName: '',
  205. bomNodeName: ''
  206. })
  207. const queryParams = reactive({
  208. pageNo: 1,
  209. pageSize: 10,
  210. name: undefined,
  211. status: undefined,
  212. deviceCategoryId: undefined,
  213. })
  214. const queryFormRef = ref() // 搜索的表单
  215. const selectedRow = ref<any>(null)
  216. const CommonBomMaterialData = ref({
  217. id: undefined,
  218. deviceCategoryId: undefined,
  219. bomNodeId: undefined,
  220. name: undefined,
  221. code: undefined,
  222. materialId: undefined,
  223. quantity: undefined,
  224. })
  225. // 从 Store 中获取左侧设备分类树选中的 节点ID
  226. let selectedId = computed(() => treeStore.selectedId);
  227. /** 查询 BOM树 列表 */
  228. const getList = async () => {
  229. loading.value = true
  230. try {
  231. const data = await BomApi.getBomPage(queryParams)
  232. list.value = handleTree(data)
  233. } finally {
  234. loading.value = false
  235. }
  236. }
  237. /** 选择物料操作 */
  238. const materialListRef = ref()
  239. const openSelectMaterialForm = (row: any) => {
  240. materialListRef.value.open(row)
  241. currentBomNodeId.value = row.id
  242. // 保存当前BOM节点的deviceCategoryId
  243. CommonBomMaterialData.value.deviceCategoryId = row.deviceCategoryId
  244. }
  245. /** 查看物料详情 */
  246. const handleView = async (row) => {
  247. currentBomNodeId.value = row.id
  248. // 保存当前行的信息
  249. currentRowInfo.value = {
  250. deviceCategoryName: row.deviceCategoryName || '暂无',
  251. bomNodeName: row.name || '暂无'
  252. }
  253. drawerVisible.value = true
  254. showDrawer.value.openDrawer()
  255. // 强制刷新物料数据
  256. await showDrawer.value.loadMaterials(row.id)
  257. }
  258. const chooseSingleMaterial = async(row) => {
  259. // 将物料关联到bom节点
  260. try {
  261. // CommonBomMaterialData.value.deviceCategoryId = row.deviceCategoryId
  262. CommonBomMaterialData.value.bomNodeId = currentBomNodeId.value
  263. CommonBomMaterialData.value.materialId = row.id
  264. CommonBomMaterialData.value.name = row.name
  265. CommonBomMaterialData.value.code = row.code
  266. const data = CommonBomMaterialData.value as unknown as CommonBomMaterialVO
  267. await CommonBomMaterialApi.createCommonBomMaterial(data);
  268. message.success(t('common.createSuccess'))
  269. // 保存成功后立即刷新抽屉数据
  270. showDrawer.value.loadMaterials(currentBomNodeId.value)
  271. await getList()
  272. } finally {
  273. // formLoading.value = false
  274. }
  275. }
  276. const chooseMaterial = async(selectedMaterials) => {
  277. // 将物料关联到bom节点
  278. try {
  279. // 转换数据结构(根据接口定义调整)
  280. const materialsData = selectedMaterials.map(material => ({
  281. deviceCategoryId: CommonBomMaterialData.value.deviceCategoryId,
  282. bomNodeId: currentBomNodeId.value,
  283. materialId: material.id,
  284. name: material.name,
  285. code: material.code,
  286. quantity: material.quantity
  287. }))
  288. // 调用批量添加接口
  289. const resultCount = await CommonBomMaterialApi.addMaterials(materialsData)
  290. message.success(`成功添加物料数量:` + resultCount)
  291. // message.success(t('common.createSuccess'))
  292. // 保存成功后立即刷新抽屉数据
  293. showDrawer.value.loadMaterials(currentBomNodeId.value)
  294. await getList()
  295. } catch (error) {
  296. message.error('添加物料失败!')
  297. }
  298. }
  299. /** 搜索按钮操作 */
  300. const handleQuery = () => {
  301. queryParams.pageNo = 1
  302. queryParams.deviceCategoryId = selectedId.value
  303. getList()
  304. }
  305. /** 查询所有数据 */
  306. const handleAllQuery = () => {
  307. queryParams.pageNo = 1
  308. queryParams.deviceCategoryId = ''
  309. getList()
  310. }
  311. /** 重置按钮操作 */
  312. const resetQuery = () => {
  313. queryFormRef.value?.resetFields()
  314. handleQuery()
  315. }
  316. /** 处理 设备分类 被点击 */
  317. const handleDeviceCategoryTreeNodeClick = async (row) => {
  318. clearRowSelection() //清除表格行选择
  319. queryParams.deviceCategoryId = row.id
  320. await getList()
  321. }
  322. // 添加处理抽屉关闭的方法
  323. const handleDrawerClosed = () => {
  324. getList() // 刷新BOM树数据
  325. }
  326. const parentId = ref('')
  327. const handleClick = (row: any) => {
  328. parentId.value = row.id
  329. selectedRow.value = row // 存储整行数据
  330. console.log('当前行被点击了:' + selectedRow.value.deviceCategoryId)
  331. }
  332. /** 添加/修改操作 */
  333. const formRef = ref()
  334. const openForm = (type: string, row) => {
  335. // 优先使用设备分类树的选择(当selectedRow不存在时)
  336. const useTreeSelection = !selectedRow.value && selectedId.value
  337. // 新增操作时使用选中行的数据
  338. if (type === 'create' && selectedRow.value) {
  339. formRef.value.open(
  340. type,
  341. null,
  342. selectedRow.value?.id || (useTreeSelection ? undefined : undefined), // 作为 parentId
  343. selectedRow.value?.deviceCategoryId || selectedId.value // // 设备分类ID:表格行 > 设备分类树 > 无
  344. )
  345. return
  346. }
  347. // 如果是没有点击左侧设备树 直接在初始化的列表页面点击某个 BOM节点的修改 也要保存当前BOM关联的设备分类ID
  348. if(row != null) {
  349. treeStore.setSelectedId(row.deviceCategoryId)
  350. formRef.value.open(type, row.id, parentId.value)
  351. return
  352. }
  353. formRef.value.open(type, null, parentId.value)
  354. }
  355. /** 展开/折叠操作 */
  356. const toggleExpandAll = () => {
  357. refreshTable.value = false
  358. isExpandAll.value = !isExpandAll.value
  359. nextTick(() => {
  360. refreshTable.value = true
  361. })
  362. }
  363. // 清除表格行选择
  364. const clearRowSelection = () => {
  365. selectedRow.value = null
  366. parentId.value = ''
  367. }
  368. /** 删除按钮操作 */
  369. const handleDelete = async (id: number) => {
  370. try {
  371. // 查找目标节点及其在树中的位置
  372. const targetNode = findNodeById(list.value, id);
  373. // 检查是否存在子节点(使用 children 属性)
  374. if (targetNode && targetNode.children && targetNode.children.length > 0) {
  375. message.error('当前BOM节点包含子节点,不可删除');
  376. return;
  377. }
  378. // 删除的二次确认
  379. await message.delConfirm()
  380. // 发起删除
  381. await BomApi.deleteBomNode(id)
  382. message.success(t('common.delSuccess'))
  383. // 刷新列表
  384. await getList()
  385. } catch {}
  386. }
  387. /** 初始化 */
  388. onMounted(() => {
  389. getList()
  390. })
  391. // 自定义箭头展开位置
  392. const toggleRowExpansion = (row) => {
  393. const $table = document.querySelector('.el-table__body-wrapper') as any
  394. if ($table) {
  395. $table.__vue__.toggleRowExpansion(row)
  396. }
  397. }
  398. const isExpanded = (row) => {
  399. const $table = document.querySelector('.el-table__body-wrapper') as any
  400. return $table?.__vue__.store.states.expandRows.value.includes(row)
  401. }
  402. /**
  403. * 递归查找树节点
  404. * 基于 handleTree 处理后的树结构(包含 children 属性)
  405. * @param nodes 树节点数组
  406. * @param id 要查找的节点ID
  407. * @returns 找到的节点或 undefined
  408. */
  409. const findNodeById = (nodes: any[], id: number): any | undefined => {
  410. // 遍历当前层级节点
  411. for (const node of nodes) {
  412. // 1. 检查当前节点是否匹配
  413. if (node.id === id) return node;
  414. // 2. 检查当前节点是否有子节点
  415. if (node.children && node.children.length > 0) {
  416. // 递归搜索子节点
  417. const foundInChildren = findNodeById(node.children, id);
  418. if (foundInChildren) return foundInChildren;
  419. }
  420. }
  421. return undefined; // 未找到
  422. };
  423. </script>
  424. <style scoped>
  425. /* 确保表格容器正确继承宽度 */
  426. :deep(.el-table) {
  427. width: 100% !important;
  428. }
  429. /* 操作按钮换行优化 */
  430. .flex-wrap {
  431. flex-wrap: wrap;
  432. }
  433. .gap-4px {
  434. gap: 4px;
  435. }
  436. /* BOM节点名称样式 */
  437. .bom-node-name {
  438. flex: 1;
  439. overflow: hidden;
  440. text-overflow: ellipsis;
  441. white-space: nowrap;
  442. }
  443. </style>