refactor(warehouse-picker): 重构逻辑库区选择器组件 feat(actual-warehouse-picker): 新增真实库区选择器组件 fix(easycode.vue): 调整钢卷质量状态校验逻辑 style(search.vue): 优化表单样式和字段显示
402 lines
11 KiB
Vue
402 lines
11 KiB
Vue
<template>
|
||
<view class="klp-actual-warehouse-picker">
|
||
<!-- 输入框触发器 - 全部修复:阻止冒泡+固定右侧按钮+文字颜色差异化 -->
|
||
<view class="picker-input" @click="openPopup">
|
||
<!-- 文本区域:未选择=浅灰色(占位色) 选中=深灰色(内容色) -->
|
||
<span class="picker-text" :class="{selected: selectedNode.actualWarehouseCode}">
|
||
{{ selectedNode.actualWarehouseCode || '请选择真实库区' }}
|
||
</span>
|
||
<!-- ✅ 终极防冒泡:原生view包裹图标 + stop.prevent 双重阻断 + 绝对定位固定右侧 -->
|
||
<view class="clear-btn-wrap" v-show="selectedNode.actualWarehouseCode" @click.stop.prevent="handleClear">
|
||
<uni-icons type="clear" size="24" color="#999"></uni-icons>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- uni-popup 原生组件 纯属性使用 无任何样式 -->
|
||
<uni-popup ref="popup" type="bottom" background-color="#fff" :mask-click="true" @mask-click="closePopup">
|
||
<!-- ✅ 弹窗内部根容器 承载所有样式 固定高度核心 -->
|
||
<view class="popup-inner-container">
|
||
<!-- 弹窗头部标题 -->
|
||
<view class="popup-header">选择实际库区</view>
|
||
|
||
<!-- 第一层横向滚动tab -->
|
||
<scroll-view scroll-x class="tab-scroll-view" scroll-with-animation>
|
||
<view class="tab-item-wrap">
|
||
<view class="tab-item" :class="{active: activeFirstId === item.actualWarehouseId}"
|
||
@click="handleFirstTabClick(item)" v-for="item in treeData" :key="item.actualWarehouseId">
|
||
{{ item.actualWarehouseName }}
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 第二层横向滚动tab,点选第一层tab后才出现 -->
|
||
<scroll-view v-if="activeFirstId" scroll-x class="tab-scroll-view tab-second" scroll-with-animation>
|
||
<view class="tab-item-wrap">
|
||
<view class="tab-item" :class="{active: activeSecondId === item.actualWarehouseId}"
|
||
@click="handleSecondTabClick(item)" v-for="item in secondLevelList" :key="item.actualWarehouseId">
|
||
{{ item.actualWarehouseName }}
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- ✅✅✅ 核心修改:筛选框抽离出来 独立节点 固定置顶 永不滚动 -->
|
||
<view class="search-box">
|
||
<uni-icons type="search" size="24" color="#999"></uni-icons>
|
||
<input type="text" v-model="searchKeyword" placeholder="输入编码搜索仓库" class="search-input"
|
||
placeholder-class="search-placeholder" />
|
||
<uni-icons v-if="searchKeyword" type="clear" size="24" color="#999" class="clear-icon"
|
||
@click="searchKeyword = ''"></uni-icons>
|
||
</view>
|
||
|
||
<!-- ✅✅✅ 仅这个区域滚动:纯数据列表区 -->
|
||
<view class="select-scroll-content">
|
||
<!-- 加载中状态 -->
|
||
<view class="loading" v-if="loading">加载中...</view>
|
||
|
||
<!-- 情况1:只选中第一层 → 展示筛选后的二级数据 -->
|
||
<view v-else-if="activeFirstId && !activeSecondId" class="select-list">
|
||
<view class="select-item" @click="handleSelectSecondItem(item)" v-for="item in filterSecondList"
|
||
:key="item.actualWarehouseId">
|
||
{{ item.name }}
|
||
<text class="code">{{ item.actualWarehouseCode }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 情况2:选中第二层 → 展示筛选后的三级数据 -->
|
||
<view v-else-if="activeSecondId" class="select-list">
|
||
<view class="select-item" @click="handleSelectThirdItem(item)" v-for="item in filterThirdList"
|
||
:key="item.actualWarehouseId">
|
||
{{ item.name }}
|
||
<text class="code">{{ item.actualWarehouseCode }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 空数据展示 -->
|
||
<view class="empty" v-else-if="showEmpty">暂无仓库数据</view>
|
||
<!-- 筛选后无数据的兜底提示 -->
|
||
<view class="empty"
|
||
v-else-if="searchKeyword && (filterSecondList.length === 0 && filterThirdList.length === 0)">暂无匹配的仓库编码
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</uni-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import {
|
||
treeActualWarehouseTwoLevel,
|
||
listActualWarehouse
|
||
} from '@/api/wms/actualWarehouse.js'
|
||
|
||
export default {
|
||
name: "klp-actual-warehouse-picker",
|
||
props: {
|
||
showEmpty: {
|
||
default: false,
|
||
type: Boolean
|
||
},
|
||
value: {
|
||
default: undefined,
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
treeData: [],
|
||
secondLevelList: [],
|
||
thirdLevelList: [],
|
||
selectedNode: {},
|
||
activeFirstId: '',
|
||
activeSecondId: '',
|
||
loading: false,
|
||
searchKeyword: '' // 筛选关键词
|
||
};
|
||
},
|
||
computed: {
|
||
// 完美双向绑定
|
||
innerValue: {
|
||
get() {
|
||
return this.value
|
||
},
|
||
set(val) {
|
||
this.$emit('input', val);
|
||
this.$emit('change', this.selectedNode);
|
||
}
|
||
},
|
||
// ✅ 精准匹配:只做 actualWarehouseCode 仓库编码的模糊筛选(纯编码匹配,无名称)
|
||
filterSecondList() {
|
||
if (!this.searchKeyword) return this.secondLevelList
|
||
const keyword = this.searchKeyword.trim().toLowerCase()
|
||
return this.secondLevelList.filter(item => {
|
||
const code = item.actualWarehouseCode?.toLowerCase() || ''
|
||
return code.includes(keyword)
|
||
})
|
||
},
|
||
// ✅ 精准匹配:只做 actualWarehouseCode 仓库编码的模糊筛选(纯编码匹配,无名称)
|
||
filterThirdList() {
|
||
if (!this.searchKeyword) return this.thirdLevelList
|
||
const keyword = this.searchKeyword.trim().toLowerCase()
|
||
return this.thirdLevelList.filter(item => {
|
||
const code = item.actualWarehouseCode?.toLowerCase() || ''
|
||
return code.includes(keyword)
|
||
})
|
||
}
|
||
},
|
||
created() {
|
||
this.getTwoLevelData()
|
||
},
|
||
methods: {
|
||
async getTwoLevelData() {
|
||
try {
|
||
this.loading = true
|
||
const res = await treeActualWarehouseTwoLevel()
|
||
this.treeData = res.data || []
|
||
} catch (err) {
|
||
uni.showToast({
|
||
title: '加载仓库数据失败',
|
||
icon: 'none'
|
||
})
|
||
console.error('仓库数据加载异常:', err)
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
// ✅ 新增:清空选中的仓库数据 + 重置所有状态
|
||
handleClear() {
|
||
// 1. 清空选中的仓库节点数据
|
||
this.selectedNode = {}
|
||
// 2. 双向绑定同步清空,父组件v-model能拿到空值
|
||
this.innerValue = undefined
|
||
// 3. 重置一级/二级选中的tab id
|
||
this.activeFirstId = ''
|
||
this.activeSecondId = ''
|
||
// 4. 清空二级、三级仓库列表数据
|
||
this.secondLevelList = []
|
||
this.thirdLevelList = []
|
||
// 5. 清空搜索关键词
|
||
this.searchKeyword = ''
|
||
// 可选:清空成功的轻提示,和你原有提示风格一致
|
||
uni.showToast({
|
||
title: '已清空选择',
|
||
icon: 'none',
|
||
duration: 1200
|
||
})
|
||
},
|
||
openPopup() {
|
||
this.$refs.popup.open()
|
||
},
|
||
closePopup() {
|
||
this.$refs.popup.close()
|
||
this.searchKeyword = ''
|
||
},
|
||
handleFirstTabClick(item) {
|
||
this.activeFirstId = item.actualWarehouseId
|
||
this.activeSecondId = ''
|
||
this.thirdLevelList = []
|
||
this.searchKeyword = ''
|
||
this.secondLevelList = item.children || []
|
||
},
|
||
async handleSecondTabClick(item) {
|
||
this.activeSecondId = item.actualWarehouseId
|
||
this.thirdLevelList = []
|
||
this.searchKeyword = ''
|
||
try {
|
||
this.loading = true
|
||
const res = await listActualWarehouse({
|
||
parentId: item.actualWarehouseId
|
||
})
|
||
this.thirdLevelList = res.data || []
|
||
} catch (err) {
|
||
uni.showToast({
|
||
title: '加载库区数据失败',
|
||
icon: 'none'
|
||
})
|
||
console.error('三级仓库加载异常:', err)
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
handleSelectSecondItem(item) {
|
||
this.selectedNode = item
|
||
this.innerValue = item.actualWarehouseId
|
||
this.closePopup()
|
||
},
|
||
handleSelectThirdItem(item) {
|
||
this.selectedNode = item
|
||
this.innerValue = item.actualWarehouseId
|
||
this.closePopup()
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.klp-actual-warehouse-picker {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
// 输入框触发器样式 - 全部优化:父容器相对定位 + 文本颜色区分 + 按钮绝对定位固定右侧
|
||
.picker-input {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
line-height: 88rpx;
|
||
padding: 0 30rpx;
|
||
// ✅ 给右侧清除按钮预留空间,防止文本和按钮重叠
|
||
padding-right: 70rpx;
|
||
border: 1px solid #e5e5e5;
|
||
background-color: #f8f9fa;
|
||
border-radius: 12rpx;
|
||
font-size: 28rpx;
|
||
box-sizing: border-box;
|
||
// ✅ 关键:父容器相对定位,为子元素绝对定位做铺垫
|
||
position: relative;
|
||
|
||
// ✅ 文本颜色差异化核心样式
|
||
.picker-text {
|
||
// 未选择仓库 - 占位文本颜色(浅灰色,placeholder色)
|
||
color: #999;
|
||
&.selected {
|
||
// 选中仓库后 - 内容文本颜色(深灰色,主色)
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
// ✅ 清除按钮:绝对定位 死死固定在输入框最右侧 永不偏移
|
||
.clear-btn-wrap {
|
||
position: absolute;
|
||
right: 20rpx; // 距离右侧固定间距
|
||
top: 50%;
|
||
transform: translateY(-50%); // 垂直居中完美对齐
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
// 按钮点击按压反馈,和你原有风格一致
|
||
&:active {
|
||
background-color: #e5e5e5;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ✅ 弹窗内部根容器 - 固定高度核心 样式100%生效
|
||
.popup-inner-container {
|
||
width: 100%;
|
||
height: 80vh; // 弹窗固定高度,适配所有手机
|
||
// max-height: 900rpx; // 大屏高度上限,防止过高
|
||
overflow: hidden; // 禁止整体滚动
|
||
display: flex;
|
||
flex-direction: column; // 垂直布局核心
|
||
}
|
||
|
||
// 弹窗头部标题 - 固定不滚动
|
||
.popup-header {
|
||
height: 90rpx;
|
||
line-height: 90rpx;
|
||
text-align: center;
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
border-bottom: 1px solid #f5f5f5;
|
||
flex-shrink: 0; // 固定高度 不被压缩
|
||
}
|
||
|
||
// 横向滚动tab容器 - 固定不滚动
|
||
.tab-scroll-view {
|
||
width: 100%;
|
||
white-space: nowrap;
|
||
padding: 20rpx 0;
|
||
border-bottom: 1px solid #f5f5f5;
|
||
flex-shrink: 0; // 固定高度 不被压缩
|
||
|
||
.tab-item-wrap {
|
||
display: inline-block;
|
||
padding: 0 20rpx;
|
||
}
|
||
|
||
.tab-item {
|
||
display: inline-block;
|
||
padding: 12rpx 30rpx;
|
||
margin: 0 10rpx;
|
||
font-size: 28rpx;
|
||
border-radius: 20rpx;
|
||
color: #666;
|
||
|
||
&.active {
|
||
background: #007aff;
|
||
color: #fff;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 二级tab区分隔线
|
||
.tab-second {
|
||
border-top: 1px solid #f5f5f5;
|
||
}
|
||
|
||
// ✅✅✅ 筛选框样式 - 固定置顶 永不滚动 核心样式
|
||
.search-box {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 16rpx 30rpx;
|
||
margin: 10rpx 20rpx 0;
|
||
background: #f8f8f8;
|
||
border-radius: 30rpx;
|
||
flex-shrink: 0; // 关键:固定高度 不参与滚动 不被压缩
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
height: 50rpx;
|
||
line-height: 50rpx;
|
||
font-size: 26rpx;
|
||
color: #333;
|
||
margin: 0 10rpx;
|
||
}
|
||
|
||
.search-placeholder {
|
||
font-size: 26rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.clear-icon {
|
||
padding: 0 8rpx;
|
||
}
|
||
}
|
||
|
||
// ✅✅✅ 唯一滚动区域:纯数据列表区
|
||
.select-scroll-content {
|
||
flex: 1; // 占满弹窗剩余所有高度
|
||
overflow-y: auto; // 仅此处滚动 核心!
|
||
padding: 10rpx 0;
|
||
box-sizing: border-box;
|
||
|
||
.select-list {
|
||
.select-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 30rpx;
|
||
font-size: 40rpx;
|
||
border-bottom: 1px solid #f5f5f5;
|
||
|
||
&:active {
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.code {
|
||
color: #000;
|
||
font-size: 30rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.loading,
|
||
.empty {
|
||
text-align: center;
|
||
padding: 50rpx 0;
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
</style> |