|
@@ -1,528 +1,483 @@
|
|
|
<template>
|
|
<template>
|
|
|
- <div class="container">
|
|
|
|
|
- <el-row :gutter="20" class="equal-height-row">
|
|
|
|
|
- <!-- 左侧设备列表 -->
|
|
|
|
|
- <el-col :span="12">
|
|
|
|
|
- <div class="card">
|
|
|
|
|
- <h3>{{ t('configPerson.deviceList') }}</h3>
|
|
|
|
|
- <div class="dept-select">
|
|
|
|
|
- <el-tree-select
|
|
|
|
|
- clearable
|
|
|
|
|
- v-model="formData.deptId1"
|
|
|
|
|
- :data="deptList"
|
|
|
|
|
- :props="defaultProps"
|
|
|
|
|
- check-strictly
|
|
|
|
|
- node-key="id"
|
|
|
|
|
- filterable
|
|
|
|
|
- :placeholder="t('configPerson.deviceListHolder')"
|
|
|
|
|
- @node-click="handleDeptDeviceTreeNodeClick"
|
|
|
|
|
- @clear="handleClearDeptForDevice"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- <!-- 设备搜索框 -->
|
|
|
|
|
- <div class="filter-input">
|
|
|
|
|
- <el-input
|
|
|
|
|
- v-model="deviceFilterText"
|
|
|
|
|
- :placeholder="t('devicePerson.filterDevicePlaceholder')"
|
|
|
|
|
- :disabled="!devicesLoaded"
|
|
|
|
|
- clearable
|
|
|
|
|
- prefix-icon="Search"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <div class="person-config-page">
|
|
|
|
|
+ <div class="selection-grid">
|
|
|
|
|
+ <section class="panel">
|
|
|
|
|
+ <div class="panel-header">
|
|
|
|
|
+ <div class="panel-title">
|
|
|
|
|
+ <span class="panel-marker"></span>
|
|
|
|
|
+ <span>{{ t('configPerson.deviceList') }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <el-tag size="small" type="info">{{ filteredDevices.length }} 台</el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <el-scrollbar height="450px">
|
|
|
|
|
- <el-checkbox-group v-model="selectedDevices">
|
|
|
|
|
- <div
|
|
|
|
|
- v-for="device in filteredDevices"
|
|
|
|
|
- :key="device.id"
|
|
|
|
|
- class="radio-item"
|
|
|
|
|
- >
|
|
|
|
|
- <el-checkbox :label="device.id">
|
|
|
|
|
- {{ device.deviceCode }} ({{ device.deviceName }}) - {{ device.devicePersons }}
|
|
|
|
|
- </el-checkbox>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-checkbox-group>
|
|
|
|
|
- </el-scrollbar>
|
|
|
|
|
|
|
+ <div class="panel-toolbar">
|
|
|
|
|
+ <el-tree-select
|
|
|
|
|
+ v-model="formData.deptId1"
|
|
|
|
|
+ :data="deptList"
|
|
|
|
|
+ :props="defaultProps"
|
|
|
|
|
+ check-strictly
|
|
|
|
|
+ node-key="id"
|
|
|
|
|
+ filterable
|
|
|
|
|
+ clearable
|
|
|
|
|
+ size="default"
|
|
|
|
|
+ class="toolbar-control"
|
|
|
|
|
+ :placeholder="t('configPerson.deviceListHolder')"
|
|
|
|
|
+ @node-click="handleDeptDeviceTreeNodeClick"
|
|
|
|
|
+ @clear="handleClearDeptForDevice" />
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="deviceFilterText"
|
|
|
|
|
+ :placeholder="t('devicePerson.filterDevicePlaceholder')"
|
|
|
|
|
+ :disabled="!devicesLoaded"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ size="default"
|
|
|
|
|
+ class="toolbar-control">
|
|
|
|
|
+ <template #prefix>
|
|
|
|
|
+ <Icon icon="ep:search" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-input>
|
|
|
</div>
|
|
</div>
|
|
|
- </el-col>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 右侧责任人选择 -->
|
|
|
|
|
- <el-col :span="12">
|
|
|
|
|
- <div class="card">
|
|
|
|
|
- <h3>{{ t('configPerson.rp') }}</h3>
|
|
|
|
|
- <div class="dept-select">
|
|
|
|
|
- <el-tree-select
|
|
|
|
|
- clearable
|
|
|
|
|
- v-model="formData.deptIds"
|
|
|
|
|
- :data="deptList"
|
|
|
|
|
- :props="defaultProps"
|
|
|
|
|
- multiple
|
|
|
|
|
- show-checkbox
|
|
|
|
|
- check-strictly
|
|
|
|
|
- node-key="id"
|
|
|
|
|
- filterable
|
|
|
|
|
- :placeholder="t('configPerson.rpHolder')"
|
|
|
|
|
- @node-click="handleDeptCheckChange"
|
|
|
|
|
- @clear="handleClearDeptForPerson"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
|
|
|
- <!-- 责任人搜索框 -->
|
|
|
|
|
- <div class="filter-input">
|
|
|
|
|
- <el-input
|
|
|
|
|
- v-model="userFilterText"
|
|
|
|
|
- :placeholder="t('devicePerson.filterUserPlaceholder')"
|
|
|
|
|
- :disabled="!personLoaded"
|
|
|
|
|
- clearable
|
|
|
|
|
- prefix-icon="Search"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <el-scrollbar class="option-scroll">
|
|
|
|
|
+ <el-checkbox-group v-model="selectedDevices" class="option-list">
|
|
|
|
|
+ <el-checkbox
|
|
|
|
|
+ v-for="device in filteredDevices"
|
|
|
|
|
+ :key="device.id"
|
|
|
|
|
+ :value="device.id"
|
|
|
|
|
+ class="option-item"
|
|
|
|
|
+ :class="{ 'is-selected': selectedDevices.includes(device.id) }">
|
|
|
|
|
+ <span class="option-content">
|
|
|
|
|
+ <span class="option-main">{{ device.deviceCode || '暂无编码' }}</span>
|
|
|
|
|
+ <span class="option-sub">{{ device.deviceName || '暂无名称' }}</span>
|
|
|
|
|
+ <span class="option-extra">当前责任人:{{ device.devicePersons || '暂无' }}</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </el-checkbox>
|
|
|
|
|
+ </el-checkbox-group>
|
|
|
|
|
+ <el-empty
|
|
|
|
|
+ v-if="devicesLoaded && filteredDevices.length === 0"
|
|
|
|
|
+ :image-size="92"
|
|
|
|
|
+ description="暂无设备" />
|
|
|
|
|
+ </el-scrollbar>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <section class="panel">
|
|
|
|
|
+ <div class="panel-header">
|
|
|
|
|
+ <div class="panel-title">
|
|
|
|
|
+ <span class="panel-marker"></span>
|
|
|
|
|
+ <span>{{ t('configPerson.rp') }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <el-tag size="small" type="info">{{ filteredUsers.length }} 人</el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="panel-toolbar">
|
|
|
|
|
+ <el-tree-select
|
|
|
|
|
+ v-model="formData.deptIds"
|
|
|
|
|
+ :data="deptList"
|
|
|
|
|
+ :props="defaultProps"
|
|
|
|
|
+ multiple
|
|
|
|
|
+ show-checkbox
|
|
|
|
|
+ check-strictly
|
|
|
|
|
+ node-key="id"
|
|
|
|
|
+ filterable
|
|
|
|
|
+ clearable
|
|
|
|
|
+ collapse-tags
|
|
|
|
|
+ collapse-tags-tooltip
|
|
|
|
|
+ :max-collapse-tags="8"
|
|
|
|
|
+ popper-class="dept-tags-popper"
|
|
|
|
|
+ size="default"
|
|
|
|
|
+ class="toolbar-control dept-tags-select"
|
|
|
|
|
+ :placeholder="t('configPerson.rpHolder')"
|
|
|
|
|
+ @change="handlePersonDeptChange"
|
|
|
|
|
+ @clear="handleClearDeptForPerson" />
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="userFilterText"
|
|
|
|
|
+ :placeholder="t('devicePerson.filterUserPlaceholder')"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ size="default"
|
|
|
|
|
+ class="toolbar-control">
|
|
|
|
|
+ <template #prefix>
|
|
|
|
|
+ <Icon icon="ep:search" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-input>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <el-scrollbar height="450px">
|
|
|
|
|
- <el-checkbox-group v-model="selectedUsers" @change="handleUserSelectionChange">
|
|
|
|
|
- <div
|
|
|
|
|
- v-for="user in filteredUsers"
|
|
|
|
|
- :key="user.id"
|
|
|
|
|
- class="list-item"
|
|
|
|
|
- >
|
|
|
|
|
- <el-checkbox :label="user.id">
|
|
|
|
|
- {{ user.nickname }} ({{ user.deptName }})
|
|
|
|
|
- </el-checkbox>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-checkbox-group>
|
|
|
|
|
- </el-scrollbar>
|
|
|
|
|
|
|
+ <el-scrollbar class="option-scroll">
|
|
|
|
|
+ <el-checkbox-group
|
|
|
|
|
+ v-model="selectedUsers"
|
|
|
|
|
+ class="option-list"
|
|
|
|
|
+ @change="handleUserSelectionChange">
|
|
|
|
|
+ <el-checkbox
|
|
|
|
|
+ v-for="user in filteredUsers"
|
|
|
|
|
+ :key="user.id"
|
|
|
|
|
+ :value="user.id"
|
|
|
|
|
+ class="option-item"
|
|
|
|
|
+ :class="{ 'is-selected': selectedUsers.includes(user.id) }">
|
|
|
|
|
+ <span class="option-content">
|
|
|
|
|
+ <span class="option-main">{{ user.nickname || user.username }}</span>
|
|
|
|
|
+ <span class="option-sub">{{ user.deptName || '暂无部门' }}</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </el-checkbox>
|
|
|
|
|
+ </el-checkbox-group>
|
|
|
|
|
+ <el-empty
|
|
|
|
|
+ v-if="personLoaded && filteredUsers.length === 0"
|
|
|
|
|
+ :image-size="92"
|
|
|
|
|
+ description="暂无责任人" />
|
|
|
|
|
+ </el-scrollbar>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <section class="panel reason-panel">
|
|
|
|
|
+ <div class="panel-header">
|
|
|
|
|
+ <div class="panel-title">
|
|
|
|
|
+ <span class="panel-marker"></span>
|
|
|
|
|
+ <span>{{ t('configPerson.reasonForAdjustment') }}</span>
|
|
|
|
|
+ <span class="required-star">*</span>
|
|
|
</div>
|
|
</div>
|
|
|
- </el-col>
|
|
|
|
|
- </el-row>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 暂存关联列表 -->
|
|
|
|
|
- <div class="submit-area">
|
|
|
|
|
- <div class="card">
|
|
|
|
|
- <h3>{{ t('configPerson.reasonForAdjustment') }}<span class="required-star">*</span></h3>
|
|
|
|
|
- <el-input
|
|
|
|
|
- v-model="formData.reason"
|
|
|
|
|
- :placeholder="t('configPerson.rfaHolder')"
|
|
|
|
|
- class="reason-input"
|
|
|
|
|
- type="textarea"
|
|
|
|
|
- :rows="3"
|
|
|
|
|
- @input="handleReasonInput"
|
|
|
|
|
- :disabled="selectedDevices.length === 0 || selectedUsers.length === 0"
|
|
|
|
|
- />
|
|
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="temp-list card">
|
|
|
|
|
- <h3>{{ t('configPerson.adjustmentRecords') }}</h3>
|
|
|
|
|
- <el-table :data="tempRelations" style="width: 100%">
|
|
|
|
|
- <el-table-column prop="deviceNames" :label="t('devicePerson.deviceName')" width="200" />
|
|
|
|
|
- <el-table-column prop="userNames" :label="t('devicePerson.rp')" />
|
|
|
|
|
- <el-table-column prop="reason" :label="t('configPerson.reasonForAdjustment')" />
|
|
|
|
|
- <el-table-column :label="t('devicePerson.operation')" width="120">
|
|
|
|
|
- <template #default="{ row }">
|
|
|
|
|
- <el-button
|
|
|
|
|
- type="danger"
|
|
|
|
|
- size="small"
|
|
|
|
|
- @click="removeTempRelation(row.deviceIds)"
|
|
|
|
|
- >
|
|
|
|
|
- {{ t('fault.del') }}
|
|
|
|
|
- </el-button>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-table-column>
|
|
|
|
|
- </el-table>
|
|
|
|
|
-
|
|
|
|
|
- <div class="submit-area">
|
|
|
|
|
- <el-button
|
|
|
|
|
- type="primary"
|
|
|
|
|
- size="large"
|
|
|
|
|
- @click="submitRelations"
|
|
|
|
|
- :disabled="isSaveDisabled"
|
|
|
|
|
- >
|
|
|
|
|
- {{ t('iotMaintain.save') }}
|
|
|
|
|
- </el-button>
|
|
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="formData.reason"
|
|
|
|
|
+ :placeholder="t('configPerson.rfaHolder')"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :rows="4"
|
|
|
|
|
+ resize="none"
|
|
|
|
|
+ size="default"
|
|
|
|
|
+ :disabled="selectedDevices.length === 0 || selectedUsers.length === 0"
|
|
|
|
|
+ @input="handleReasonInput" />
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <section class="panel record-panel">
|
|
|
|
|
+ <div class="panel-header">
|
|
|
|
|
+ <div class="panel-title">
|
|
|
|
|
+ <span class="panel-marker"></span>
|
|
|
|
|
+ <span>{{ t('configPerson.adjustmentRecords') }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ size="default"
|
|
|
|
|
+ :disabled="isSaveDisabled"
|
|
|
|
|
+ @click="submitRelations">
|
|
|
|
|
+ <Icon icon="ep:check" class="mr-5px" />
|
|
|
|
|
+ {{ t('iotMaintain.save') }}
|
|
|
|
|
+ </el-button>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <ZmTable
|
|
|
|
|
+ :data="tempRelations"
|
|
|
|
|
+ :loading="false"
|
|
|
|
|
+ :show-border="true"
|
|
|
|
|
+ settings-cache-key="pms-device-person-config-record"
|
|
|
|
|
+ class="record-table">
|
|
|
|
|
+ <ZmTableColumn prop="deviceNames" :label="t('devicePerson.deviceName')" min-width="240" />
|
|
|
|
|
+ <ZmTableColumn prop="userNames" :label="t('devicePerson.rp')" min-width="220">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <span>{{ row.userNames || '未选择责任人' }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </ZmTableColumn>
|
|
|
|
|
+ <ZmTableColumn
|
|
|
|
|
+ prop="reason"
|
|
|
|
|
+ :label="t('configPerson.reasonForAdjustment')"
|
|
|
|
|
+ min-width="280"
|
|
|
|
|
+ align="left">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <span>{{ row.reason || '未填写调整原因' }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </ZmTableColumn>
|
|
|
|
|
+ <ZmTableColumn :label="t('devicePerson.operation')" width="120" fixed="right" action>
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-button type="danger" link size="default" @click="removeTempRelation(row.deviceIds)">
|
|
|
|
|
+ <Icon icon="ep:delete" class="mr-4px" />
|
|
|
|
|
+ {{ t('fault.del') }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </ZmTableColumn>
|
|
|
|
|
+ </ZmTable>
|
|
|
|
|
+ </section>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-import { ref, computed, onMounted } from 'vue'
|
|
|
|
|
|
|
+import { computed, onMounted, reactive, ref, unref, watch } from 'vue'
|
|
|
|
|
+import { useDebounceFn } from '@vueuse/core'
|
|
|
import { ElMessage } from 'element-plus'
|
|
import { ElMessage } from 'element-plus'
|
|
|
-import {defaultProps, handleTree} from "@/utils/tree";
|
|
|
|
|
-import * as DeptApi from "@/api/system/dept";
|
|
|
|
|
-import {IotDeviceApi, IotDeviceVO} from "@/api/pms/device";
|
|
|
|
|
-import {IotDevicePersonApi, IotDevicePersonVO} from "@/api/pms/iotdeviceperson";
|
|
|
|
|
-import * as UserApi from "@/api/system/user";
|
|
|
|
|
-import {simpleUserList, UserVO} from "@/api/system/user";
|
|
|
|
|
-import { useRouter, useRoute } from 'vue-router'
|
|
|
|
|
-const router = useRouter()
|
|
|
|
|
-const route = useRoute() // 获取路由参数
|
|
|
|
|
-const { t } = useI18n() // 国际化
|
|
|
|
|
-import { useTagsViewStore } from "@/store/modules/tagsView";
|
|
|
|
|
|
|
+import { useRoute, useRouter } from 'vue-router'
|
|
|
|
|
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
|
|
|
|
|
+import * as DeptApi from '@/api/system/dept'
|
|
|
|
|
+import * as UserApi from '@/api/system/user'
|
|
|
|
|
+import type { UserVO } from '@/api/system/user'
|
|
|
|
|
+import { IotDeviceApi, type IotDeviceVO } from '@/api/pms/device'
|
|
|
|
|
+import { IotDevicePersonApi } from '@/api/pms/iotdeviceperson'
|
|
|
|
|
+import { useTagsViewStore } from '@/store/modules/tagsView'
|
|
|
|
|
+import { defaultProps, handleTree } from '@/utils/tree'
|
|
|
|
|
|
|
|
defineOptions({ name: 'ConfigDevicePerson' })
|
|
defineOptions({ name: 'ConfigDevicePerson' })
|
|
|
|
|
|
|
|
-const simpleDevices = ref<IotDeviceVO[]>([])
|
|
|
|
|
-const simpleUsers = ref<UserVO[]>([])
|
|
|
|
|
-
|
|
|
|
|
-const deptList = ref<Tree[]>([]) // 树形结构
|
|
|
|
|
-const { delView } = useTagsViewStore() // 视图操作
|
|
|
|
|
-
|
|
|
|
|
-const formData = ref({
|
|
|
|
|
- id: undefined,
|
|
|
|
|
- deviceCode: undefined,
|
|
|
|
|
- deviceName: undefined,
|
|
|
|
|
- brand: undefined,
|
|
|
|
|
- model: undefined,
|
|
|
|
|
- deptId: undefined as string | undefined,
|
|
|
|
|
- deptId1: undefined as string | undefined,
|
|
|
|
|
- deviceStatus: undefined,
|
|
|
|
|
- assetProperty: undefined,
|
|
|
|
|
- reason: '',
|
|
|
|
|
- picUrl: undefined,
|
|
|
|
|
- deptIds: [] as number[], // 改为部门ID数组
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-const queryParams = reactive({
|
|
|
|
|
- deptId1: formData.value.deptId1,
|
|
|
|
|
- deptId: formData.value.deptId,
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-const emit = defineEmits(['success', 'node-click']) // 定义 success 树点击 事件,用于操作成功后的回调
|
|
|
|
|
-
|
|
|
|
|
-// 响应式数据
|
|
|
|
|
-const selectedDevices = ref<number[]>([])
|
|
|
|
|
-// 责任人过滤文本
|
|
|
|
|
-const userFilterText = ref('')
|
|
|
|
|
-const selectedUsers = ref<number[]>([])
|
|
|
|
|
-const tempRelations = ref<Array<{
|
|
|
|
|
|
|
+type TempRelation = {
|
|
|
deviceIds: number[]
|
|
deviceIds: number[]
|
|
|
deviceNames: string
|
|
deviceNames: string
|
|
|
userIds: number[]
|
|
userIds: number[]
|
|
|
userNames: string
|
|
userNames: string
|
|
|
reason: string
|
|
reason: string
|
|
|
-}>>([])
|
|
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-watch(selectedDevices, async (newVal, oldVal) => {
|
|
|
|
|
- // 找出新增的设备
|
|
|
|
|
- const addedDevices = newVal.filter(id => !oldVal.includes(id));
|
|
|
|
|
- // 找出取消选择的设备
|
|
|
|
|
- const removedDevices = oldVal.filter(id => !newVal.includes(id))
|
|
|
|
|
|
|
+type ConfigFormData = {
|
|
|
|
|
+ deptId1?: number
|
|
|
|
|
+ deptIds: number[]
|
|
|
|
|
+ reason: string
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // 移除对应设备的关联记录
|
|
|
|
|
- tempRelations.value = tempRelations.value.filter(
|
|
|
|
|
- r => !removedDevices.includes(r.deviceIds[0])
|
|
|
|
|
- )
|
|
|
|
|
|
|
+const router = useRouter()
|
|
|
|
|
+const route = useRoute()
|
|
|
|
|
+const { t } = useI18n()
|
|
|
|
|
+const { delView } = useTagsViewStore()
|
|
|
|
|
+const { ZmTable, ZmTableColumn } = useTableComponents<TempRelation>()
|
|
|
|
|
|
|
|
- // 为新增的设备创建关联记录
|
|
|
|
|
- if (addedDevices.length > 0) {
|
|
|
|
|
- const addedDeviceObjects = simpleDevices.value.filter(d =>
|
|
|
|
|
- addedDevices.includes(d.id)
|
|
|
|
|
- );
|
|
|
|
|
- addedDeviceObjects.forEach(device => {
|
|
|
|
|
- // 使用当前已选的责任人和调整原因
|
|
|
|
|
- updateDeviceRelation(device, selectedUsers.value);
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const deptList = ref<Tree[]>([])
|
|
|
|
|
+const simpleDevices = ref<IotDeviceVO[]>([])
|
|
|
|
|
+const simpleUsers = ref<UserVO[]>([])
|
|
|
|
|
+const selectedDevices = ref<number[]>([])
|
|
|
|
|
+const selectedUsers = ref<number[]>([])
|
|
|
|
|
+const tempRelations = ref<TempRelation[]>([])
|
|
|
|
|
+const deviceFilterText = ref('')
|
|
|
|
|
+const userFilterText = ref('')
|
|
|
|
|
+const devicesLoaded = ref(false)
|
|
|
|
|
+const personLoaded = ref(false)
|
|
|
|
|
|
|
|
- // 当没有选中设备时清空其他选项
|
|
|
|
|
- if (newVal.length === 0) {
|
|
|
|
|
- selectedUsers.value = []
|
|
|
|
|
- formData.value.reason = ''
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const formData = reactive<ConfigFormData>({
|
|
|
|
|
+ deptId1: undefined,
|
|
|
|
|
+ deptIds: [],
|
|
|
|
|
+ reason: ''
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
- // 获取设备部门ID集合
|
|
|
|
|
- const deptIdSet = new Set<number>()
|
|
|
|
|
- newVal.forEach(deviceId => {
|
|
|
|
|
- const device = simpleDevices.value.find(d => d.id === deviceId)
|
|
|
|
|
- if (device?.deptId) deptIdSet.add(device.deptId)
|
|
|
|
|
|
|
+const filteredDevices = computed(() => {
|
|
|
|
|
+ const searchText = deviceFilterText.value.toLowerCase().trim()
|
|
|
|
|
+ if (!searchText) return simpleDevices.value
|
|
|
|
|
+
|
|
|
|
|
+ return simpleDevices.value.filter((device) => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ (device.deviceCode || '').toLowerCase().includes(searchText) ||
|
|
|
|
|
+ (device.deviceName || '').toLowerCase().includes(searchText) ||
|
|
|
|
|
+ (device.devicePersons || '').toLowerCase().includes(searchText)
|
|
|
|
|
+ )
|
|
|
})
|
|
})
|
|
|
- formData.value.deptIds = Array.from(deptIdSet)
|
|
|
|
|
- // 部门更新后自动加载人员
|
|
|
|
|
- await getUserList()
|
|
|
|
|
-}, { deep: true })
|
|
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const filteredUsers = computed(() => {
|
|
|
|
|
+ const searchText = userFilterText.value.toLowerCase().trim()
|
|
|
|
|
+ if (!searchText) return simpleUsers.value
|
|
|
|
|
+
|
|
|
|
|
+ return simpleUsers.value.filter((user) => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ (user.nickname || '').toLowerCase().includes(searchText) ||
|
|
|
|
|
+ (user.username || '').toLowerCase().includes(searchText) ||
|
|
|
|
|
+ (user.deptName || '').toLowerCase().includes(searchText)
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const selectedUserObjects = computed(() => {
|
|
|
|
|
+ return simpleUsers.value.filter((user) => selectedUsers.value.includes(user.id))
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const isSaveDisabled = computed(() => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ tempRelations.value.length === 0 ||
|
|
|
|
|
+ selectedUsers.value.length === 0 ||
|
|
|
|
|
+ tempRelations.value.some((item) => !item.reason?.trim() || item.userIds.length === 0)
|
|
|
|
|
+ )
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
-// 监听部门ID变化 部门变化后重新筛选用户
|
|
|
|
|
watch(
|
|
watch(
|
|
|
- () => formData.value.deptIds,
|
|
|
|
|
- async (newVal, oldVal) => {
|
|
|
|
|
- // 找出被删除的部门
|
|
|
|
|
- const deletedDeptIds = oldVal?.filter(id => !newVal?.includes(id)) || []
|
|
|
|
|
-
|
|
|
|
|
- if (deletedDeptIds.length > 0) {
|
|
|
|
|
- // 找出被删除部门下的用户ID
|
|
|
|
|
- const usersToRemove = simpleUsers.value
|
|
|
|
|
- .filter(user => deletedDeptIds.includes(user.deptId))
|
|
|
|
|
- .map(user => user.id)
|
|
|
|
|
-
|
|
|
|
|
- // 同步更新已选用户
|
|
|
|
|
- selectedUsers.value = selectedUsers.value.filter(
|
|
|
|
|
- id => !usersToRemove.includes(id)
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- // 更新关联记录
|
|
|
|
|
- tempRelations.value = tempRelations.value.map(relation => {
|
|
|
|
|
- const validUserIds = relation.userIds.filter(
|
|
|
|
|
- id => !usersToRemove.includes(id)
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- // 获取有效用户名称
|
|
|
|
|
- const validUsers = simpleUsers.value.filter(
|
|
|
|
|
- u => validUserIds.includes(u.id)
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- ...relation,
|
|
|
|
|
- userIds: validUserIds,
|
|
|
|
|
- userNames: validUsers.map(u => u.nickname).join(', ')
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ selectedDevices,
|
|
|
|
|
+ (newVal, oldVal) => {
|
|
|
|
|
+ const removedDeviceIds = oldVal.filter((id) => !newVal.includes(id))
|
|
|
|
|
+ tempRelations.value = tempRelations.value.filter(
|
|
|
|
|
+ (relation) => !removedDeviceIds.includes(relation.deviceIds[0])
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if (newVal.length === 0) {
|
|
|
|
|
+ selectedUsers.value = []
|
|
|
|
|
+ formData.deptIds = []
|
|
|
|
|
+ formData.reason = ''
|
|
|
|
|
+ simpleUsers.value = []
|
|
|
|
|
+ personLoaded.value = false
|
|
|
|
|
+ tempRelations.value = []
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 重新加载人员列表
|
|
|
|
|
- await getUserList()
|
|
|
|
|
|
|
+ syncPersonDeptBySelectedDevices()
|
|
|
|
|
+ syncRelations()
|
|
|
|
|
+ void debouncedGetUserList()
|
|
|
},
|
|
},
|
|
|
- { deep: true } // 深度监听数组变化
|
|
|
|
|
|
|
+ { deep: true }
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
-const handleUserSelectionChange = (userIds: number[]) => {
|
|
|
|
|
- // 处理清空责任人的情况
|
|
|
|
|
- if (userIds.length === 0) {
|
|
|
|
|
- tempRelations.value.forEach(relation => {
|
|
|
|
|
- relation.userIds = []
|
|
|
|
|
- relation.userNames = ''
|
|
|
|
|
- })
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (selectedDevices.value.length === 0) {
|
|
|
|
|
- ElMessage.warning('请先选择设备')
|
|
|
|
|
- selectedUsers.value = []
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 获取所有选中设备
|
|
|
|
|
- const devices = simpleDevices.value.filter(d =>
|
|
|
|
|
- selectedDevices.value.includes(d.id)
|
|
|
|
|
- )
|
|
|
|
|
|
|
+const getDeviceLabel = (device: IotDeviceVO) => {
|
|
|
|
|
+ return `${device.deviceCode || '暂无编码'}(${device.deviceName || '暂无名称'})`
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // 更新所有设备的关联关系
|
|
|
|
|
- devices.forEach(device => {
|
|
|
|
|
- updateDeviceRelation(device, userIds)
|
|
|
|
|
- })
|
|
|
|
|
|
|
+const getUserNames = () => {
|
|
|
|
|
+ return selectedUserObjects.value.map((user) => user.nickname || user.username).join('、')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 处理 部门-设备 树 被点击 */
|
|
|
|
|
-const handleDeptDeviceTreeNodeClick = async (row: { [key: string]: any }) => {
|
|
|
|
|
- emit('node-click', row)
|
|
|
|
|
- formData.value.deptId1 = row.id
|
|
|
|
|
- await getDeviceList()
|
|
|
|
|
|
|
+const syncRelations = () => {
|
|
|
|
|
+ const userIds = [...selectedUsers.value]
|
|
|
|
|
+ const userNames = getUserNames()
|
|
|
|
|
+ const selectedDeviceSet = new Set(selectedDevices.value)
|
|
|
|
|
+
|
|
|
|
|
+ tempRelations.value = simpleDevices.value
|
|
|
|
|
+ .filter((device) => selectedDeviceSet.has(device.id))
|
|
|
|
|
+ .map((device) => ({
|
|
|
|
|
+ deviceIds: [device.id],
|
|
|
|
|
+ deviceNames: getDeviceLabel(device),
|
|
|
|
|
+ userIds,
|
|
|
|
|
+ userNames,
|
|
|
|
|
+ reason: formData.reason
|
|
|
|
|
+ }))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 新增计算属性:判断保存按钮是否禁用
|
|
|
|
|
-const isSaveDisabled = computed(() => {
|
|
|
|
|
- // 当没有调整记录或调整原因为空时禁用按钮
|
|
|
|
|
- return tempRelations.value.length === 0 || !formData.value.reason.trim();
|
|
|
|
|
-});
|
|
|
|
|
|
|
+const syncPersonDeptBySelectedDevices = () => {
|
|
|
|
|
+ const deptIdSet = new Set<number>()
|
|
|
|
|
+ selectedDevices.value.forEach((deviceId) => {
|
|
|
|
|
+ const device = simpleDevices.value.find((item) => item.id === deviceId)
|
|
|
|
|
+ if (device?.deptId) {
|
|
|
|
|
+ deptIdSet.add(device.deptId)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ formData.deptIds = Array.from(deptIdSet)
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-/** 获得 部门下的设备 列表 */
|
|
|
|
|
const getDeviceList = async () => {
|
|
const getDeviceList = async () => {
|
|
|
|
|
+ devicesLoaded.value = false
|
|
|
try {
|
|
try {
|
|
|
- // 查询到数据后才能进行搜索 筛选
|
|
|
|
|
- devicesLoaded.value = false
|
|
|
|
|
-
|
|
|
|
|
- const params = { deptId: formData.value.deptId1 }
|
|
|
|
|
- const data = await IotDeviceApi.simpleDevices(params)
|
|
|
|
|
|
|
+ const data = await IotDeviceApi.simpleDevices({ deptId: formData.deptId1 })
|
|
|
simpleDevices.value = data || []
|
|
simpleDevices.value = data || []
|
|
|
-
|
|
|
|
|
- devicesLoaded.value = true
|
|
|
|
|
|
|
+ selectedDevices.value = selectedDevices.value.filter((id) =>
|
|
|
|
|
+ simpleDevices.value.some((device) => device.id === id)
|
|
|
|
|
+ )
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
simpleDevices.value = []
|
|
simpleDevices.value = []
|
|
|
- // 即使失败也启用搜索框(避免卡在禁用状态)
|
|
|
|
|
|
|
+ ElMessage.error('获取设备列表失败')
|
|
|
|
|
+ } finally {
|
|
|
devicesLoaded.value = true
|
|
devicesLoaded.value = true
|
|
|
- console.error('获取设备列表失败:', error)
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 处理 部门-人员 树 被点击 */
|
|
|
|
|
-const handleDeptUserTreeNodeClick = async (row: { [key: string]: any }) => {
|
|
|
|
|
- emit('node-click', row)
|
|
|
|
|
- formData.value.deptId = row.id
|
|
|
|
|
- await getUserList()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/** 获得 部门下的人员 列表 */
|
|
|
|
|
const getUserList = async () => {
|
|
const getUserList = async () => {
|
|
|
- try {
|
|
|
|
|
- // 开始加载时禁用搜索框
|
|
|
|
|
- personLoaded.value = false
|
|
|
|
|
-
|
|
|
|
|
- // 保留当前选中但不在被删除部门的用户
|
|
|
|
|
- const currentDeptIds = formData.value.deptIds || []
|
|
|
|
|
- const validUserIds = simpleUsers.value
|
|
|
|
|
- .filter(user => currentDeptIds.includes(user.deptId))
|
|
|
|
|
- .map(user => user.id)
|
|
|
|
|
|
|
+ const deptIds = formData.deptIds || []
|
|
|
|
|
|
|
|
- selectedUsers.value = selectedUsers.value.filter(
|
|
|
|
|
- id => validUserIds.includes(id)
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ if (!deptIds.length) {
|
|
|
|
|
+ simpleUsers.value = []
|
|
|
|
|
+ selectedUsers.value = []
|
|
|
|
|
+ personLoaded.value = true
|
|
|
|
|
+ syncRelations()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const params = {
|
|
|
|
|
- deptIds: formData.value.deptIds,
|
|
|
|
|
- // deptId: formData.value.deptId,
|
|
|
|
|
|
|
+ personLoaded.value = false
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = await UserApi.simpleUserList({
|
|
|
|
|
+ deptIds,
|
|
|
pageNo: 1,
|
|
pageNo: 1,
|
|
|
- pageSize: 10 }
|
|
|
|
|
- const data = await UserApi.simpleUserList(params)
|
|
|
|
|
|
|
+ pageSize: 100
|
|
|
|
|
+ } as any)
|
|
|
simpleUsers.value = data || []
|
|
simpleUsers.value = data || []
|
|
|
-
|
|
|
|
|
- personLoaded.value = true
|
|
|
|
|
|
|
+ selectedUsers.value = selectedUsers.value.filter((id) =>
|
|
|
|
|
+ simpleUsers.value.some((user) => user.id === id)
|
|
|
|
|
+ )
|
|
|
|
|
+ syncRelations()
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
simpleUsers.value = []
|
|
simpleUsers.value = []
|
|
|
- // 即使失败也启用搜索框(避免卡在禁用状态)
|
|
|
|
|
|
|
+ selectedUsers.value = []
|
|
|
|
|
+ ElMessage.error('获取责任人列表失败')
|
|
|
|
|
+ } finally {
|
|
|
personLoaded.value = true
|
|
personLoaded.value = true
|
|
|
- console.error('获取人员列表失败:', error)
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 设备加载状态标记 只有加载完设备才能通过文本框筛选
|
|
|
|
|
-const devicesLoaded = ref(false)
|
|
|
|
|
|
|
+const debouncedGetUserList = useDebounceFn(async () => {
|
|
|
|
|
+ if (!selectedDevices.value.length) return
|
|
|
|
|
+ await getUserList()
|
|
|
|
|
+}, 600)
|
|
|
|
|
|
|
|
-// 人员加载状态标记 只有加载完人员才能通过文本框筛选
|
|
|
|
|
-const personLoaded = ref(false)
|
|
|
|
|
|
|
+const handleDeptDeviceTreeNodeClick = async (row: Tree) => {
|
|
|
|
|
+ formData.deptId1 = row.id
|
|
|
|
|
+ selectedDevices.value = []
|
|
|
|
|
+ deviceFilterText.value = ''
|
|
|
|
|
+ await getDeviceList()
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-// 设备过滤文本
|
|
|
|
|
-const deviceFilterText = ref('')
|
|
|
|
|
|
|
+const handlePersonDeptChange = async () => {
|
|
|
|
|
+ userFilterText.value = ''
|
|
|
|
|
+ await getUserList()
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-// 计算属性:过滤设备列表
|
|
|
|
|
-const filteredDevices = computed(() => {
|
|
|
|
|
- const searchText = deviceFilterText.value.toLowerCase().trim()
|
|
|
|
|
- if (!searchText) return simpleDevices.value
|
|
|
|
|
|
|
+const handleUserSelectionChange = (userIds: number[]) => {
|
|
|
|
|
+ if (!selectedDevices.value.length) {
|
|
|
|
|
+ ElMessage.warning('请先选择设备')
|
|
|
|
|
+ selectedUsers.value = []
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return simpleDevices.value.filter(device => {
|
|
|
|
|
- return (
|
|
|
|
|
- (device.deviceCode || '').toLowerCase().includes(searchText) ||
|
|
|
|
|
- (device.deviceName || '').toLowerCase().includes(searchText)
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
-})
|
|
|
|
|
|
|
+ selectedUsers.value = userIds
|
|
|
|
|
+ syncRelations()
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-// 新增输入处理方法
|
|
|
|
|
const handleReasonInput = (value: string) => {
|
|
const handleReasonInput = (value: string) => {
|
|
|
- formData.value.reason = value
|
|
|
|
|
- // 同步到所有已选设备的记录
|
|
|
|
|
- selectedDevices.value.forEach(deviceId => {
|
|
|
|
|
- const relation = tempRelations.value.find(
|
|
|
|
|
- r => r.deviceIds[0] === deviceId
|
|
|
|
|
- )
|
|
|
|
|
- if (relation) {
|
|
|
|
|
- relation.reason = value
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ formData.reason = value
|
|
|
|
|
+ syncRelations()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 更新设备关联关系
|
|
|
|
|
-const updateDeviceRelation = (device: IotDeviceVO, userIds: number[]) => {
|
|
|
|
|
- // 获取当前选择的人员
|
|
|
|
|
- const users = simpleUsers.value.filter(u => userIds.includes(u.id))
|
|
|
|
|
-
|
|
|
|
|
- const newRelation = {
|
|
|
|
|
- deviceIds: [device.id],
|
|
|
|
|
- deviceNames: `${device.deviceCode} (${device.deviceName})`,
|
|
|
|
|
- userIds: users.map(u => u.id),
|
|
|
|
|
- userNames: users.map(u => u.nickname).join(', '),
|
|
|
|
|
- reason: formData.value.reason
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const existIndex = tempRelations.value.findIndex(
|
|
|
|
|
- // r => r.deviceIds[0] === device.id
|
|
|
|
|
- r => r.deviceIds.includes(device.id)
|
|
|
|
|
|
|
+const removeTempRelation = (deviceIds: number[]) => {
|
|
|
|
|
+ selectedDevices.value = selectedDevices.value.filter((id) => !deviceIds.includes(id))
|
|
|
|
|
+ tempRelations.value = tempRelations.value.filter(
|
|
|
|
|
+ (relation) => relation.deviceIds.join() !== deviceIds.join()
|
|
|
)
|
|
)
|
|
|
-
|
|
|
|
|
- if (existIndex > -1) {
|
|
|
|
|
- tempRelations.value[existIndex] = newRelation
|
|
|
|
|
- } else {
|
|
|
|
|
- tempRelations.value.push(newRelation)
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 处理 设备关联 部门选择清空事件
|
|
|
|
|
const handleClearDeptForDevice = () => {
|
|
const handleClearDeptForDevice = () => {
|
|
|
simpleDevices.value = []
|
|
simpleDevices.value = []
|
|
|
selectedDevices.value = []
|
|
selectedDevices.value = []
|
|
|
- devicesLoaded.value = false // 清空时禁用搜索框
|
|
|
|
|
|
|
+ selectedUsers.value = []
|
|
|
|
|
+ tempRelations.value = []
|
|
|
deviceFilterText.value = ''
|
|
deviceFilterText.value = ''
|
|
|
|
|
+ devicesLoaded.value = false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 处理 人员关联 部门选择清空事件
|
|
|
|
|
const handleClearDeptForPerson = () => {
|
|
const handleClearDeptForPerson = () => {
|
|
|
- formData.value.deptIds = []
|
|
|
|
|
|
|
+ formData.deptIds = []
|
|
|
simpleUsers.value = []
|
|
simpleUsers.value = []
|
|
|
selectedUsers.value = []
|
|
selectedUsers.value = []
|
|
|
- personLoaded.value = false // 清空时禁用搜索框
|
|
|
|
|
userFilterText.value = ''
|
|
userFilterText.value = ''
|
|
|
-
|
|
|
|
|
- // 清空所有暂存记录中的责任人
|
|
|
|
|
- tempRelations.value.forEach(relation => {
|
|
|
|
|
- relation.userIds = []
|
|
|
|
|
- relation.userNames = ''
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ personLoaded.value = false
|
|
|
|
|
+ syncRelations()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 新增部门选择监听
|
|
|
|
|
-const handleDeptCheckChange = () => {
|
|
|
|
|
- // getUserList() // 选择变化时重新加载人员
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 过滤后的人员列表计算属性
|
|
|
|
|
-const filteredUsers = computed(() => {
|
|
|
|
|
- const searchText = userFilterText.value.toLowerCase().trim()
|
|
|
|
|
- if (!searchText) return simpleUsers.value
|
|
|
|
|
|
|
+const submitRelations = async () => {
|
|
|
|
|
+ if (!selectedDevices.value.length) {
|
|
|
|
|
+ ElMessage.error(t('iotMaintain.deviceHolder'))
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return simpleUsers.value.filter(user => {
|
|
|
|
|
- return (user.nickname || '').toLowerCase().includes(searchText)
|
|
|
|
|
- })
|
|
|
|
|
-})
|
|
|
|
|
|
|
+ if (!selectedUsers.value.length) {
|
|
|
|
|
+ ElMessage.error(t('configPerson.selectPersons'))
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-const removeTempRelation = (deviceIds: number[]) => {
|
|
|
|
|
- tempRelations.value = tempRelations.value.filter(
|
|
|
|
|
- r => r.deviceIds.join() !== deviceIds.join()
|
|
|
|
|
- )
|
|
|
|
|
- // 同步取消勾选设备
|
|
|
|
|
- selectedDevices.value = selectedDevices.value.filter(
|
|
|
|
|
- id => !deviceIds.includes(id)
|
|
|
|
|
- )
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ if (tempRelations.value.some((item) => !item.reason?.trim())) {
|
|
|
|
|
+ ElMessage.error(t('configPerson.rfaHolder'))
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-const submitRelations = async () => {
|
|
|
|
|
try {
|
|
try {
|
|
|
- // 校验必须选择设备
|
|
|
|
|
- if (selectedDevices.value.length === 0) {
|
|
|
|
|
- ElMessage.error(t('iotMaintain.deviceHolder')) // 请选择设备
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- // 校验必须选择责任人
|
|
|
|
|
- if (selectedUsers.value.length === 0) {
|
|
|
|
|
- ElMessage.error(t('configPerson.selectPersons')) // 请选择责任人
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- // 校验所有调整原因
|
|
|
|
|
- const hasEmptyReason = tempRelations.value.some(
|
|
|
|
|
- item => !item.reason?.trim()
|
|
|
|
|
- )
|
|
|
|
|
- if (hasEmptyReason) {
|
|
|
|
|
- ElMessage.error(t('configPerson.rfaHolder'))
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- // 校验 设备 责任人 必须都选择 不能为空
|
|
|
|
|
- // 转换为后端需要的格式
|
|
|
|
|
- const submitData = tempRelations.value.flatMap(relation => {
|
|
|
|
|
- return relation.deviceIds.map(deviceId => ({
|
|
|
|
|
|
|
+ const submitData = tempRelations.value.flatMap((relation) => {
|
|
|
|
|
+ return relation.deviceIds.map((deviceId) => ({
|
|
|
deviceId,
|
|
deviceId,
|
|
|
userIds: relation.userIds,
|
|
userIds: relation.userIds,
|
|
|
userNames: relation.userNames,
|
|
userNames: relation.userNames,
|
|
|
reason: relation.reason
|
|
reason: relation.reason
|
|
|
}))
|
|
}))
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
await IotDevicePersonApi.saveDevicePersonRelation(submitData)
|
|
await IotDevicePersonApi.saveDevicePersonRelation(submitData)
|
|
|
- // 模拟API调用
|
|
|
|
|
ElMessage.success('提交成功')
|
|
ElMessage.success('提交成功')
|
|
|
tempRelations.value = []
|
|
tempRelations.value = []
|
|
|
delView(unref(router.currentRoute.value))
|
|
delView(unref(router.currentRoute.value))
|
|
@@ -532,150 +487,251 @@ const submitRelations = async () => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 初始化 */
|
|
|
|
|
|
|
+const findDeptNode = (nodes: Tree[], deptId: number): Tree | null => {
|
|
|
|
|
+ for (const node of nodes) {
|
|
|
|
|
+ if (node.id === deptId) return node
|
|
|
|
|
+
|
|
|
|
|
+ if (node.children?.length) {
|
|
|
|
|
+ const found = findDeptNode(node.children, deptId)
|
|
|
|
|
+ if (found) return found
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
onMounted(async () => {
|
|
onMounted(async () => {
|
|
|
try {
|
|
try {
|
|
|
- // 初始化部门树
|
|
|
|
|
const deptData = await DeptApi.getSimpleDeptList()
|
|
const deptData = await DeptApi.getSimpleDeptList()
|
|
|
- deptList.value = handleTree(deptData) // 转换为树形结构
|
|
|
|
|
|
|
+ deptList.value = handleTree(deptData)
|
|
|
|
|
|
|
|
- // 从路由参数获取部门ID
|
|
|
|
|
- const routeDeptId = route.query.deptId ? Number(route.query.deptId) : null
|
|
|
|
|
|
|
+ const routeDeptId = route.query.deptId ? Number(route.query.deptId) : undefined
|
|
|
|
|
+ const targetDeptId = routeDeptId || deptList.value[0]?.id
|
|
|
|
|
|
|
|
- let targetDeptId: number
|
|
|
|
|
-
|
|
|
|
|
- if (routeDeptId) {
|
|
|
|
|
- // 如果路由参数有部门ID,使用路由参数中的部门ID
|
|
|
|
|
- targetDeptId = routeDeptId
|
|
|
|
|
- } else if (deptList.value.length > 0) {
|
|
|
|
|
- // 否则使用第一个节点
|
|
|
|
|
- targetDeptId = deptList.value[0].id
|
|
|
|
|
- } else {
|
|
|
|
|
- console.warn("部门树数据为空,无法设置默认值")
|
|
|
|
|
|
|
+ if (!targetDeptId) {
|
|
|
|
|
+ ElMessage.warning('暂无可用部门数据')
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 设置默认选中的部门(转换后树的第一个节点)
|
|
|
|
|
- // if (deptList.value.length > 0) {
|
|
|
|
|
- // 获取转换后树的第一个节点
|
|
|
|
|
- // const firstRootNode = deptList.value[0]
|
|
|
|
|
|
|
+ formData.deptId1 = targetDeptId
|
|
|
|
|
+ formData.deptIds = [targetDeptId]
|
|
|
|
|
|
|
|
- // 设置设备部门的默认值
|
|
|
|
|
- formData.value.deptId1 = targetDeptId
|
|
|
|
|
- // 触发设备部门的节点点击事件,加载设备列表
|
|
|
|
|
const targetDeptNode = findDeptNode(deptList.value, targetDeptId)
|
|
const targetDeptNode = findDeptNode(deptList.value, targetDeptId)
|
|
|
- // 触发设备部门的节点点击事件,加载设备列表
|
|
|
|
|
if (targetDeptNode) {
|
|
if (targetDeptNode) {
|
|
|
await handleDeptDeviceTreeNodeClick(targetDeptNode)
|
|
await handleDeptDeviceTreeNodeClick(targetDeptNode)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ await getDeviceList()
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 设置责任人部门的默认值
|
|
|
|
|
- formData.value.deptIds = [targetDeptId]
|
|
|
|
|
- // 触发责任人部门的节点点击事件,加载人员列表
|
|
|
|
|
- await getUserList() //handleDeptUserTreeNodeClick(firstRootNode)
|
|
|
|
|
- // } else {
|
|
|
|
|
- // console.warn("部门树数据为空,无法设置默认值")
|
|
|
|
|
- // }
|
|
|
|
|
|
|
+ await getUserList()
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- console.error("初始化部门树失败:", error)
|
|
|
|
|
- ElMessage.error("加载部门数据失败")
|
|
|
|
|
|
|
+ ElMessage.error('加载部门数据失败')
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
+</script>
|
|
|
|
|
|
|
|
-// 在部门树中查找指定ID的节点
|
|
|
|
|
-const findDeptNode = (nodes: Tree[], deptId: number): Tree | null => {
|
|
|
|
|
- for (const node of nodes) {
|
|
|
|
|
- if (node.id === deptId) {
|
|
|
|
|
- return node
|
|
|
|
|
- }
|
|
|
|
|
- if (node.children && node.children.length > 0) {
|
|
|
|
|
- const found = findDeptNode(node.children, deptId)
|
|
|
|
|
- if (found) return found
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return null
|
|
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+.person-config-page {
|
|
|
|
|
+ min-height: calc(
|
|
|
|
|
+ 100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
|
|
|
|
|
+ );
|
|
|
|
|
+ background: #f5f7fb;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-</script>
|
|
|
|
|
|
|
+.selection-grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-<style scoped>
|
|
|
|
|
-.equal-height-row {
|
|
|
|
|
|
|
+.panel {
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: var(--el-bg-color);
|
|
|
|
|
+ border: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ box-shadow: 0 8px 24px rgb(15 35 70 / 5%);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-header {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- align-items: stretch;
|
|
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ min-height: 56px;
|
|
|
|
|
+ padding: 0 18px;
|
|
|
|
|
+ background: #f4f9ff;
|
|
|
|
|
+ border-bottom: 1px solid var(--el-border-color-lighter);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.container {
|
|
|
|
|
- padding: 20px;
|
|
|
|
|
|
|
+.panel-title {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.card {
|
|
|
|
|
- border: 1px solid #ebeef5;
|
|
|
|
|
|
|
+.panel-marker {
|
|
|
|
|
+ width: 4px;
|
|
|
|
|
+ height: 18px;
|
|
|
|
|
+ margin-right: 10px;
|
|
|
|
|
+ background: var(--el-color-primary);
|
|
|
border-radius: 4px;
|
|
border-radius: 4px;
|
|
|
- padding: 20px;
|
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
- background: white;
|
|
|
|
|
- box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
|
|
|
|
|
- transition: box-shadow 0.2s;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.list-item {
|
|
|
|
|
- padding: 8px 12px;
|
|
|
|
|
- border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
|
|
+.panel-toolbar {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: minmax(0, 1fr);
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ padding: 16px 18px 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.dept-select {
|
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
|
|
+.toolbar-control {
|
|
|
|
|
+ width: 100%;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.action-bar {
|
|
|
|
|
- text-align: right;
|
|
|
|
|
|
|
+.dept-tags-select :deep(.el-select__tags) {
|
|
|
|
|
+ flex-wrap: nowrap;
|
|
|
|
|
+ max-width: calc(100% - 48px);
|
|
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.temp-list {
|
|
|
|
|
- margin-top: 20px;
|
|
|
|
|
|
|
+.dept-tags-select :deep(.el-tag) {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ max-width: 92px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.submit-area {
|
|
|
|
|
- margin-top: 20px;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
|
|
+.dept-tags-select :deep(.el-tag__content) {
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-h3 {
|
|
|
|
|
- margin: 0 0 15px 0;
|
|
|
|
|
- color: #303133;
|
|
|
|
|
|
|
+:global(.dept-tags-popper .el-select__selection) {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ max-width: 520px;
|
|
|
|
|
+ max-height: 220px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.radio-item {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- padding: 8px 12px;
|
|
|
|
|
- border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
|
|
+:global(.dept-tags-popper .el-select__selected-item) {
|
|
|
|
|
+ max-width: 120px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:global(.dept-tags-popper .el-tag__content),
|
|
|
|
|
+:global(.dept-tags-popper .el-select__tags-text) {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ max-width: 92px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ vertical-align: bottom;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.option-scroll {
|
|
|
|
|
+ height: 430px;
|
|
|
|
|
+ padding: 14px 18px 18px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.option-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.option-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ height: auto;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ background: var(--el-fill-color-blank);
|
|
|
|
|
+ border: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ transition:
|
|
|
|
|
+ border-color 0.2s,
|
|
|
|
|
+ background-color 0.2s,
|
|
|
|
|
+ box-shadow 0.2s;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover,
|
|
|
|
|
+ &.is-selected {
|
|
|
|
|
+ background: #f7fbff;
|
|
|
|
|
+ border-color: var(--el-color-primary-light-5);
|
|
|
|
|
+ box-shadow: 0 6px 18px rgb(64 158 255 / 10%);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-checkbox__input) {
|
|
|
|
|
+ margin-top: 2px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-checkbox__label) {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ padding-left: 10px;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.radio-item .el-radio {
|
|
|
|
|
|
|
+.option-content {
|
|
|
|
|
+ display: grid;
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
- height: 100%;
|
|
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ grid-template-columns: minmax(90px, 0.7fr) minmax(120px, 1fr) minmax(150px, 1.2fr);
|
|
|
|
|
+ gap: 12px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.radio-item .el-radio__label {
|
|
|
|
|
- display: block;
|
|
|
|
|
- white-space: nowrap;
|
|
|
|
|
|
|
+.option-main,
|
|
|
|
|
+.option-sub,
|
|
|
|
|
+.option-extra {
|
|
|
|
|
+ min-width: 0;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.required-star {
|
|
|
|
|
- color: #ff4d4f;
|
|
|
|
|
- margin-left: 3px;
|
|
|
|
|
- vertical-align: middle;
|
|
|
|
|
|
|
+.option-main {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.option-sub,
|
|
|
|
|
+.option-extra {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.reason-panel,
|
|
|
|
|
+.record-panel {
|
|
|
|
|
+ margin-top: 16px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.filter-input {
|
|
|
|
|
- margin-bottom: 15px;
|
|
|
|
|
|
|
+.reason-panel {
|
|
|
|
|
+ padding-bottom: 18px;
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-textarea) {
|
|
|
|
|
+ padding: 18px 18px 0;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.no-data {
|
|
|
|
|
- padding: 20px;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- color: #999;
|
|
|
|
|
|
|
+.record-panel {
|
|
|
|
|
+ padding-bottom: 18px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.record-table {
|
|
|
|
|
+ width: calc(100% - 36px);
|
|
|
|
|
+ margin: 18px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.required-star {
|
|
|
|
|
+ margin-left: 4px;
|
|
|
|
|
+ color: var(--el-color-danger);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (width <= 1180px) {
|
|
|
|
|
+ .selection-grid {
|
|
|
|
|
+ grid-template-columns: minmax(0, 1fr);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|