DeviceCompleteSet.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. <template>
  2. <el-row :gutter="20">
  3. <!-- 左侧部门树 -->
  4. <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
  5. <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
  6. <ContentWrap style="border: none; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1)">
  7. <!-- 搜索工作栏 -->
  8. <el-form
  9. class="-mb-15px"
  10. :model="queryParams"
  11. ref="queryFormRef"
  12. :inline="true"
  13. @submit.prevent
  14. label-width="68px">
  15. <!-- <el-form-item label="部门名称" prop="deptName" style="margin-left: 20px">
  16. <el-input
  17. v-model="queryParams.deptName"
  18. placeholder="请输入部门名称"
  19. clearable
  20. @keyup.enter="handleQuery"
  21. class="!w-200px"
  22. />
  23. </el-form-item> -->
  24. <el-form-item label="成套名称" prop="name">
  25. <el-input
  26. v-model="queryParams.name"
  27. placeholder="请输入成套名称"
  28. clearable
  29. @keyup.enter="handleQuery"
  30. class="!w-200px" />
  31. </el-form-item>
  32. <el-form-item>
  33. <el-button @click="handleAdd" type="primary"
  34. ><Icon icon="ep:plus" class="mr-5px" />新增成套</el-button
  35. >
  36. <el-button @click="handleQuery"
  37. ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.search') }}</el-button
  38. >
  39. <el-button @click="resetQuery"
  40. ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('devicePerson.reset') }}</el-button
  41. >
  42. </el-form-item>
  43. </el-form>
  44. </ContentWrap>
  45. <!-- 列表 -->
  46. <ContentWrap style="border: none; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1)">
  47. <zm-table :loading="loading" height="calc(85vh - 138px)" :data="list">
  48. <zm-table-column :label="t('monitor.serial')" width="70" align="center">
  49. <template #default="scope">
  50. {{ scope.$index + 1 }}
  51. </template>
  52. </zm-table-column>
  53. <zm-table-column label="部门名称" align="center" prop="deptName" />
  54. <zm-table-column label="成套名称" align="center" prop="name" />
  55. <zm-table-column label="成套类型" align="center">
  56. <template #default="scope">
  57. <dict-tag :type="DICT_TYPE.DEVICE_GROUP_TYPE" :value="scope.row.type" />
  58. </template>
  59. </zm-table-column>
  60. <zm-table-column label="在线状态" align="center">
  61. <template #default="scope">
  62. <el-tag type="success" :underline="false" v-if="scope.row.ifOnline === true"
  63. >在线</el-tag
  64. >
  65. <el-tag type="info" :underline="false" v-else>离线</el-tag>
  66. </template>
  67. </zm-table-column>
  68. <zm-table-column label="描述" align="center" prop="remark" />
  69. <zm-table-column label="设备数量" align="center" prop="deviceCount">
  70. <template #default="scope">
  71. {{ (scope.row.details && scope.row.details.length) || 0 }}
  72. </template>
  73. </zm-table-column>
  74. <zm-table-column label="主设备" align="center" prop="mainDeviceName">
  75. <template #default="scope">
  76. {{
  77. scope.row.details.filter((item) => item.ifMaster)[0]?.deviceName +
  78. ' ' +
  79. scope.row.details.filter((item) => item.ifMaster)[0]?.deviceCode || '无'
  80. }}
  81. </template>
  82. </zm-table-column>
  83. <zm-table-column
  84. :label="t('devicePerson.operation')"
  85. align="center"
  86. min-width="120px"
  87. action>
  88. <template #default="scope">
  89. <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
  90. <el-button link type="danger" @click="handleDelete(scope.row.id)"> 删除 </el-button>
  91. </template>
  92. </zm-table-column>
  93. </zm-table>
  94. <!-- 分页 -->
  95. <Pagination
  96. :total="total"
  97. v-model:page="queryParams.pageNo"
  98. v-model:limit="queryParams.pageSize"
  99. @pagination="getList" />
  100. </ContentWrap>
  101. </el-col>
  102. </el-row>
  103. <!-- 新增/编辑成套设备对话框 -->
  104. <el-dialog :title="dialogTitle" v-model="dialogVisible" width="800px" @close="cancel">
  105. <template #header>
  106. <div class="my-header" style="padding-bottom: 20px">
  107. <span>{{ dialogTitle }}</span>
  108. </div>
  109. </template>
  110. <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
  111. <el-row :gutter="20">
  112. <el-col :span="8">
  113. <el-form-item label="成套名称" prop="name">
  114. <el-input v-model="formData.name" placeholder="请输入成套名称" />
  115. </el-form-item>
  116. </el-col>
  117. <el-col :span="8">
  118. <el-form-item label="成套类型" prop="type">
  119. <el-select
  120. v-model="formData.type"
  121. placeholder="请选择成套类型"
  122. clearable
  123. class="!w-240px">
  124. <el-option
  125. v-for="dict in getStrDictOptions(DICT_TYPE.DEVICE_GROUP_TYPE)"
  126. :key="dict.value"
  127. :label="dict.label"
  128. :value="dict.value" />
  129. </el-select>
  130. </el-form-item>
  131. </el-col>
  132. <el-col :span="8">
  133. <el-form-item :label="t('iotDevice.dept')" prop="deptId">
  134. <el-tree-select
  135. v-model="formData.deptId"
  136. :data="deptList"
  137. :props="defaultProps"
  138. check-strictly
  139. node-key="id"
  140. filterable
  141. placeholder="请选择所在部门"
  142. @change="handleDeptChange" />
  143. </el-form-item>
  144. </el-col>
  145. </el-row>
  146. <el-row :gutter="20">
  147. <el-col :span="24">
  148. <el-form-item label="描述" prop="remark">
  149. <el-input
  150. v-model="formData.remark"
  151. type="textarea"
  152. placeholder="请输入描述"
  153. :rows="2" />
  154. </el-form-item>
  155. </el-col>
  156. </el-row>
  157. <el-row :gutter="20">
  158. <el-col :span="24">
  159. <el-form-item label="选择设备" prop="devices">
  160. <div class="transfer-container">
  161. <el-transfer
  162. v-model="selectedDeviceIds"
  163. :data="deviceOptions"
  164. :titles="['设备列表', '已选择设备']"
  165. :button-texts="['移除', '添加']"
  166. filterable
  167. :filter-method="filterDeviceMethod"
  168. filter-placeholder="请输入设备名称"
  169. @change="rightDeviceChange">
  170. <template #left-empty>
  171. <el-empty :image-size="60" :description="isEdit ? '加载中...' : '请选择设备'" />
  172. </template>
  173. <template #right-empty>
  174. <el-empty :image-size="60" :description="isEdit ? '加载中...' : '请选择设备'" />
  175. </template>
  176. </el-transfer>
  177. </div>
  178. </el-form-item>
  179. </el-col>
  180. </el-row>
  181. <el-row :gutter="20" v-if="selectedDevices.length > 0">
  182. <el-col :span="12">
  183. <el-form-item label="设置主设备" prop="mainDevice" label-width="100">
  184. <el-select
  185. v-model="mainDeviceId"
  186. placeholder="请选择主设备"
  187. clearable
  188. filterable
  189. @change="setMainDevice">
  190. <el-option
  191. v-for="device in selectedDevices"
  192. :key="device.id"
  193. :label="device.label"
  194. :value="device.id" />
  195. </el-select>
  196. </el-form-item>
  197. </el-col>
  198. </el-row>
  199. </el-form>
  200. <template #footer>
  201. <el-button @click="cancel">取 消</el-button>
  202. <el-button type="primary" @click="submit">确 定</el-button>
  203. </template>
  204. </el-dialog>
  205. </template>
  206. <script setup lang="ts">
  207. import { IotDeviceApi } from '@/api/pms/device'
  208. import DeptTree from '@/views/system/user/DeptTree2.vue'
  209. import { defaultProps, handleTree } from '@/utils/tree'
  210. import * as DeptApi from '@/api/system/dept'
  211. import { ElMessageBox } from 'element-plus'
  212. import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
  213. const deptList = ref<Tree[]>([]) // 树形结构
  214. import { useTableComponents } from '@/components/ZmTable/useTableComponents'
  215. const { ZmTable, ZmTableColumn } = useTableComponents()
  216. defineOptions({ name: 'IotDeviceComplete' })
  217. const loading = ref(true) // 列表的加载中
  218. const { t } = useI18n()
  219. let isLeftContentCollapsed = ref(false)
  220. const list = ref([]) // 列表的数据
  221. const total = ref(0) // 列表的总页数
  222. const queryParams = reactive({
  223. pageNo: 1,
  224. pageSize: 10,
  225. deptName: undefined,
  226. name: undefined,
  227. deptId: undefined
  228. })
  229. const queryFormRef = ref(null) // 搜索的表单
  230. const contentSpan = ref(20)
  231. const treeShow = ref(true)
  232. // 对话框相关
  233. const dialogVisible = ref(false)
  234. const dialogTitle = ref('')
  235. const isEdit = ref(false)
  236. // 表单相关
  237. const formRef = ref()
  238. const formData = ref({
  239. name: '',
  240. details: [],
  241. deptId: '',
  242. remark: '',
  243. type: ''
  244. })
  245. // 表单验证规则
  246. const formRules = {
  247. name: [{ required: true, message: '成套名称不能为空', trigger: 'blur' }],
  248. code: [{ required: true, message: '成套编码不能为空', trigger: 'blur' }],
  249. deptId: [{ required: true, message: '请选择部门', trigger: 'change' }],
  250. devices: [
  251. {
  252. required: true,
  253. validator: (rule: any, value: any, callback: any) => {
  254. if (selectedDeviceIds.value.length === 0) {
  255. callback(new Error('请至少选择一个设备'))
  256. } else {
  257. callback()
  258. }
  259. },
  260. trigger: 'change'
  261. }
  262. ],
  263. mainDevice: [
  264. {
  265. required: true,
  266. validator: (rule: any, value: any, callback: any) => {
  267. if (!mainDeviceId.value) {
  268. callback(new Error('请选择主设备'))
  269. } else {
  270. callback()
  271. }
  272. },
  273. trigger: 'change'
  274. }
  275. ]
  276. }
  277. // 部门树数据
  278. const selectedDeptId = ref<number | string>('')
  279. // 新增时部门改变时获取设备列表
  280. const handleDeptChange = async (deptId) => {
  281. if (deptId) {
  282. selectedDeptId.value = deptId
  283. getDeviceList(deptId)
  284. }
  285. // 清空主设备she 设备列表
  286. selectedDevices.value = []
  287. mainDeviceId.value = ''
  288. selectedDeviceIds.value = []
  289. }
  290. // 获取设备列表
  291. const getDeviceList = async (deptId) => {
  292. try {
  293. const res = await IotDeviceApi.getIotDeviceSetOptions(deptId)
  294. deviceOptions.value = res.map((item) => ({
  295. key: item.id, // 始终使用id作为key
  296. label: `${item.deviceName} (${item.deviceCode})`,
  297. ...item
  298. }))
  299. } catch (err) {
  300. console.error(err)
  301. }
  302. }
  303. // 设备选择相关
  304. const deviceOptions = ref<any[]>([])
  305. const selectedDeviceIds = ref([])
  306. const selectedDevices = ref([])
  307. const mainDeviceId = ref('')
  308. /** 查询列表 */
  309. const getList = async () => {
  310. loading.value = true
  311. try {
  312. const data = await IotDeviceApi.getIotDeviceSetList(queryParams)
  313. list.value = data.list
  314. total.value = data.total
  315. } finally {
  316. loading.value = false
  317. }
  318. }
  319. /** 首页处理部门被点击 */
  320. const handleDeptNodeClick = async (row) => {
  321. queryParams.deptId = row.id
  322. await getList()
  323. }
  324. /** 搜索按钮操作 */
  325. const handleQuery = () => {
  326. queryParams.pageNo = 1
  327. getList()
  328. }
  329. /** 重置按钮操作 */
  330. const resetQuery = () => {
  331. queryFormRef.value.resetFields()
  332. handleQuery()
  333. }
  334. // 设备过滤方法
  335. const filterDeviceMethod = (query, item) => {
  336. return item.label.toLowerCase().includes(query.toLowerCase())
  337. }
  338. // 更新已选择的设备列表
  339. const updateSelectedDevices = () => {
  340. // 根据 selectedDeviceIds 从 deviceOptions 中找到对应的设备
  341. selectedDevices.value = deviceOptions.value.filter((item) =>
  342. selectedDeviceIds.value.includes(item.key)
  343. )
  344. console.log('selectedDevices>>>>>>>>>>>>>>>>', selectedDevices.value)
  345. // 构建 details 数据
  346. formData.value.details = selectedDevices.value.map((item) => ({
  347. deviceId: item.id,
  348. deviceName: item.deviceName,
  349. deviceCode: item.deviceCode,
  350. deptId: item.deptId,
  351. ifMaster: item.id === mainDeviceId.value ? true : false
  352. }))
  353. // 如果主设备不在当前选择中,则清空主设备
  354. if (
  355. mainDeviceId.value &&
  356. !selectedDevices.value.some((device) => device.id === mainDeviceId.value)
  357. ) {
  358. mainDeviceId.value = ''
  359. }
  360. }
  361. const rightDeviceChange = (val) => {
  362. selectedDeviceIds.value = val
  363. updateSelectedDevices()
  364. // 手动触发验证
  365. if (formRef.value) {
  366. formRef.value.validateField('devices')
  367. if (val.length > 0) {
  368. formRef.value.validateField('mainDevice')
  369. }
  370. }
  371. }
  372. // 设置主设备
  373. const setMainDevice = (val) => {
  374. mainDeviceId.value = val
  375. // 更新 details 中的 ifMaster 字段
  376. console.log('selectedDevices.value>>>>>>>>>>>>>>>>', selectedDevices.value)
  377. formData.value.details = selectedDevices.value.map((item) => ({
  378. deviceId: item.id,
  379. deviceName: item.deviceName,
  380. deviceCode: item.deviceCode,
  381. deptId: item.deptId,
  382. ifMaster: item.id === mainDeviceId.value ? true : false
  383. }))
  384. console.log('formData.value.details>>>>>>>>>>>>>>>>', formData.value.details)
  385. // 手动触发验证
  386. if (formRef.value) {
  387. formRef.value.validateField('mainDevice')
  388. }
  389. }
  390. // 显示新增对话框
  391. const handleAdd = () => {
  392. isEdit.value = false
  393. dialogTitle.value = '新增成套设备'
  394. resetForm()
  395. dialogVisible.value = true
  396. }
  397. // 显示编辑对话框
  398. const handleEdit = (row) => {
  399. isEdit.value = true
  400. dialogTitle.value = '编辑成套设备'
  401. formData.value = {
  402. ...row
  403. }
  404. // 先清空之前的选项
  405. selectedDeviceIds.value = []
  406. selectedDevices.value = []
  407. deviceOptions.value = []
  408. // 获取设备列表后再设置已选择的设备
  409. getDeviceList(row.deptId).then(() => {
  410. // 设置已选择的设备IDs(使用deviceOptions中的key值)
  411. selectedDeviceIds.value = row.details.map((item) => {
  412. // 在编辑模式下,使用deviceOptions中对应项的key作为标识
  413. const option = deviceOptions.value.find((opt) => opt.deviceId === item.deviceId)
  414. return option ? option.key : item.deviceId
  415. })
  416. mainDeviceId.value = row.details.find((item) => item.ifMaster)?.deviceId || ''
  417. // 更新selectedDevices数组
  418. nextTick(() => {
  419. updateSelectedDevices()
  420. })
  421. })
  422. dialogVisible.value = true
  423. }
  424. //删除成套
  425. const handleDelete = async (id: number) => {
  426. ElMessageBox.confirm('确定要删除该成套设备吗?', '提示', {
  427. confirmButtonText: '确定',
  428. cancelButtonText: '取消',
  429. type: 'warning'
  430. })
  431. .then(async () => {
  432. try {
  433. await IotDeviceApi.deleteIotDeviceSet(id)
  434. ElMessage.success('删除成功')
  435. getList()
  436. } catch (error) {
  437. console.error(error)
  438. }
  439. })
  440. .catch(() => {
  441. // 取消操作
  442. })
  443. }
  444. // 重置表单
  445. const resetForm = () => {
  446. formData.value = {
  447. name: '',
  448. details: [],
  449. deptId: '',
  450. remark: ''
  451. }
  452. selectedDeviceIds.value = []
  453. selectedDevices.value = []
  454. deviceOptions.value = []
  455. mainDeviceId.value = ''
  456. }
  457. // 取消操作
  458. const cancel = () => {
  459. dialogVisible.value = false
  460. resetForm()
  461. }
  462. // 提交表单
  463. const submit = async () => {
  464. if (!formRef.value) return
  465. await formRef.value.validate(async (valid) => {
  466. if (!valid) return
  467. try {
  468. const data = {
  469. ...formData.value
  470. }
  471. console.log('提交数据:', data)
  472. if (isEdit.value) {
  473. await IotDeviceApi.updateIotDeviceSet(data)
  474. ElMessage.success('编辑成功')
  475. } else {
  476. await IotDeviceApi.createIotDeviceSet(data)
  477. ElMessage.success('新增成功')
  478. }
  479. dialogVisible.value = false
  480. getList()
  481. } catch (error) {
  482. console.error(error)
  483. }
  484. })
  485. }
  486. onMounted(async () => {
  487. getList()
  488. deptList.value = handleTree(await DeptApi.getSimpleDeptList())
  489. })
  490. </script>
  491. <style scoped>
  492. .transfer-container {
  493. display: flex;
  494. flex-direction: column;
  495. }
  496. .transfer-footer {
  497. margin-top: 8px;
  498. text-align: right;
  499. }
  500. ::deep(.el-transfer-panel) {
  501. width: 300px;
  502. }
  503. ::deep(.el-tree--highlight-current) {
  504. height: 200px !important;
  505. }
  506. /* 调整穿梭框面板主体高度 */
  507. ::deep(.el-transfer-panel__body) {
  508. height: 850px !important;
  509. }
  510. /* 如果需要进一步调整,可以分别设置左右面板的高度 */
  511. ::deep(.el-transfer-panel .el-transfer-panel__list) {
  512. height: 850px !important; /* 调整列表区域高度 */
  513. }
  514. .el-transfer {
  515. --el-transfer-panel-body-height: 500px;
  516. }
  517. </style>