feat: 封装下钻式库位选择器,并修复层级颜色识别问题

This commit is contained in:
DXC
2026-03-06 15:11:30 +08:00
parent 8aaf45468e
commit f9eb3e9646
5 changed files with 303 additions and 47 deletions

View File

@ -0,0 +1,286 @@
<template>
<div class="warehouse-selector">
<el-input
:model-value="modelValue"
readonly
placeholder="请选择库位"
@click="handleOpen"
clearable
@clear="handleClear"
>
<template #suffix>
<el-icon class="clear-icon" @click.stop="handleOpen">
<ArrowDown />
</el-icon>
</template>
</el-input>
<el-popover
ref="popoverRef"
:visible="popoverVisible"
placement="bottom-start"
:width="380"
trigger="click"
@update:visible="handleVisibleChange"
>
<template #reference>
<div ref="triggerRef" style="width: 0; height: 0; overflow: hidden;"></div>
</template>
<div class="selector-container">
<!-- 顶部导航区 -->
<div class="selector-header">
<div class="header-left">
<el-button
v-if="currentPath.length > 0"
type="primary"
link
size="small"
@click="handleBack"
>
<el-icon><ArrowLeft /></el-icon>
返回上一级
</el-button>
</div>
<div class="header-right">
<el-breadcrumb separator="/">
<el-breadcrumb-item>
<span class="breadcrumb-root" @click="handleGoHome">全部</span>
</el-breadcrumb-item>
<el-breadcrumb-item v-for="(item, index) in currentPath" :key="index">
<span class="breadcrumb-item">{{ item.name }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
<!-- 列表展示区 -->
<div class="selector-body">
<div v-if="currentList.length === 0" class="empty-tip">
当前层级无库位数据
</div>
<ul v-else class="location-list">
<li
v-for="item in currentList"
:key="item.id"
class="location-item"
@click="handleSelect(item)"
>
<div class="item-left">
<el-icon><Location /></el-icon>
<span class="item-name">{{ item.name }}</span>
</div>
<div class="item-right">
<el-button
v-if="item.children && item.children.length > 0"
type="primary"
link
size="small"
@click.stop="handleDrillDown(item)"
>
进入下级
<el-icon><ArrowRight /></el-icon>
</el-button>
<el-tag v-else type="info" size="small">末级</el-tag>
</div>
</li>
</ul>
</div>
</div>
</el-popover>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ArrowDown, ArrowLeft, ArrowRight, Location } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
interface WarehouseItem {
id: number
name: string
full_path: string
level: number
children?: WarehouseItem[]
}
interface Props {
modelValue?: string
options: WarehouseItem[]
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
options: () => []
})
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const popoverRef = ref()
const popoverVisible = ref(false)
const triggerRef = ref<HTMLElement>()
// 当前导航路径(保存每一层的节点信息)
const currentPath = ref<WarehouseItem[]>([])
// 当前显示的列表数据
const currentList = computed(() => {
if (currentPath.value.length === 0) {
// 顶层:显示根节点列表
return props.options
}
// 非顶层:显示当前层级最后一个节点的 children
const lastNode = currentPath.value[currentPath.value.length - 1]
return lastNode?.children || []
})
// 处理弹窗显示/隐藏
const handleVisibleChange = (visible: boolean) => {
popoverVisible.value = visible
if (!visible) {
// 关闭时重置导航
currentPath.value = []
}
}
// 打开弹窗
const handleOpen = () => {
popoverVisible.value = true
}
// 清空选择
const handleClear = () => {
emit('update:modelValue', '')
currentPath.value = []
}
// 返回上一级
const handleBack = () => {
if (currentPath.value.length > 0) {
currentPath.value.pop()
}
}
// 返回顶层
const handleGoHome = () => {
currentPath.value = []
}
// 进入下一级
const handleDrillDown = (item: WarehouseItem) => {
currentPath.value.push(item)
}
// 选择当前项
const handleSelect = (item: WarehouseItem) => {
emit('update:modelValue', item.full_path)
popoverVisible.value = false
currentPath.value = []
ElMessage.success(`已选择库位:${item.full_path}`)
}
</script>
<style scoped>
.warehouse-selector {
width: 100%;
}
.clear-icon {
cursor: pointer;
}
.clear-icon:hover {
color: #409eff;
}
.selector-container {
margin: -12px;
}
.selector-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
border-bottom: 1px solid #ebeef5;
background: #f5f7fa;
}
.header-left {
flex-shrink: 0;
}
.header-right {
flex: 1;
overflow: hidden;
}
.breadcrumb-root {
cursor: pointer;
color: #409eff;
}
.breadcrumb-root:hover {
text-decoration: underline;
}
.breadcrumb-item {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.selector-body {
max-height: 300px;
overflow-y: auto;
}
.empty-tip {
padding: 40px 0;
text-align: center;
color: #909399;
}
.location-list {
list-style: none;
margin: 0;
padding: 0;
}
.location-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
cursor: pointer;
transition: background 0.2s;
}
.location-item:hover {
background: #f5f7fa;
}
.item-left {
display: flex;
align-items: center;
gap: 8px;
}
.item-left .el-icon {
color: #409eff;
}
.item-name {
font-size: 14px;
color: #303133;
}
.item-right {
display: flex;
align-items: center;
gap: 8px;
}
</style>