ConfigDeviceAllot.vue 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. <template>
  2. <div class="container" >
  3. <el-row :gutter="20" class="equal-height-row">
  4. <!-- 左侧设备列表 -->
  5. <el-col :span="12" class="col-height">
  6. <div class="card left-card">
  7. <h3>设备列表</h3>
  8. <div class="dept-select">
  9. <el-tree-select
  10. clearable
  11. v-model="formData.deptId"
  12. :data="deptList"
  13. :props="defaultProps"
  14. check-strictly
  15. node-key="id"
  16. filterable
  17. placeholder="请选择设备所属部门"
  18. @node-click="handleDeptDeviceTreeNodeClick"
  19. />
  20. </div>
  21. <el-scrollbar height="500px">
  22. <el-checkbox-group v-model="selectedDevices">
  23. <div
  24. v-for="device in simpleDevices"
  25. :key="device.id"
  26. class="checkbox-item"
  27. >
  28. <el-checkbox :label="device.id">
  29. {{ device.deviceCode }} ({{ device.deviceName }}) - {{ device.deptName }}
  30. </el-checkbox>
  31. </div>
  32. </el-checkbox-group>
  33. </el-scrollbar>
  34. </div>
  35. </el-col>
  36. <!-- 右侧部门选择 -->
  37. <el-col :span="12" class="col-height">
  38. <div class="card right-card">
  39. <h3>部门列表</h3>
  40. <ContentWrap class="dept-tree-container" height="400px">
  41. <DeptTree2 ref="deptTreeRef" v-model="selectedDeptId" height="100%" @update:modelValue="handleDeptChange"/>
  42. </ContentWrap>
  43. </div>
  44. </el-col>
  45. </el-row>
  46. <!-- 暂存关联列表 -->
  47. <div class="submit-area">
  48. <div class="card">
  49. <el-input
  50. v-model="formData.reason"
  51. placeholder="请输入调拨原因"
  52. class="reason-input"
  53. type="textarea"
  54. :rows="3"
  55. @input="updateTempRelations"
  56. />
  57. </div>
  58. <el-button
  59. type="primary"
  60. size="large"
  61. @click="submitRelations"
  62. :disabled="tempRelations.length === 0"
  63. >
  64. 保 存
  65. </el-button>
  66. <div class="temp-list card" v-if="false">
  67. <h3>设备调拨记录</h3>
  68. <el-table :data="tempRelations" style="width: 100%">
  69. <el-table-column prop="deviceNames" label="设备" width="200" />
  70. <el-table-column prop="deptName" label="部门" />
  71. <el-table-column prop="deptId" label="部门id" v-if="false"/>
  72. <el-table-column prop="reason" label="调拨原因" />
  73. <el-table-column label="操作" width="120">
  74. <template #default="{ row }">
  75. <el-button
  76. type="danger"
  77. size="small"
  78. @click="removeTempRelation(row.deviceId)"
  79. >
  80. 删除
  81. </el-button>
  82. </template>
  83. </el-table-column>
  84. </el-table>
  85. </div>
  86. </div>
  87. </div>
  88. </template>
  89. <script setup lang="ts">
  90. import { ref, computed } from 'vue'
  91. import { ElMessage } from 'element-plus'
  92. import {defaultProps, handleTree} from "@/utils/tree";
  93. import * as DeptApi from "@/api/system/dept";
  94. import {IotDeviceApi, IotDeviceVO} from "@/api/pms/device";
  95. import DeptTree2 from "@/views/pms/device/DeptTree2.vue";
  96. import { useRouter } from 'vue-router'
  97. const router = useRouter()
  98. defineOptions({ name: 'ConfigDeviceAllot' })
  99. const selectedDeptId = ref<number | string>('')
  100. const simpleDevices = ref<IotDeviceVO[]>([])
  101. const deptList = ref<Tree[]>([]) // 树形结构
  102. const selectedDevices = ref<number[]>([]) // 改为数组存储多选
  103. const formData = ref({
  104. id: undefined,
  105. deviceCode: undefined,
  106. deviceName: undefined,
  107. brand: undefined,
  108. model: undefined,
  109. deptId: undefined as string | undefined,
  110. deviceStatus: undefined,
  111. reason: '',
  112. assetProperty: undefined,
  113. picUrl: undefined,
  114. })
  115. const queryParams = reactive({
  116. deptId: formData.value.deptId,
  117. })
  118. const deptTreeRef = ref<InstanceType<typeof DeptTree2>>()
  119. const emit = defineEmits(['success', 'node-click']) // 定义 success 树点击 事件,用于操作成功后的回调
  120. // 响应式数据
  121. const tempRelationsMap = ref(new Map<number, {
  122. deviceId: number
  123. deviceNames: string
  124. deptId: number
  125. deptName: string
  126. reason: string
  127. }>())
  128. // 计算属性转换 Map 为数组
  129. const tempRelations = computed(() =>
  130. Array.from(tempRelationsMap.value.values())
  131. )
  132. const updateTempRelations = () => {
  133. if (!selectedDeptId.value) {
  134. // 未选择部门时清除所有关联
  135. tempRelationsMap.value.clear()
  136. return
  137. }
  138. // 获取部门信息
  139. const treeNode = deptTreeRef.value?.treeRef?.getNode(selectedDeptId.value)
  140. const deptName = treeNode?.data?.name || '未知部门'
  141. // 创建当前应该存在的设备集合
  142. const currentDeviceIds = new Set(selectedDevices.value)
  143. // 清理无效关联(包含:1.当前部门外的关联 2.未选中的设备)
  144. Array.from(tempRelationsMap.value.entries()).forEach(([deviceId, relation]) => {
  145. if (relation.deptId !== selectedDeptId.value || !currentDeviceIds.has(deviceId)) {
  146. tempRelationsMap.value.delete(deviceId)
  147. }
  148. })
  149. // 添加/更新当前选中设备的关联
  150. selectedDevices.value.forEach(deviceId => {
  151. const device = simpleDevices.value.find(d => d.id === deviceId)
  152. if (device) {
  153. tempRelationsMap.value.set(deviceId, {
  154. deviceId,
  155. deviceNames: `${device.deviceCode} (${device.deviceName})`,
  156. deptId: selectedDeptId.value as number,
  157. deptName,
  158. reason: formData.value.reason
  159. })
  160. }
  161. })
  162. }
  163. // 添加设备选择监听
  164. watch([selectedDevices, selectedDeptId], () => {
  165. updateTempRelations();
  166. }, {deep: true, immediate: true, debounce: 100})
  167. // 修改部门变更处理方法
  168. const handleDeptChange = (deptId) => {
  169. if (!deptId) {
  170. selectedDeptId.value = ''
  171. return
  172. }
  173. // 校验部门选择
  174. if (!validateDeptSelection(deptId)) {
  175. // 重置部门选择
  176. selectedDeptId.value = ''
  177. deptTreeRef.value?.treeRef?.setCurrentKey(undefined) // 清除树的选择状态
  178. deptTreeRef.value?.treeRef?.setCurrentNode(null)
  179. return
  180. }
  181. selectedDeptId.value = deptId
  182. updateTempRelations()
  183. }
  184. /** 处理 部门-设备 树 被点击 */
  185. const handleDeptDeviceTreeNodeClick = async (row: { [key: string]: any }) => {
  186. emit('node-click', row)
  187. formData.value.deptId = row.id
  188. selectedDevices.value = []
  189. await getDeviceList()
  190. }
  191. /** 获得 部门下的设备 列表 */
  192. const getDeviceList = async () => {
  193. try {
  194. const params = { deptId: formData.value.deptId }
  195. const data = await IotDeviceApi.simpleDevices(params)
  196. simpleDevices.value = data || []
  197. } catch (error) {
  198. simpleDevices.value = []
  199. console.error('获取设备列表失败:', error)
  200. }
  201. }
  202. // 计算选中的设备原部门集合
  203. const originDeptIds = computed(() =>
  204. selectedDevices.value
  205. .map(deviceId =>
  206. simpleDevices.value.find(d => d.id === deviceId)?.deptId
  207. )
  208. .filter(Boolean) as number[]
  209. )
  210. // 部门选择校验方法
  211. const validateDeptSelection = (deptId: number) => {
  212. if (originDeptIds.value.includes(deptId)) {
  213. ElMessage.error('不能选择设备原属部门作为调拨部门')
  214. return false
  215. }
  216. return true
  217. }
  218. const removeTempRelation = (deviceId: number) => {
  219. tempRelationsMap.value.delete(deviceId)
  220. selectedDevices.value = selectedDevices.value.filter(id => id !== deviceId)
  221. };
  222. const submitRelations = async () => {
  223. try {
  224. // 提交前二次校验
  225. const invalidDept = tempRelations.value.some(r =>
  226. originDeptIds.value.includes(r.deptId)
  227. )
  228. if (invalidDept) {
  229. ElMessage.error('存在调拨部门与设备原属部门相同的情况,请检查')
  230. return
  231. }
  232. // 转换为后端需要的格式
  233. const submitData = tempRelations.value.map(r => ({
  234. deviceId: r.deviceId,
  235. deptId: r.deptId,
  236. reason: r.reason
  237. }))
  238. await IotDeviceApi.saveDeviceAllot(submitData)
  239. // 模拟API调用
  240. console.log('提交数据:', submitData)
  241. ElMessage.success('提交成功')
  242. tempRelations.value = []
  243. router.back()
  244. } catch (error) {
  245. ElMessage.error('提交失败,请重试')
  246. }
  247. }
  248. /** 初始化 */
  249. onMounted(async () => {
  250. // 初始化部门树 根据选择的部门 查询设备列表 部门人员列表
  251. deptList.value = handleTree(await DeptApi.getSimpleDeptList())
  252. nextTick(() => {
  253. // 强制重新计算布局
  254. window.dispatchEvent(new Event('resize'))
  255. })
  256. })
  257. </script>
  258. <style scoped>
  259. .container {
  260. padding: 20px;
  261. }
  262. .card {
  263. border: 1px solid #ebeef5;
  264. border-radius: 4px;
  265. padding: 20px;
  266. margin-bottom: 20px;
  267. background: white;
  268. display: flex;
  269. flex-direction: column;
  270. height: 100%; /* 根据实际需要调整整体高度 */
  271. box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
  272. transition: box-shadow 0.2s;
  273. }
  274. .card:hover {
  275. box-shadow: 0 4px 16px rgba(0,0,0,0.15);
  276. }
  277. .list-item {
  278. padding: 8px 12px;
  279. border-bottom: 1px solid #f0f0f0;
  280. }
  281. .dept-select {
  282. margin-bottom: 20px;
  283. }
  284. .user-list {
  285. margin-bottom: 20px;
  286. border: 1px solid #ebeef5;
  287. border-radius: 4px;
  288. padding: 10px;
  289. }
  290. .action-bar {
  291. text-align: right;
  292. }
  293. .temp-list {
  294. margin-top: 20px;
  295. }
  296. .submit-area {
  297. margin-top: 5px;
  298. text-align: center;
  299. }
  300. h3 {
  301. margin: 0 0 15px 0;
  302. color: #303133;
  303. }
  304. .dept-select {
  305. flex-shrink: 0; /* 防止搜索框被压缩 */
  306. }
  307. .el-scrollbar__wrap {
  308. overflow-x: hidden;
  309. }
  310. .tree-container .el-tree {
  311. height: 100% !important;
  312. }
  313. /* 左侧滚动区域 */
  314. .el-scrollbar,
  315. .tree-container {
  316. scrollbar-width: thin;
  317. scrollbar-color: #c0c4cc transparent;
  318. }
  319. .left-card,
  320. .right-card {
  321. flex: 1;
  322. display: flex;
  323. flex-direction: column;
  324. height: 100%; /* 继承父级高度 */
  325. }
  326. .dept-tree-container {
  327. flex: 1;
  328. min-height: 0; /* 关键:允许内容区域收缩 */
  329. position: relative;
  330. overflow: hidden;
  331. }
  332. /* 调整树形组件内部滚动 */
  333. :deep(.el-tree) {
  334. height: 100%;
  335. overflow: auto;
  336. }
  337. .equal-height-row {
  338. display: flex;
  339. align-items: stretch; /* 关键:等高布局 */
  340. }
  341. .col-height {
  342. display: flex;
  343. flex-direction: column;
  344. min-height: 400px;/* 统一最小高度 */
  345. }
  346. </style>