<template>
|
<el-dialog title="选择产品结构树设备(可多选)" :visible.sync="localVisible" width="680px" top="4vh" custom-class="product-tree-dialog" append-to-body :lock-scroll="true" :close-on-click-modal="false" @open="load">
|
<div class="picker-content">
|
<el-input v-model="keyword" placeholder="输入名称或编码筛选" clearable prefix-icon="el-icon-search" style="margin-bottom:10px"></el-input>
|
<div class="tree-scroll">
|
<el-tree
|
ref="tree"
|
:data="tree"
|
node-key="id"
|
v-loading="loading"
|
lazy
|
:load="loadNode"
|
show-checkbox
|
check-strictly
|
:filter-node-method="filterNode"
|
:props="treeProps"
|
@check="handleCheck"
|
>
|
<span class="tree-node" slot-scope="{ data }">
|
<span>{{ data.name }}</span>
|
<small v-if="data.code">{{ data.code }}</small>
|
</span>
|
</el-tree>
|
</div>
|
<div class="selected-panel">
|
<div class="picker-count">已选择 {{ checkedCount }} 台设备</div>
|
<div v-if="selectedDevices.length" class="selected-list">
|
<el-tag
|
v-for="device in selectedDevices"
|
:key="device.id"
|
closable
|
size="small"
|
@close="removeSelected(device)"
|
>
|
<span v-if="device.xNo">{{ device.xNo }} / </span>{{ device.name }}
|
</el-tag>
|
</div>
|
</div>
|
</div>
|
<template slot="footer">
|
<el-button @click="localVisible = false">取消</el-button>
|
<el-button type="primary" :disabled="checkedCount === 0" @click="confirm">确认选择</el-button>
|
</template>
|
</el-dialog>
|
</template>
|
<script>
|
import api from '@/api'
|
export default {
|
name: 'ProductTreePicker',
|
props: {
|
visible: { type: Boolean, default: false },
|
xNo: { type: String, default: '' },
|
selectedIds: { type: Array, default: () => [] }
|
},
|
data() {
|
return { localVisible: false, keyword: '', tree: [], checkedCount: 0, selectedDevices: [], selectedMap: {}, loading: false }
|
},
|
computed: {
|
treeProps() {
|
return {
|
label: 'name',
|
children: 'children',
|
disabled: data => data.nodeType !== 'DEVICE'
|
}
|
}
|
},
|
watch: {
|
visible(value) {
|
this.localVisible = value
|
},
|
localVisible(value) { this.$emit('update:visible', value) },
|
keyword(value) { this.$refs.tree && this.$refs.tree.filter(value) },
|
xNo() { if (this.localVisible) this.load() },
|
selectedIds() { if (this.localVisible) this.$nextTick(this.restoreChecked) }
|
},
|
methods: {
|
load() {
|
const lockedXNo = String(this.xNo || '').trim()
|
this.loading = true
|
const ids = (this.selectedIds || []).map(String).filter(Boolean)
|
const treeRequest = lockedXNo
|
? api.get('/system/tree/product/side-roots', { params: { xNo: lockedXNo } })
|
: api.get('/system/tree/product/roots')
|
const selectedRequest = ids.length
|
? api.get('/system/tree/product/selected', { params: { ids: ids.join(',') } })
|
: Promise.resolve({ data: [] })
|
Promise.all([treeRequest, selectedRequest]).then(([treeRes, selectedRes]) => {
|
this.tree = treeRes.data || []
|
this.selectedMap = (selectedRes.data || []).reduce((map, node) => {
|
map[String(node.id)] = node
|
return map
|
}, {})
|
this.keyword = ''
|
this.$nextTick(() => { this.restoreChecked(); this.syncSelected() })
|
}).finally(() => { this.loading = false })
|
},
|
loadNode(node, resolve) {
|
if (!node || !node.data || !node.data.id) {
|
resolve([])
|
return
|
}
|
api.get('/system/tree/product/children', { params: { parentId: node.data.id } })
|
.then(res => {
|
resolve(res.data || [])
|
this.$nextTick(this.restoreChecked)
|
})
|
.catch(() => resolve([]))
|
},
|
restoreChecked() {
|
if (!this.$refs.tree) return
|
const ids = Object.keys(this.selectedMap).filter(id => Boolean(this.$refs.tree.getNode(id)))
|
this.$refs.tree.setCheckedKeys(ids)
|
this.syncSelected()
|
},
|
handleCheck(node) {
|
const tree = this.$refs.tree
|
const treeNode = tree && tree.getNode(node.id)
|
this.syncSelected()
|
if (treeNode && treeNode.checked) {
|
const xNos = Array.from(new Set(this.selectedDevices.map(item => item.xNo).filter(Boolean)))
|
if (xNos.length > 1) {
|
tree.setChecked(node.id, false, true)
|
this.syncSelected()
|
this.$message.warning('同一故障只能选择同一个rk_X号下的设备')
|
}
|
}
|
},
|
syncSelected() {
|
const tree = this.$refs.tree
|
if (!tree) return
|
const selectedMap = { ...this.selectedMap }
|
const checked = tree.getCheckedNodes().filter(node => node.nodeType === 'DEVICE')
|
const checkedIds = new Set(checked.map(node => String(node.id)))
|
Object.keys(selectedMap).forEach(id => {
|
if (tree.getNode(id) && !checkedIds.has(id)) delete selectedMap[id]
|
})
|
checked.forEach(node => { selectedMap[String(node.id)] = this.decorateNode(node) })
|
this.selectedMap = selectedMap
|
this.selectedDevices = Object.values(selectedMap)
|
this.checkedCount = this.selectedDevices.length
|
},
|
decorateNode(node) {
|
if (!this.$refs.tree) return { ...node }
|
const pathNodes = []
|
let current = this.$refs.tree.getNode(node.id)
|
while (current && current.data) {
|
pathNodes.unshift(current.data)
|
current = current.parent
|
}
|
const xNode = pathNodes.find(item => item.nodeLevel === 2)
|
const parentPathNames = pathNodes
|
.slice(0, -1)
|
.filter(item => item.nodeLevel > 2)
|
.map(item => item.name)
|
return { ...node, pathNames: pathNodes.map(item => item.name), parentPathNames, xNo: xNode ? xNode.name : '' }
|
},
|
removeSelected(device) {
|
const selectedMap = { ...this.selectedMap }
|
delete selectedMap[String(device.id)]
|
this.selectedMap = selectedMap
|
if (this.$refs.tree && this.$refs.tree.getNode(device.id)) this.$refs.tree.setChecked(device.id, false, true)
|
this.syncSelected()
|
},
|
deviceNodes() {
|
if (!this.$refs.tree) return []
|
return this.$refs.tree.getCheckedNodes().filter(node => node.nodeType === 'DEVICE')
|
},
|
filterNode(value, data) {
|
if (!value) return true
|
return (data.name || '').includes(value) || (data.code || '').includes(value)
|
},
|
confirm() {
|
const nodes = this.selectedDevices
|
if (!nodes.length) return this.$message.warning('请选择设备')
|
const selected = nodes
|
const xNos = Array.from(new Set(selected.map(item => item.xNo).filter(Boolean)))
|
if (xNos.length > 1) {
|
return this.$message.warning('同一故障只能选择同一个rk_X号下的设备')
|
}
|
this.$emit('select', selected)
|
this.localVisible = false
|
}
|
}
|
}
|
</script>
|
<style>
|
.product-tree-dialog {
|
display: flex;
|
flex-direction: column;
|
max-height: calc(100vh - 32px);
|
overflow: hidden;
|
}
|
.product-tree-dialog .el-dialog__header,
|
.product-tree-dialog .el-dialog__footer {
|
flex: 0 0 auto;
|
}
|
.product-tree-dialog .el-dialog__body {
|
display: flex;
|
flex: 1 1 auto;
|
min-height: 0;
|
padding-bottom: 0;
|
}
|
.picker-content {
|
display: flex;
|
flex-direction: column;
|
width: 100%;
|
height: 62vh;
|
min-height: 320px;
|
max-height: 540px;
|
}
|
.tree-scroll {
|
flex: 1 1 auto;
|
min-height: 180px;
|
overflow: auto;
|
padding: 6px;
|
border: 1px solid #ebeef5;
|
border-radius: 4px;
|
}
|
.selected-panel {
|
flex: 0 0 auto;
|
margin-top: 10px;
|
padding-top: 10px;
|
border-top: 1px solid #ebeef5;
|
}
|
.picker-count {
|
color: #6d7f91;
|
font-size: 13px;
|
line-height: 24px;
|
}
|
.selected-list {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 6px;
|
max-height: 76px;
|
overflow: auto;
|
margin-top: 6px;
|
}
|
.selected-list .el-tag {
|
max-width: 100%;
|
height: auto;
|
white-space: normal;
|
line-height: 20px;
|
}
|
.tree-node { display: flex; align-items: center; gap: 8px; }
|
.tree-node small { color: #95a2b0; }
|
</style>
|