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

View File

@@ -10,7 +10,7 @@
> >
<el-form ref="dialogFormRef" :model="formContent" :rules="rules"> <el-form ref="dialogFormRef" :model="formContent" :rules="rules">
<el-row :gutter="24"> <el-row :gutter="24">
<el-col :span="12"> <el-col :span="9">
<el-form-item :label-width="110" label="名称" prop="name"> <el-form-item :label-width="110" label="名称" prop="name">
<el-input <el-input
v-model="formContent.name" v-model="formContent.name"
@@ -181,10 +181,10 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="15">
<DevSelect <DevSelect
v-model="formContent.devIds" v-model="formContent.devIds"
title="被检设备列表" :titles="['被检设备列表', '已选被检设备列表']"
filter-placeholder="请输入内容搜索" filter-placeholder="请输入内容搜索"
:data="devData" :data="devData"
:height="230" :height="230"
@@ -414,7 +414,19 @@ const generateData = () => {
i.disabled = allDisabled.value 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() { function useMetaInfo() {