396 lines
8.7 KiB
Vue
396 lines
8.7 KiB
Vue
<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"
|
||
:class="{ 'is-selected': item.full_path === modelValue }"
|
||
>
|
||
<!-- 左侧热区:点击进入下级或选中 -->
|
||
<div
|
||
class="item-main"
|
||
:class="{ 'has-children': item.children && item.children.length > 0 }"
|
||
@click="handleItemClick(item)"
|
||
>
|
||
<el-icon class="item-icon"><Location /></el-icon>
|
||
<span class="item-name">{{ item.name }}</span>
|
||
<el-icon v-if="item.children && item.children.length > 0" class="item-arrow">
|
||
<ArrowRight />
|
||
</el-icon>
|
||
</div>
|
||
<!-- 右侧操作区 -->
|
||
<div class="item-actions">
|
||
<el-tag v-if="!item.children || item.children.length === 0" type="info" size="small">
|
||
末级
|
||
</el-tag>
|
||
<el-button
|
||
v-else
|
||
type="primary"
|
||
size="small"
|
||
@click.stop="handleDrillDown(item)"
|
||
>
|
||
进入下级
|
||
<el-icon><ArrowRight /></el-icon>
|
||
</el-button>
|
||
</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 || []
|
||
})
|
||
|
||
// 监听弹窗显示状态,恢复选中层级
|
||
watch(popoverVisible, (visible) => {
|
||
if (visible) {
|
||
restoreSelection()
|
||
}
|
||
})
|
||
|
||
// 根据 modelValue 恢复选择状态
|
||
const restoreSelection = () => {
|
||
if (!props.modelValue) {
|
||
// 无选中值,重置到顶层
|
||
currentPath.value = []
|
||
return
|
||
}
|
||
|
||
// 根据 full_path 查找父级路径
|
||
const path = findPathByFullPath(props.options, props.modelValue)
|
||
if (path) {
|
||
// 找到路径:还原 currentPath(不包含最后一个节点,因为它是当前选中的节点)
|
||
currentPath.value = path
|
||
} else {
|
||
// 未找到(可能树结构已变化),重置到顶层
|
||
currentPath.value = []
|
||
}
|
||
}
|
||
|
||
// 根据 full_path 在树中查找从根到目标节点的路径
|
||
const findPathByFullPath = (
|
||
tree: WarehouseItem[],
|
||
targetFullPath: string,
|
||
currentPath: WarehouseItem[] = []
|
||
): WarehouseItem[] | null => {
|
||
for (const node of tree) {
|
||
// 检查当前节点是否匹配
|
||
if (node.full_path === targetFullPath) {
|
||
return currentPath
|
||
}
|
||
|
||
// 递归检查子节点
|
||
if (node.children && node.children.length > 0) {
|
||
const result = findPathByFullPath(node.children, targetFullPath, [...currentPath, node])
|
||
if (result) {
|
||
return result
|
||
}
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
// 处理弹窗显示/隐藏
|
||
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 handleItemClick = (item: WarehouseItem) => {
|
||
if (item.children && item.children.length > 0) {
|
||
// 有子节点:进入下一级
|
||
handleDrillDown(item)
|
||
} else {
|
||
// 末级:直接选中
|
||
handleSelect(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: 320px;
|
||
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;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.location-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.location-item:hover {
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
/* 选中高亮样式 */
|
||
.location-item.is-selected {
|
||
background: rgba(64, 158, 255, 0.1);
|
||
}
|
||
|
||
.location-item.is-selected .item-name {
|
||
color: #409eff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 左侧主热区 - 占80%宽度 */
|
||
.item-main {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 14px 12px;
|
||
cursor: pointer;
|
||
min-width: 0;
|
||
}
|
||
|
||
.item-main.has-children {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.item-main:hover {
|
||
background: rgba(64, 158, 255, 0.08);
|
||
}
|
||
|
||
.item-icon {
|
||
color: #409eff;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.item-name {
|
||
font-size: 14px;
|
||
color: #303133;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.item-arrow {
|
||
color: #c0c4cc;
|
||
font-size: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 右侧操作区 */
|
||
.item-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
padding-right: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
</style>
|