feat: 封装下钻式库位选择器,并修复层级颜色识别问题
This commit is contained in:
286
inventory-web/src/components/WarehouseSelector.vue
Normal file
286
inventory-web/src/components/WarehouseSelector.vue
Normal 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>
|
||||
@ -214,14 +214,14 @@ const savePrinterConfig = async () => {
|
||||
|
||||
// ==================== 库位管理相关 ====================
|
||||
// 根据层级返回"新增下级"按钮颜色
|
||||
// 第1层(顶级)=primary蓝色, 第2层=success绿色, 第3层=warning橙色, 第4层及以上=info灰色
|
||||
// el-tree中顶层节点level=1,第1层=primary蓝色,第2层=success绿色,第3层=warning橙色,第4层及以上=danger红色
|
||||
const getAddBtnType = (level: number) => {
|
||||
const levelTypes: Record<number, string> = {
|
||||
0: 'primary', // 第1层(顶级)
|
||||
1: 'success', // 第2层
|
||||
2: 'warning', // 第3层
|
||||
1: 'primary', // 第1层(顶级)
|
||||
2: 'success', // 第2层
|
||||
3: 'warning', // 第3层
|
||||
}
|
||||
return levelTypes[level] || 'info' // 第4层及以上
|
||||
return levelTypes[level] || 'danger' // 第4层及以上
|
||||
}
|
||||
|
||||
const warehouseDialogVisible = ref(false)
|
||||
|
||||
@ -327,21 +327,10 @@
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="库位" prop="warehouse_location">
|
||||
<el-tree-select
|
||||
<WarehouseSelector
|
||||
v-model="form.warehouse_location"
|
||||
:data="warehouseOptions"
|
||||
:props="{ label: 'full_path', value: 'full_path', children: 'children' }"
|
||||
placeholder="请选择库位"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
filterable
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<span>{{ data.name }}</span>
|
||||
</template>
|
||||
</el-tree-select>
|
||||
:options="warehouseOptions"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -660,6 +649,7 @@ import {
|
||||
import {getLabelPreview, executePrint} from '@/api/common/print'
|
||||
import { getWarehouseTree } from '@/api/common/warehouse'
|
||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||
import WarehouseSelector from '@/components/WarehouseSelector.vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// ------------------------------------
|
||||
|
||||
@ -287,21 +287,10 @@
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="6"><el-form-item label="SKU" prop="sku"><el-input v-model="form.sku" placeholder="自动生成" disabled /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="库位" prop="warehouse_location">
|
||||
<el-tree-select
|
||||
<WarehouseSelector
|
||||
v-model="form.warehouse_location"
|
||||
:data="warehouseOptions"
|
||||
:props="{ label: 'full_path', value: 'full_path', children: 'children' }"
|
||||
placeholder="请选择库位"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
filterable
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<span>{{ data.name }}</span>
|
||||
</template>
|
||||
</el-tree-select>
|
||||
:options="warehouseOptions"
|
||||
/>
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="入库日期"><el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled /></el-form-item></el-col>
|
||||
</el-row>
|
||||
@ -528,6 +517,7 @@ import {
|
||||
} from '@/api/inbound/product'
|
||||
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||
import WarehouseSelector from '@/components/WarehouseSelector.vue'
|
||||
import { getLabelPreview, executePrint } from '@/api/common/print'
|
||||
import { getWarehouseTree } from '@/api/common/warehouse'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
@ -346,21 +346,10 @@
|
||||
<el-col :span="6"><el-form-item label="编码/SKU" prop="sku"><el-input v-model="form.sku" placeholder="系统自动生成" disabled/></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="入库日期" prop="in_date"><el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled/></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="库位" prop="warehouse_location">
|
||||
<el-tree-select
|
||||
<WarehouseSelector
|
||||
v-model="form.warehouse_location"
|
||||
:data="warehouseOptions"
|
||||
:props="{ label: 'full_path', value: 'full_path', children: 'children' }"
|
||||
placeholder="请选择库位"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
filterable
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<span>{{ data.name }}</span>
|
||||
</template>
|
||||
</el-tree-select>
|
||||
:options="warehouseOptions"
|
||||
/>
|
||||
</el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
@ -592,6 +581,7 @@ import {
|
||||
} from '@/api/inbound/semi'
|
||||
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||
import WarehouseSelector from '@/components/WarehouseSelector.vue'
|
||||
import {getLabelPreview, executePrint} from '@/api/common/print'
|
||||
import { getWarehouseTree } from '@/api/common/warehouse'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
Reference in New Issue
Block a user