ConfigDevicePerson.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. <template>
  2. <div class="container">
  3. <el-row :gutter="20" class="equal-height-row">
  4. <!-- 左侧设备列表 -->
  5. <el-col :span="12">
  6. <div class="card">
  7. <h3>{{ t('configPerson.deviceList') }}</h3>
  8. <div class="dept-select">
  9. <el-tree-select
  10. clearable
  11. v-model="formData.deptId1"
  12. :data="deptList"
  13. :props="defaultProps"
  14. check-strictly
  15. node-key="id"
  16. filterable
  17. :placeholder="t('configPerson.deviceListHolder')"
  18. @node-click="handleDeptDeviceTreeNodeClick"
  19. />
  20. </div>
  21. <!-- 设备搜索框 -->
  22. <div class="filter-input">
  23. <el-input
  24. v-model="deviceFilterText"
  25. :placeholder="t('devicePerson.filterDevicePlaceholder')"
  26. clearable
  27. prefix-icon="Search"
  28. />
  29. </div>
  30. <el-scrollbar height="400px">
  31. <el-checkbox-group v-model="selectedDevices">
  32. <div
  33. v-for="device in filteredDevices"
  34. :key="device.id"
  35. class="radio-item"
  36. >
  37. <el-checkbox :label="device.id">
  38. {{ device.deviceCode }} ({{ device.deviceName }}) - {{ device.devicePersons }}
  39. </el-checkbox>
  40. </div>
  41. </el-checkbox-group>
  42. </el-scrollbar>
  43. </div>
  44. </el-col>
  45. <!-- 右侧责任人选择 -->
  46. <el-col :span="12">
  47. <div class="card">
  48. <h3>{{ t('configPerson.rp') }}</h3>
  49. <div class="dept-select">
  50. <el-tree-select
  51. clearable
  52. v-model="formData.deptId"
  53. :data="deptList"
  54. :props="defaultProps"
  55. check-strictly
  56. node-key="id"
  57. filterable
  58. :placeholder="t('configPerson.rpHolder')"
  59. @node-click="handleDeptUserTreeNodeClick"
  60. />
  61. </div>
  62. <el-scrollbar height="450px">
  63. <el-checkbox-group v-model="selectedUsers" @change="handleUserSelectionChange">
  64. <div
  65. v-for="user in simpleUsers"
  66. :key="user.id"
  67. class="list-item"
  68. >
  69. <el-checkbox :label="user.id">
  70. {{ user.nickname }} ({{ user.deptName }})
  71. </el-checkbox>
  72. </div>
  73. </el-checkbox-group>
  74. </el-scrollbar>
  75. </div>
  76. </el-col>
  77. </el-row>
  78. <!-- 暂存关联列表 -->
  79. <div class="submit-area">
  80. <div class="card">
  81. <h3>{{ t('configPerson.reasonForAdjustment') }}<span class="required-star">*</span></h3>
  82. <el-input
  83. v-model="formData.reason"
  84. :placeholder="t('configPerson.rfaHolder')"
  85. class="reason-input"
  86. type="textarea"
  87. :rows="3"
  88. @input="handleReasonInput"
  89. :disabled="selectedDevices.length === 0 || selectedUsers.length === 0"
  90. />
  91. </div>
  92. <div class="temp-list card">
  93. <h3>{{ t('configPerson.adjustmentRecords') }}</h3>
  94. <el-table :data="tempRelations" style="width: 100%">
  95. <el-table-column prop="deviceNames" :label="t('devicePerson.deviceName')" width="200" />
  96. <el-table-column prop="userNames" :label="t('devicePerson.rp')" />
  97. <el-table-column prop="reason" :label="t('configPerson.reasonForAdjustment')" />
  98. <el-table-column :label="t('devicePerson.operation')" width="120">
  99. <template #default="{ row }">
  100. <el-button
  101. type="danger"
  102. size="small"
  103. @click="removeTempRelation(row.deviceIds)"
  104. >
  105. {{ t('fault.del') }}
  106. </el-button>
  107. </template>
  108. </el-table-column>
  109. </el-table>
  110. <div class="submit-area">
  111. <el-button
  112. type="primary"
  113. size="large"
  114. @click="submitRelations"
  115. :disabled="isSaveDisabled"
  116. >
  117. {{ t('iotMaintain.save') }}
  118. </el-button>
  119. </div>
  120. </div>
  121. </div>
  122. </div>
  123. </template>
  124. <script setup lang="ts">
  125. import { ref, computed } from 'vue'
  126. import { ElMessage } from 'element-plus'
  127. import {defaultProps, handleTree} from "@/utils/tree";
  128. import * as DeptApi from "@/api/system/dept";
  129. import {IotDeviceApi, IotDeviceVO} from "@/api/pms/device";
  130. import {IotDevicePersonApi, IotDevicePersonVO} from "@/api/pms/iotdeviceperson";
  131. import * as UserApi from "@/api/system/user";
  132. import {simpleUserList, UserVO} from "@/api/system/user";
  133. import { useRouter } from 'vue-router'
  134. const router = useRouter()
  135. const { t } = useI18n() // 国际化
  136. import { useTagsViewStore } from "@/store/modules/tagsView";
  137. defineOptions({ name: 'ConfigDevicePerson' })
  138. const simpleDevices = ref<IotDeviceVO[]>([])
  139. const simpleUsers = ref<UserVO[]>([])
  140. const deptList = ref<Tree[]>([]) // 树形结构
  141. const { delView } = useTagsViewStore() // 视图操作
  142. const formData = ref({
  143. id: undefined,
  144. deviceCode: undefined,
  145. deviceName: undefined,
  146. brand: undefined,
  147. model: undefined,
  148. deptId: undefined as string | undefined,
  149. deptId1: undefined as string | undefined,
  150. deviceStatus: undefined,
  151. assetProperty: undefined,
  152. reason: '',
  153. picUrl: undefined,
  154. })
  155. const queryParams = reactive({
  156. deptId1: formData.value.deptId1,
  157. deptId: formData.value.deptId,
  158. })
  159. const emit = defineEmits(['success', 'node-click']) // 定义 success 树点击 事件,用于操作成功后的回调
  160. // 响应式数据
  161. const selectedDevice = ref<number>(0)
  162. const selectedDevices = ref<number[]>([])
  163. const selectedDept = ref('')
  164. const selectedUsers = ref<number[]>([])
  165. const tempRelations = ref<Array<{
  166. deviceIds: number[]
  167. deviceNames: string
  168. userIds: number[]
  169. userNames: string
  170. reason: string
  171. }>>([])
  172. watch(selectedDevices, (newVal, oldVal) => {
  173. // 找出新增的设备
  174. const addedDevices = newVal.filter(id => !oldVal.includes(id));
  175. // 找出取消选择的设备
  176. const removedDevices = oldVal.filter(id => !newVal.includes(id))
  177. // 移除对应设备的关联记录
  178. tempRelations.value = tempRelations.value.filter(
  179. r => !removedDevices.includes(r.deviceIds[0])
  180. )
  181. // 为新增的设备创建关联记录
  182. if (addedDevices.length > 0) {
  183. const addedDeviceObjects = simpleDevices.value.filter(d =>
  184. addedDevices.includes(d.id)
  185. );
  186. addedDeviceObjects.forEach(device => {
  187. // 使用当前已选的责任人和调整原因
  188. updateDeviceRelation(device, selectedUsers.value);
  189. });
  190. }
  191. // 当没有选中设备时清空其他选项
  192. if (newVal.length === 0) {
  193. selectedUsers.value = []
  194. formData.value.reason = ''
  195. }
  196. })
  197. const handleUserSelectionChange = (userIds: number[]) => {
  198. if (selectedDevices.value.length === 0) {
  199. ElMessage.warning('请先选择设备')
  200. selectedUsers.value = []
  201. return
  202. }
  203. // 获取所有选中设备
  204. const devices = simpleDevices.value.filter(d =>
  205. selectedDevices.value.includes(d.id)
  206. )
  207. // 更新所有设备的关联关系
  208. devices.forEach(device => {
  209. updateDeviceRelation(device, userIds)
  210. })
  211. }
  212. /** 处理 部门-设备 树 被点击 */
  213. const handleDeptDeviceTreeNodeClick = async (row: { [key: string]: any }) => {
  214. emit('node-click', row)
  215. formData.value.deptId1 = row.id
  216. await getDeviceList()
  217. }
  218. // 新增计算属性:判断保存按钮是否禁用
  219. const isSaveDisabled = computed(() => {
  220. // 当没有调整记录或调整原因为空时禁用按钮
  221. return tempRelations.value.length === 0 || !formData.value.reason.trim();
  222. });
  223. /** 获得 部门下的设备 列表 */
  224. const getDeviceList = async () => {
  225. try {
  226. const params = { deptId: formData.value.deptId1 }
  227. const data = await IotDeviceApi.simpleDevices(params)
  228. simpleDevices.value = data || []
  229. } catch (error) {
  230. simpleDevices.value = []
  231. console.error('获取设备列表失败:', error)
  232. }
  233. }
  234. /** 处理 部门-人员 树 被点击 */
  235. const handleDeptUserTreeNodeClick = async (row: { [key: string]: any }) => {
  236. emit('node-click', row)
  237. formData.value.deptId = row.id
  238. await getUserList()
  239. }
  240. /** 获得 部门下的人员 列表 */
  241. const getUserList = async () => {
  242. try {
  243. const params = {
  244. deptId: formData.value.deptId,
  245. pageNo: 1,
  246. pageSize: 10 }
  247. const data = await UserApi.simpleUserList(params)
  248. simpleUsers.value = data || []
  249. } catch (error) {
  250. simpleUsers.value = []
  251. console.error('获取人员列表失败:', error)
  252. }
  253. }
  254. // 设备过滤文本
  255. const deviceFilterText = ref('')
  256. // 计算属性:过滤设备列表
  257. const filteredDevices = computed(() => {
  258. const searchText = deviceFilterText.value.toLowerCase().trim()
  259. if (!searchText) return simpleDevices.value
  260. return simpleDevices.value.filter(device => {
  261. return (
  262. (device.deviceCode || '').toLowerCase().includes(searchText) ||
  263. (device.deviceName || '').toLowerCase().includes(searchText)
  264. )
  265. })
  266. })
  267. // 新增输入处理方法
  268. const handleReasonInput = (value: string) => {
  269. formData.value.reason = value
  270. // 同步到所有已选设备的记录
  271. selectedDevices.value.forEach(deviceId => {
  272. const relation = tempRelations.value.find(
  273. r => r.deviceIds[0] === deviceId
  274. )
  275. if (relation) {
  276. relation.reason = value
  277. }
  278. })
  279. }
  280. // 更新设备关联关系
  281. const updateDeviceRelation = (device: IotDeviceVO, userIds: number[]) => {
  282. // 获取当前选择的人员
  283. const users = simpleUsers.value.filter(u => userIds.includes(u.id))
  284. const newRelation = {
  285. deviceIds: [device.id],
  286. deviceNames: `${device.deviceCode} (${device.deviceName})`,
  287. userIds: users.map(u => u.id),
  288. userNames: users.map(u => u.nickname).join(', '),
  289. reason: formData.value.reason
  290. }
  291. const existIndex = tempRelations.value.findIndex(
  292. // r => r.deviceIds[0] === device.id
  293. r => r.deviceIds.includes(device.id)
  294. )
  295. if (existIndex > -1) {
  296. tempRelations.value[existIndex] = newRelation
  297. } else {
  298. tempRelations.value.push(newRelation)
  299. }
  300. }
  301. const clearSelection = () => {
  302. selectedDevice.value = ''
  303. selectedUsers.value = []
  304. selectedDept.value = ''
  305. }
  306. const removeTempRelation = (deviceIds: number[]) => {
  307. tempRelations.value = tempRelations.value.filter(
  308. r => r.deviceIds.join() !== deviceIds.join()
  309. )
  310. // 同步取消勾选设备
  311. selectedDevices.value = selectedDevices.value.filter(
  312. id => !deviceIds.includes(id)
  313. )
  314. }
  315. const submitRelations = async () => {
  316. try {
  317. // 校验所有调整原因
  318. const hasEmptyReason = tempRelations.value.some(
  319. item => !item.reason?.trim()
  320. )
  321. if (hasEmptyReason) {
  322. ElMessage.error('请填写调整原因')
  323. return
  324. }
  325. // 转换为后端需要的格式
  326. const submitData = tempRelations.value.flatMap(relation => {
  327. return relation.deviceIds.map(deviceId => ({
  328. deviceId,
  329. userIds: relation.userIds,
  330. reason: relation.reason
  331. }))
  332. })
  333. await IotDevicePersonApi.saveDevicePersonRelation(submitData)
  334. // 模拟API调用
  335. ElMessage.success('提交成功')
  336. tempRelations.value = []
  337. delView(unref(router.currentRoute.value))
  338. router.back()
  339. } catch (error) {
  340. ElMessage.error('提交失败,请重试')
  341. }
  342. }
  343. /** 初始化 */
  344. onMounted(async () => {
  345. // 初始化部门树 根据选择的部门 查询设备列表 部门人员列表
  346. deptList.value = handleTree(await DeptApi.getSimpleDeptList())
  347. })
  348. </script>
  349. <style scoped>
  350. .equal-height-row {
  351. display: flex;
  352. align-items: stretch;
  353. }
  354. .container {
  355. padding: 20px;
  356. }
  357. .card {
  358. border: 1px solid #ebeef5;
  359. border-radius: 4px;
  360. padding: 20px;
  361. margin-bottom: 20px;
  362. background: white;
  363. box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
  364. transition: box-shadow 0.2s;
  365. }
  366. .list-item {
  367. padding: 8px 12px;
  368. border-bottom: 1px solid #f0f0f0;
  369. }
  370. .dept-select {
  371. margin-bottom: 20px;
  372. }
  373. .action-bar {
  374. text-align: right;
  375. }
  376. .temp-list {
  377. margin-top: 20px;
  378. }
  379. .submit-area {
  380. margin-top: 20px;
  381. text-align: center;
  382. }
  383. h3 {
  384. margin: 0 0 15px 0;
  385. color: #303133;
  386. }
  387. .radio-item {
  388. width: 100%;
  389. padding: 8px 12px;
  390. border-bottom: 1px solid #f0f0f0;
  391. }
  392. .radio-item .el-radio {
  393. width: 100%;
  394. height: 100%;
  395. }
  396. .radio-item .el-radio__label {
  397. display: block;
  398. white-space: nowrap;
  399. overflow: hidden;
  400. text-overflow: ellipsis;
  401. }
  402. .required-star {
  403. color: #ff4d4f;
  404. margin-left: 3px;
  405. vertical-align: middle;
  406. }
  407. .filter-input {
  408. margin-bottom: 15px;
  409. }
  410. .no-data {
  411. padding: 20px;
  412. text-align: center;
  413. color: #999;
  414. }
  415. </style>