Files
KCGL/inventory-web/src/components/WarehouseSelector.vue

396 lines
8.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>