DeviceCompleteSet.vue 16 KB

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