UPDATE: 重构检修计划选择被检设备组件,并处理对应数据逻辑

This commit is contained in:
贾同学
2025-09-03 20:44:32 +08:00
parent da6a72807b
commit 74e015bd12
6 changed files with 483 additions and 270 deletions

View File

@@ -0,0 +1,305 @@
<template>
<el-row>
<el-col :span="24">
<el-card header-class="card-header" body-class="card-body" footer-class="card-footer">
<template #header>
<el-text size="large">{{ props.title }}</el-text>
<el-text type="info" size="small">{{ statistics.checked }}/{{ statistics.total }}</el-text>
</template>
<el-row>
<el-col :span="24">
<div style="display: flex; justify-content: space-between; align-items: center">
<el-checkbox
:disabled="props.disabled"
size="small"
v-model="filter.checkAll"
label="全选"
></el-checkbox>
<el-input
style="width: 82%; margin: 0 10px"
size="small"
v-model="filter.text"
:placeholder="props.filterPlaceholder"
/>
<el-dropdown size="small" @command="handleCommand">
<span class="el-dropdown-link">
<el-icon size="16"><Filter /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="">默认</el-dropdown-item>
<el-dropdown-item command="manufacturer">设备厂家</el-dropdown-item>
<el-dropdown-item command="cityName">所属地市</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-col>
</el-row>
<el-tree
ref="treeRef"
show-checkbox
accordion
default-expand-all
:data="treeData"
:default-checked-keys="defaultCheckedKeys"
:filter-node-method="filterNode"
node-key="id"
:style="{ marginTop: '20px', height: `${props.height}px` }"
@check-change="handleCheckChange"
>
<template #empty>
<el-empty :image-size="80" description="暂无可选设备" />
</template>
<template #default="{ node, data }">
<div v-if="data.id.startsWith('manufacturer')" style="display: flex; align-items: center">
<el-icon><OfficeBuilding /></el-icon>
<span style="margin-left: 4px">{{ data.label }}</span>
</div>
<div v-else-if="data.id.startsWith('cityName')" style="display: flex; align-items: center">
<el-icon><Location /></el-icon>
<span style="margin-left: 4px">{{ data.label }}</span>
</div>
<div v-else style="flex: 1; display: flex; align-items: center; justify-content: space-between">
<span>
<el-icon><Cpu /></el-icon>
<span v-if="node.level === 1" style="margin-left: 4px">
{{ data.cityName + ' - ' + data.manufacturer + ' - ' + data.name }}
</span>
<span v-if="node.level === 2" style="margin-left: 4px">
{{ data.name }}
</span>
</span>
<el-tooltip effect="light" placement="top">
<template #content>
<el-descriptions border size="small" title="被检设备详情">
<el-descriptions-item label="设备名称">
{{ data.name }}
</el-descriptions-item>
<el-descriptions-item label="设备厂家">
{{ data.manufacturer }}
</el-descriptions-item>
<el-descriptions-item label="所属地市">
{{ data.cityName }}
</el-descriptions-item>
<el-descriptions-item label="所属供电公司">
{{ data.gdName }}
</el-descriptions-item>
<el-descriptions-item label="所属电站">
{{ data.subName }}
</el-descriptions-item>
</el-descriptions>
</template>
<el-icon>
<Warning />
</el-icon>
</el-tooltip>
</div>
</template>
</el-tree>
<!-- 传递 footer 插槽到 el-card -->
<template #footer>
<slot name="footer"></slot>
</template>
</el-card>
</el-col>
</el-row>
</template>
<script lang="ts" name="DevSelect" setup>
import { ref, watch } from 'vue'
import type { FilterNodeMethodFunction, TreeInstance } from 'element-plus'
interface Tree {
[key: string]: any
}
interface Device {
id: string
name: string
checked: boolean
importFlag: number
[key: string]: any
}
const modelValue = defineModel<string[]>({ default: [] })
const props = defineProps({
data: {
type: Array as unknown as PropType<Device[]>,
default: (): Device[] => []
},
height: {
type: Number,
default: 240
},
title: {
type: String,
default: ''
},
filterPlaceholder: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
}
})
const treeRef = ref<TreeInstance>()
const treeData = ref<Tree[]>([])
const defaultCheckedKeys = ref<string[]>([])
const filter = ref({
groupBy: '',
text: '',
checkAll: false
})
const statistics = ref({
total: 0,
checked: 0
})
const disabledKeys = ref<string[]>([])
onMounted(() => {
if (props.data) {
initTree(props.data)
}
})
watch(
() => props.data,
(newVal: Device[]) => {
if (newVal) {
initTree(newVal)
}
}
)
watch(
() => filter.value.checkAll,
val => {
setCheckedStatus(val)
if (val) {
statistics.value.checked = statistics.value.total
} else {
statistics.value.checked = treeRef.value?.getCheckedNodes().length || 0
}
}
)
watch(
() => filter.value.text,
val => {
treeRef.value!.filter(val)
}
)
const initTree = (data: Device[]) => {
disabledKeys.value = data.filter(item => item.disabled).map(item => item.id)
if (disabledKeys.value.length > 0) {
modelValue.value = []
}
treeData.value = convertToTree(data, filter.value.groupBy)
defaultCheckedKeys.value = data.filter(item => item.checked).map(item => item.id)
statistics.value.checked = defaultCheckedKeys.value.length
statistics.value.total = data.length
if (statistics.value.total > 0) {
filter.value.checkAll = statistics.value.checked === statistics.value.total
}
}
const setCheckedStatus = (checked: boolean) => {
treeData.value.forEach(item => {
if (!disabledKeys.value.includes(item.id)) {
treeRef.value?.setChecked(item.id, checked, true)
}
})
}
const convertToTree = (data: any[], groupBy?: string | undefined) => {
if (groupBy) {
// 创建一个映射来存储每个分组
const groupMap = new Map()
// 遍历数据根据groupBy字段值进行分组
data.forEach(item => {
const groupValue = item[groupBy] || '未分组'
if (!groupMap.has(groupValue)) {
groupMap.set(groupValue, [])
}
groupMap.get(groupValue).push(item)
})
// 将分组转换为树形结构
const treeData: Tree[] = []
groupMap.forEach((items, groupName) => {
const groupNode: Tree = {
id: `${groupBy}_${groupName}`,
label: groupName,
disabled: props.disabled,
children: (items as any[]).map((item: any) => ({
label: item.name,
children: [],
disabled: item.disabled ? item.disabled : props.disabled,
...item
}))
}
treeData.push(groupNode)
})
return treeData
} else {
return data.map(item => {
const treeItem: Tree = {
label: item.name,
children: [],
disabled: item.disabled ? item.disabled : props.disabled,
...item
}
return treeItem
})
}
}
const filterNode: FilterNodeMethodFunction = (value: string, data: Tree) => {
if (!value) return true
return data.label.includes(value)
}
const handleCommand = (command: string) => {
filter.value.groupBy = command
const oldCheckedKeys = treeRef.value?.getCheckedKeys() || []
treeData.value = convertToTree(props.data, filter.value.groupBy)
treeRef.value?.setCheckedKeys(oldCheckedKeys)
if (filter.value.checkAll) {
setCheckedStatus(true)
}
}
const handleCheckChange = () => {
const checkedKeys = treeRef.value?.getCheckedKeys().filter(item => !item.toString().includes('_')) || []
modelValue.value = checkedKeys
.filter(key => !disabledKeys.value.includes(key.toString()))
.map(key => key.toString())
statistics.value.checked = checkedKeys.length || 0
filter.value.checkAll = statistics.value.checked === statistics.value.total
}
</script>
<style lang="scss">
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--el-color-info-light-9);
margin: 0;
padding: 10px 15px;
color: var(--el-color-black);
border: 1px solid var(--el-border-color);
user-select: none;
}
.card-body {
padding: 15px;
}
.card-footer {
margin: 0;
padding: 10px 15px;
}
.el-dropdown-link {
cursor: pointer;
color: var(--el-color-primary);
display: flex;
align-items: center;
}
</style>

View File

@@ -10,7 +10,7 @@
>
<el-form ref="dialogFormRef" :model="formContent" :rules="rules">
<el-row :gutter="24">
<el-col :span="10">
<el-col :span="12">
<el-form-item :label-width="110" label="名称" prop="name">
<el-input
v-model="formContent.name"
@@ -122,7 +122,7 @@
clearable
filterable
placeholder="请选择误差体系"
@change="handleErrorSysChange"
@change="handleErrorSysChange"
>
<el-option
v-for="(option, index) in pqErrorArray"
@@ -181,81 +181,42 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="14">
<el-transfer
v-model="value"
:data="allData"
:filter-method="filterMethod"
:titles="['未绑定被检设备', '已绑定被检设备']"
<el-col :span="12">
<DevSelect
v-model="formContent.devIds"
title="被检设备列表"
filter-placeholder="请输入内容搜索"
filterable
:data="devData"
:height="250"
:disabled="allDisabled"
>
<template #default="{ option }">
<div style="display: flex; align-items: center; justify-content: space-between">
<span>
{{ JSON.parse(option.label).manufacturer }} - {{ JSON.parse(option.label).name }}
</span>
<el-tooltip effect="light" placement="top">
<template #content>
<el-descriptions border size="small" title="被检设备详情">
<el-descriptions-item label="设备名称">
{{ JSON.parse(option.label).name }}
</el-descriptions-item>
<el-descriptions-item label="设备厂家">
{{ JSON.parse(option.label).manufacturer }}
</el-descriptions-item>
<el-descriptions-item label="所属地市">
{{ JSON.parse(option.label).cityName }}
</el-descriptions-item>
<el-descriptions-item label="所属供电公司">
{{ JSON.parse(option.label).gdName }}
</el-descriptions-item>
<el-descriptions-item label="所属电站">
{{ JSON.parse(option.label).subName }}
</el-descriptions-item>
</el-descriptions>
</template>
<el-icon>
<Warning />
</el-icon>
</el-tooltip>
<template v-if="planType === 0 && !allDisabled" #footer>
<div style="text-align: right">
<el-button
v-if="modeStore.currentMode !== '比对式'"
v-auth.plan="'import'"
icon="Download"
class="transfer-footer"
size="small"
type="primary"
@click="importFile('')"
>
导入被检设备
</el-button>
<el-button
v-if="modeStore.currentMode === '比对式'"
v-auth.plan="'import'"
icon="Download"
class="transfer-footer"
size="small"
type="primary"
@click="importFile('比对式')"
>
导入被检设备
</el-button>
</div>
</template>
<template v-if="planType === 0 && !allDisabled" #left-footer>
<el-button
v-if="modeStore.currentMode !== '比对式'"
v-auth.plan="'import'"
:icon="Download"
class="transfer-footer"
size="small"
type="primary"
@click="importFile('')"
>
导入被检设备
</el-button>
<el-button
v-if="modeStore.currentMode === '比对式'"
v-auth.plan="'import'"
:icon="Download"
class="transfer-footer"
size="small"
type="primary"
@click="importFile('比对式')"
>
导入被检设备
</el-button>
</template>
<template v-if="planType === 0 && !allDisabled" #right-footer>
<el-text></el-text>
</template>
<template #left-empty>
<el-empty :image-size="60" description="暂无被检设备" />
</template>
<template #right-empty>
<el-empty :image-size="60" description="暂无被检设备" />
</template>
</el-transfer>
</DevSelect>
</el-col>
</el-row>
@@ -274,7 +235,7 @@
<el-input-number
:disabled="isSelectDisabled || allDisabled"
style="width: 240px"
v-model="formContent.testConfig.maxTime"
v-model="formContent.testConfig!.maxTime"
:precision="0"
:step="1"
:min="1"
@@ -292,7 +253,7 @@
<el-input-number
:disabled="isSelectDisabled || allDisabled"
style="width: 240px"
v-model="formContent.testConfig.waveRecord"
v-model="formContent.testConfig!.waveRecord"
:precision="0"
:step="1"
:min="1"
@@ -310,7 +271,7 @@
<el-input-number
:disabled="isSelectDisabled || allDisabled"
style="width: 240px"
v-model="formContent.testConfig.realTime"
v-model="formContent.testConfig!.realTime"
:precision="0"
:step="1"
:min="1"
@@ -328,7 +289,7 @@
<el-input-number
:disabled="isSelectDisabled || allDisabled"
style="width: 240px"
v-model="formContent.testConfig.statistics"
v-model="formContent.testConfig!.statistics"
:precision="0"
:step="1"
:min="1"
@@ -346,7 +307,7 @@
<el-input-number
:disabled="isSelectDisabled || allDisabled"
style="width: 240px"
v-model="formContent.testConfig.flicker"
v-model="formContent.testConfig!.flicker"
:precision="0"
:step="1"
:min="1"
@@ -371,7 +332,7 @@
</template>
<script lang="ts" setup>
import { CascaderOption, ElMessage, type FormItemRule } from 'element-plus'
import { type CascaderOption, ElMessage, type FormItemRule } from 'element-plus'
import { computed, defineProps, reactive, ref } from 'vue'
import { dialogBig } from '@/utils/elementBind'
import { type Plan } from '@/api/plan/interface'
@@ -380,31 +341,28 @@ import {
getBoundPqDevList,
getBoundStandardDevAllList,
getPqErrSysList,
getPqErrSysTestItemList,
getPqScriptList,
getTestSourceList,
getUnboundPqDevList,
updatePlan,
getPqErrSysTestItemList
} from '@/api/plan/plan.ts'
updatePlan
} from '@/api/plan/plan'
import { useDictStore } from '@/stores/modules/dict'
import { type TestSource } from '@/api/device/interface/testSource'
import { type TestScript } from '@/api/device/interface/testScript'
import { type ErrorSystem } from '@/api/device/interface/error'
import { type Device } from '@/api/device/interface/device'
import { getPqReportAllName } from '@/api/device/report/index.ts'
import { getPqReportAllName } from '@/api/device/report/index'
import { useAppSceneStore, useModeStore } from '@/stores/modules/mode'
import { Download } from '@element-plus/icons-vue'
import { getAllPqStandardDev } from '@/api/device/standardDevice/index.ts'
import { StandardDevice } from '@/api/device/interface/standardDevice'
import { Dict } from '@/api/system/dictionary/interface'
import { getDictTreeByCode } from '@/api/system/dictionary/dictTree'
import { getAllPqStandardDev } from '@/api/device/standardDevice/index'
import { type StandardDevice } from '@/api/device/interface/standardDevice'
import { type Dict } from '@/api/system/dictionary/interface'
import { getAllUser } from '@/api/user/user'
import ImportExcel from '@/components/ImportExcel/index.vue'
import { downloadTemplate, importPqDev } from '@/api/device/device/index'
import { getTestConfig } from '@/api/system/base'
import { getRegRes } from '@/api/system/versionRegister'
const modeStore = useModeStore()
const AppSceneStore = useAppSceneStore()
const dictStore = useDictStore()
@@ -423,15 +381,13 @@ const pqReportName = ref<{ name: string }[]>([])
const pqSourceArray = ref<{ label: string; value: string }[]>()
const pqScriptArray = ref<{ label: string; value: string }[]>()
const pqErrorArray = ref<{ label: string; value: string }[]>()
const pqDevArray = ref<{ label: string; value: string }[]>()
const pqStandardDevArray = ref<{ label: string; value: string }[]>()
const secondLevelOptions: any[] = []
const userArray = ref<{ label: string; value: string }[]>([])
const unboundPqDevList = ref<Device.ResPqDev[]>([]) //指定模式下所有未绑定的设备
const boundPqDevList = ref<Device.ResPqDev[]>([]) //根据检测计划id查询出所有已绑定的设备
const value = ref<string[]>([])
const allData = ref<[any[], any[]]>([])
const devData = ref<any[]>([])
const isSelectDisabled = ref(false)
const planType = ref<number>(0)
const subPlanBindStandardDev = ref<any>([]) //哪些标准设备已经被子计划绑定
@@ -439,7 +395,11 @@ const activeNames = ref(['1'])
const allDisabled = ref(false)
const generateData = () => {
const manufacturerDict = dictStore.getDictData('Dev_Manufacturers')
unboundPqDevList.value.forEach(i => {
const boundPqDevIds: string[] = boundPqDevList.value.map(i => i.id)
const allPqDevList = [...unboundPqDevList.value, ...boundPqDevList.value]
allPqDevList.forEach(i => {
// 确保字段不为空且字典存在再进行查找
if (i.manufacturer && manufacturerDict) {
const manufacturer = manufacturerDict.find(item => item.id === i.manufacturer)
@@ -447,42 +407,14 @@ const generateData = () => {
i.manufacturer = manufacturer.name
}
}
})
const unboundData = unboundPqDevList.value.map((i: Device.ResPqDev) => ({
key: i.id,
label: JSON.stringify(i),
disabled: allDisabled.value
}))
boundPqDevList.value.forEach(i => {
// 确保字段不为空且字典存在再进行查找
if (i.manufacturer && manufacturerDict) {
const manufacturer = manufacturerDict.find(item => item.id === i.manufacturer)
if (manufacturer) {
i.manufacturer = manufacturer.name
}
i.checked = boundPqDevIds.includes(i.id)
if (i.assign) {
i.disabled = i.checkState != 0 || i.assign == 1 || allDisabled.value
} else {
i.disabled = allDisabled.value
}
})
const boundData = boundPqDevList.value.map((i: Device.ResPqDev) => ({
key: i.id,
label: JSON.stringify(i),
//tips: i.description
disabled: i.checkState != 0 || i.assign == 1 || allDisabled.value
}))
allData.value = [...unboundData, ...boundData]
}
const filterMethod = (query: string, item: { label?: string }) => {
const dataJSON = item.label ? JSON.parse(item.label) : {}
return (
(dataJSON.name?.toLowerCase().includes(query.toLowerCase()) ||
dataJSON.manufacturer?.toLowerCase().includes(query.toLowerCase()) ||
dataJSON.subName?.toLowerCase().includes(query.toLowerCase()) ||
dataJSON.gdName?.toLowerCase().includes(query.toLowerCase()) ||
dataJSON.cityName?.toLowerCase().includes(query.toLowerCase())) ??
false
)
devData.value = allPqDevList
}
function useMetaInfo() {
@@ -621,7 +553,6 @@ const rules = computed(() => {
// 关闭弹窗
const close = () => {
value.value = []
dialogVisible.value = false
// 清空dialogForm中的值
resetFormContent()
@@ -636,7 +567,6 @@ const save = () => {
try {
dialogFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
formContent.devIds = value.value
if (formContent.id) {
// 把数据处理原则转成字典ID
const patternItem = dictStore
@@ -650,9 +580,6 @@ const save = () => {
if (planType.value == 1) {
formContent.fatherPlanId = formContent.id
formContent.id = ''
//formContent.devIds = []
//formContent.standardDevIds = []
// formContent.standardDevMap = new Map<string, number>()
await addPlan(formContent)
emit('update:tab')
// 编辑子计划
@@ -709,7 +636,6 @@ const save = () => {
// 打开弹窗,可能是新增,也可能是编辑
const open = async (sign: string, data: Plan.ReqPlan, currentMode: string, plan: number) => {
unboundPqDevList.value = []
boundPqDevList.value = []
//处理异步调用
@@ -753,11 +679,13 @@ const open = async (sign: string, data: Plan.ReqPlan, currentMode: string, plan:
// 比对式下这两个接口不需要
pqSource_Result = { data: [] }
PqScript_Result = { data: [] }
formContent.testConfig.maxTime = sysTestConfig_Result.data?.maxTime
formContent.testConfig.waveRecord = sysRegRes_Result.data?.waveRecord
formContent.testConfig.realTime = sysRegRes_Result.data?.realTime
formContent.testConfig.statistics = sysRegRes_Result.data?.statistics
formContent.testConfig.flicker = sysRegRes_Result.data?.flicker
if (formContent.testConfig) {
formContent.testConfig.maxTime = (sysTestConfig_Result.data as { maxTime?: number })?.maxTime || 3
formContent.testConfig.waveRecord = (sysRegRes_Result.data as { waveRecord?: number })?.waveRecord || 0
formContent.testConfig.realTime = (sysRegRes_Result.data as { realTime?: number })?.realTime || 0
formContent.testConfig.statistics = (sysRegRes_Result.data as { statistics?: number })?.statistics || 0
formContent.testConfig.flicker = (sysRegRes_Result.data as { flicker?: number })?.flicker || 0
}
} else {
const commonResults = await Promise.all([
getTestSourceList(data),
@@ -793,9 +721,6 @@ const open = async (sign: string, data: Plan.ReqPlan, currentMode: string, plan:
formContent.sourceIds = pqSourceList.value[0]?.id ?? ''
formContent.datasourceIds = dictStore.getDictData('Datasource')[0]?.code ?? ''
formContent.dataRule = dictStore.getDictData('Data_Rule')[0]?.id ?? ''
} else {
//编辑时先给表单赋值(这会没接收被检设备),需要手动再给被检设备复制后整体表单赋值
@@ -911,16 +836,14 @@ const open = async (sign: string, data: Plan.ReqPlan, currentMode: string, plan:
pqToArray() //将对象转为数组
//比对式测试项下拉框
if (mode.value == '比对式') {
// 如果是编辑模式且已有误差体系ID则加载对应的测试项
if (formContent.errorSysId) {
await loadTestItemsForErrorSys(formContent.errorSysId);
await loadTestItemsForErrorSys(formContent.errorSysId)
}
}
if (mode.value != '比对式') {
selectByMode.value = false
// 将 formContent.sourceIds 从数组转换为字符串
@@ -933,8 +856,6 @@ const open = async (sign: string, data: Plan.ReqPlan, currentMode: string, plan:
}
}
value.value = boundPqDevList.value.map((i: { id: { toString: () => any } }) => i.id.toString())
if (AppSceneStore.currentScene == '1') {
//楼下场景不用报告模版
formContent.associateReport = 0
@@ -960,14 +881,14 @@ const open = async (sign: string, data: Plan.ReqPlan, currentMode: string, plan:
// 误差体系选择变化时的处理函数
const handleErrorSysChange = async (value: string) => {
// 清空测试项选择
formContent.testItems = [];
formContent.testItems = []
// 如果是比对式模式且选择了误差体系
if (mode.value === '比对式' && value) {
try {
const res = await getPqErrSysTestItemList({ errorSysId: value });
const res = await getPqErrSysTestItemList({ errorSysId: value })
// 清空原有选项
secondLevelOptions.length = 0;
secondLevelOptions.length = 0
if (res.data) {
// 将返回的键值对对象转换为下拉选项格式
@@ -975,29 +896,28 @@ const handleErrorSysChange = async (value: string) => {
secondLevelOptions.push({
value: key,
label: res.data[key]
});
});
})
})
}
} catch (error) {
console.error('获取测试项失败:', error);
console.error('获取测试项失败:', error)
// 发生错误时清空选项
secondLevelOptions.length = 0;
secondLevelOptions.length = 0
}
} else {
// 如果没有选择误差体系,清空测试项选项
secondLevelOptions.length = 0;
secondLevelOptions.length = 0
}
}
// 根据误差体系ID加载测试项用于初始化
const loadTestItemsForErrorSys = async (errorSysId: string) => {
if (!errorSysId) return;
if (!errorSysId) return
try {
const res = await getPqErrSysTestItemList({ errorSysId: errorSysId });
const res = await getPqErrSysTestItemList({ errorSysId: errorSysId })
// 清空原有选项
secondLevelOptions.length = 0;
secondLevelOptions.length = 0
if (res.data) {
// 将返回的键值对对象转换为下拉选项格式
@@ -1005,12 +925,12 @@ const loadTestItemsForErrorSys = async (errorSysId: string) => {
secondLevelOptions.push({
value: key,
label: res.data[key]
});
});
})
})
}
} catch (error) {
console.error('获取测试项失败:', error);
secondLevelOptions.length = 0;
console.error('获取测试项失败:', error)
secondLevelOptions.length = 0
}
}
@@ -1045,12 +965,6 @@ function pqToArray() {
value: item.id
}))
const sourceArray4 = Array.isArray(pqDevList.value) ? pqDevList.value : []
pqDevArray.value = sourceArray4.map(item => ({
label: item.name,
value: item.id
}))
const sourceArray5 = Array.isArray(pqStandardDevList.value) ? pqStandardDevList.value : []
if (planType.value === 0) {
pqStandardDevArray.value = sourceArray5.map(item => ({