UPDATE: 优化选择被检设备组件

This commit is contained in:
贾同学
2025-09-04 19:39:20 +08:00
parent 25f3570c18
commit 5a7eea1052
2 changed files with 261 additions and 62 deletions

View File

@@ -1,10 +1,10 @@
<template>
<el-row>
<el-col :span="24">
<el-col :span="11">
<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>
<el-text size="large">{{ props.titles[0] }}</el-text>
<el-text type="info" size="small"> {{ statistics.total }} </el-text>
</template>
<el-row>
<el-col :span="24">
@@ -14,11 +14,12 @@
size="small"
v-model="filter.checkAll"
label="全选"
@change="handleCheckAllChange"
></el-checkbox>
<el-input
style="width: 82%; margin: 0 10px"
size="small"
v-model="filter.text"
v-model="filter.leftText"
:placeholder="props.filterPlaceholder"
/>
<el-dropdown size="small" @command="handleCommand">
@@ -26,47 +27,62 @@
<el-icon size="16"><Filter /></el-icon>
</span>
<template #dropdown>
<el-radio-group v-model="filter.groupBy">
<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-item command="subName">
<el-radio value="subName" size="small">所属电站</el-radio>
</el-dropdown-item>
<el-dropdown-item command="cityName">
<el-radio value="cityName" size="small">所属地市</el-radio>
</el-dropdown-item>
<el-dropdown-item command="manufacturer">
<el-radio value="manufacturer" size="small">设备厂家</el-radio>
</el-dropdown-item>
</el-dropdown-menu>
</el-radio-group>
</template>
</el-dropdown>
</div>
</el-col>
</el-row>
<el-scrollbar ref="scrollbarRef" :height="props.height" style="margin-top: 20px">
<el-scrollbar ref="leftScrollbarRef" :height="props.height" style="margin-top: 20px">
<el-tree
ref="treeRef"
ref="leftTreeRef"
show-checkbox
accordion
default-expand-all
:data="treeData"
:data="leftTreeData"
:default-checked-keys="defaultCheckedKeys"
:default-expanded-keys="defaultCheckedKeys"
:filter-node-method="filterNode"
node-key="id"
:style="{ padding: '0 25px' }"
style="padding-left: 0; padding-right: 20px"
@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>
<div v-if="data.id.startsWith('subName')" style="display: flex; align-items: center">
<el-icon style="margin-top: 2px"><Lightning /></el-icon>
<span style="margin-left: 4px">{{ data.label }}</span>
</div>
<div
v-else-if="data.id.startsWith('manufacturer')"
style="display: flex; align-items: center"
>
<el-icon style="margin-top: 2px"><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>
<el-icon style="margin-top: 2px"><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 style="display: flex; align-items: center">
<el-icon style="margin-top: 2px"><Cpu /></el-icon>
<span v-if="node.level === 1" style="margin-left: 4px">
{{ data.cityName + ' - ' + data.manufacturer + ' - ' + data.name }}
</span>
@@ -94,7 +110,7 @@
</el-descriptions-item>
</el-descriptions>
</template>
<el-icon>
<el-icon style="margin-top: 2px; margin-right: 5px">
<Warning />
</el-icon>
</el-tooltip>
@@ -109,11 +125,139 @@
</template>
</el-card>
</el-col>
<el-col :span="11" :offset="1">
<el-card header-class="card-header" body-class="card-body" footer-class="card-footer">
<template #header>
<el-text size="large">{{ props.titles[1] }}</el-text>
<el-text type="info" size="small">{{ statistics.checked }} </el-text>
</template>
<el-row>
<el-col :span="24">
<div style="display: flex; justify-content: space-between; align-items: center">
<el-button size="small" :disabled="props.disabled" @click="clearAll">清除</el-button>
<el-input
style="width: 80%; margin: 0 10px"
size="small"
v-model="filter.rightText"
: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-radio-group v-model="filter.groupBy">
<el-dropdown-menu>
<el-dropdown-item command="subName">
<el-radio value="subName" size="small">所属电站</el-radio>
</el-dropdown-item>
<el-dropdown-item command="manufacturer">
<el-radio value="manufacturer" size="small">设备厂家</el-radio>
</el-dropdown-item>
<el-dropdown-item command="cityName">
<el-radio value="cityName" size="small">所属地市</el-radio>
</el-dropdown-item>
</el-dropdown-menu>
</el-radio-group>
</template>
</el-dropdown>
</div>
</el-col>
</el-row>
<el-scrollbar ref="rightScrollbarRef" :height="rightHeight" style="margin-top: 20px">
<el-tree
ref="rightTreeRef"
accordion
:data="rightTreeData"
:filter-node-method="filterNode"
node-key="id"
default-expand-all
style="padding-left: 0; padding-right: 15px"
>
<template #empty>
<el-empty :image-size="80" description="暂未选择设备" />
</template>
<template #default="{ node, data }">
<div v-if="data.id.startsWith('subName')" style="display: flex; align-items: center">
<el-icon style="margin-top: 2px"><Lightning /></el-icon>
<span style="margin-left: 4px">{{ data.label }}</span>
</div>
<div
v-else-if="data.id.startsWith('manufacturer')"
style="display: flex; align-items: center"
>
<el-icon style="margin-top: 2px"><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 style="margin-top: 2px"><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 style="display: flex; align-items: center">
<el-icon style="margin-top: 2px"><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>
<span style="display: flex; align-items: center">
<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 style="margin-top: 2px; margin-right: 5px">
<Warning />
</el-icon>
</el-tooltip>
<el-button
v-if="!data.disabled && !props.disabled"
text
circle
@click="removeNode(data)"
size="small"
icon="Close"
></el-button>
</span>
</div>
</template>
</el-tree>
</el-scrollbar>
</el-card>
</el-col>
</el-row>
</template>
<script lang="ts" name="DevSelect" setup>
import { ref, watch } from 'vue'
import type { FilterNodeMethodFunction, ScrollbarInstance, TreeInstance } from 'element-plus'
<script lang="ts" setup>
import { nextTick, ref, watch } from 'vue'
import type {
FilterNodeMethodFunction,
RenderContentContext,
ScrollbarInstance,
TreeInstance,
TreeKey
} from 'element-plus'
import { Cpu, Lightning, Location, OfficeBuilding, Warning } from '@element-plus/icons-vue'
interface Tree {
[key: string]: any
@@ -122,10 +266,9 @@ interface Device {
id: string
name: string
checked: boolean
importFlag: number
[key: string]: any
}
type Data = RenderContentContext['data']
const modelValue = defineModel<string[]>({ default: [] })
const props = defineProps({
@@ -137,9 +280,9 @@ const props = defineProps({
type: Number,
default: 240
},
title: {
type: String,
default: ''
titles: {
type: Array,
default: () => ['', '']
},
filterPlaceholder: {
type: String,
@@ -150,13 +293,17 @@ const props = defineProps({
default: false
}
})
const treeRef = ref<TreeInstance>()
const scrollbarRef = ref<ScrollbarInstance>()
const treeData = ref<Tree[]>([])
const leftTreeRef = ref<TreeInstance>()
const rightTreeRef = ref<TreeInstance>()
const leftScrollbarRef = ref<ScrollbarInstance>()
const rightScrollbarRef = ref<ScrollbarInstance>()
const leftTreeData = ref<Tree[]>([])
const rightTreeData = ref<Tree[]>([])
const defaultCheckedKeys = ref<string[]>([])
const filter = ref({
groupBy: '',
text: '',
groupBy: 'subName',
leftText: '',
rightText: '',
checkAll: false
})
const statistics = ref({
@@ -164,57 +311,78 @@ const statistics = ref({
checked: 0
})
const disabledKeys = ref<string[]>([])
onMounted(() => {
if (props.data) {
initTree(props.data)
}
})
const rightHeight = computed(() => {
if (!props.disabled) {
return props.height + 45.2
}
return props.height
})
watch(
() => props.data,
(newVal: Device[]) => {
if (newVal) {
initTree(newVal)
}
initTree(newVal || [])
}
)
watch(
() => filter.value.checkAll,
() => filter.value.leftText,
val => {
setCheckedStatus(val)
if (val) {
statistics.value.checked = statistics.value.total
} else {
statistics.value.checked = treeRef.value?.getCheckedNodes().length || 0
}
leftTreeRef.value!.filter(val)
}
)
watch(
() => filter.value.text,
() => filter.value.rightText,
val => {
treeRef.value!.filter(val)
rightTreeRef.value!.filter(val)
}
)
const initTree = (data: Device[]) => {
// 禁用Key数据
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)
// 左侧树数据
leftTreeData.value = convertToTree(data, filter.value.groupBy)
// 右侧树数据
rightTreeData.value = convertToTree(
data.filter(item => item.checked),
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 handleCheckAllChange = () => {
nextTick(() => {
setCheckedStatus(filter.value.checkAll)
})
}
const convertToTree = (data: any[], groupBy?: string | undefined) => {
const setCheckedStatus = (checked: boolean) => {
if (checked) {
leftTreeRef.value?.setCheckedKeys(props.data.map(item => item.id) as TreeKey[])
} else {
const filterKeys = props.data
.filter(item => !disabledKeys.value.includes(item.id))
.map(item => item.id) as TreeKey[]
filterKeys.forEach(key => {
leftTreeRef.value?.setChecked(key, false, true)
})
}
}
const convertToTree = (data: Device[], groupBy?: string | undefined) => {
if (groupBy) {
// 创建一个映射来存储每个分组
const groupMap = new Map()
@@ -268,23 +436,42 @@ const filterNode: FilterNodeMethodFunction = (value: string, data: Tree) => {
}
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)
const oldCheckedKeys = leftTreeRef.value?.getCheckedKeys() || []
leftTreeData.value = convertToTree(props.data, filter.value.groupBy)
leftTreeRef.value?.setCheckedKeys(oldCheckedKeys)
rightTreeData.value = convertToTree(
props.data.filter(item => oldCheckedKeys.includes(item.id)),
filter.value.groupBy
)
if (filter.value.checkAll) {
setCheckedStatus(true)
}
scrollbarRef.value?.setScrollTop(0)
leftScrollbarRef.value?.setScrollTop(0)
rightScrollbarRef.value?.setScrollTop(0)
}
const handleCheckChange = () => {
const checkedKeys = treeRef.value?.getCheckedKeys().filter(item => !item.toString().includes('_')) || []
const checkedKeys = leftTreeRef.value?.getCheckedKeys().filter(item => !item.toString().includes('_')) || []
modelValue.value = checkedKeys
.filter(key => !disabledKeys.value.includes(key.toString()))
.map(key => key.toString())
rightTreeData.value = convertToTree(
props.data.filter(item => checkedKeys.includes(item.id)),
filter.value.groupBy
)
statistics.value.checked = checkedKeys.length || 0
filter.value.checkAll = statistics.value.checked === statistics.value.total
}
const clearAll = () => {
if (statistics.value.checked > 0) {
setCheckedStatus(false)
}
}
const removeNode = (data: Data) => {
nextTick(() => {
leftTreeRef.value?.setChecked(data, false, true)
rightTreeRef.value?.remove(data)
})
}
</script>
<style lang="scss">
.card-header {

View File

@@ -10,7 +10,7 @@
>
<el-form ref="dialogFormRef" :model="formContent" :rules="rules">
<el-row :gutter="24">
<el-col :span="12">
<el-col :span="9">
<el-form-item :label-width="110" label="名称" prop="name">
<el-input
v-model="formContent.name"
@@ -181,10 +181,10 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="15">
<DevSelect
v-model="formContent.devIds"
title="被检设备列表"
:titles="['被检设备列表', '已选被检设备列表']"
filter-placeholder="请输入内容搜索"
:data="devData"
:height="230"
@@ -414,7 +414,19 @@ const generateData = () => {
i.disabled = allDisabled.value
}
})
devData.value = allPqDevList
// 排序逻辑
devData.value = allPqDevList.sort((a, b) => {
// 首先按 checked 排序(选中的在前)
if (a.checked !== b.checked) {
return a.checked ? -1 : 1
}
// 然后按 disabled 排序(未禁用的在前)
if (a.disabled !== b.disabled) {
return a.disabled ? 1 : -1
}
// 最后按名称排序(升序)
return a.name.localeCompare(b.name)
})
}
function useMetaInfo() {