Files
klp-mono/apps/hand-factory/components/klp-material-picker/klp-material-picker.vue
砂糖 5bae1f405b feat: 更新版本号至1.3.23并新增真实库区选择器组件
refactor(warehouse-picker): 重构逻辑库区选择器组件
feat(actual-warehouse-picker): 新增真实库区选择器组件
fix(easycode.vue): 调整钢卷质量状态校验逻辑
style(search.vue): 优化表单样式和字段显示
2026-01-13 15:49:43 +08:00

521 lines
16 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>
<view>
<!-- 物品类型选择 -->
<view class="form-item form-item-optional">
<text class="form-label-optional">物品类型</text>
<view class="picker-input" @click="!disabled && showItemTypePicker()" :class="{ 'picker-input-disabled': disabled }">
<text class="picker-text" :class="{ 'picker-placeholder': !itemType }">
{{ itemType === 'product' ? '成品' : itemType === 'raw_material' ? '原料' : '请选择物品类型' }}
</text>
<!-- 互斥显示选中类型=清除按钮 未选中=下拉箭头 -->
<!-- <text class="picker-clear" v-if="itemType && !disabled" @click.stop.prevent="handleClearItemType"></text> -->
<text class="picker-arrow"></text>
</view>
</view>
<!-- 产品选择仅当选择了成品类型时显示 -->
<view class="form-item form-item-optional" v-if="itemType === 'product'">
<text class="form-label-optional">选择产品</text>
<view class="picker-input" @click="!disabled && showProductPicker()" :class="{ 'picker-input-disabled': disabled }">
<text class="picker-text" :class="{ 'picker-placeholder': !selectedName }">
{{ loadingProducts ? '加载中...' : selectedName || '请选择产品' }}
</text>
<!-- 互斥显示选中产品=清除按钮 未选中/加载中=下拉箭头 -->
<text class="picker-clear" v-if="selectedName && !disabled && !loadingProducts" @click.stop.prevent="handleClearProduct"></text>
<text class="picker-arrow" v-else-if="!disabled && !loadingProducts"></text>
</view>
</view>
<!-- 原材料选择仅当选择了原料类型时显示 -->
<view class="form-item form-item-optional" v-if="itemType === 'raw_material'">
<text class="form-label-optional">选择原材料</text>
<view class="picker-input" @click="!disabled && showRawMaterialPicker()" :class="{ 'picker-input-disabled': disabled }">
<text class="picker-text" :class="{ 'picker-placeholder': !selectedName }">
{{ loadingRawMaterials ? '加载中...' : selectedName || '请选择原材料' }}
</text>
<!-- 互斥显示选中原料=清除按钮 未选中/加载中=下拉箭头 -->
<text class="picker-clear" v-if="selectedName && !disabled && !loadingRawMaterials" @click.stop.prevent="handleClearRawMaterial"></text>
<text class="picker-arrow" v-else-if="!disabled && !loadingRawMaterials"></text>
</view>
</view>
<!-- 产品选择弹窗仅显示产品 -->
<uni-popup ref="productPopup" type="bottom">
<view class="warehouse-popup">
<view class="popup-header">
<text class="popup-title">选择产品</text>
<text class="popup-close" @click="closeProductPicker"></text>
</view>
<view class="popup-search">
<input v-model="productSearchKeyword" @input="filterProducts" placeholder="搜索产品名称" class="search-input" />
</view>
<scroll-view scroll-y class="popup-body">
<view v-if="loadingProducts" class="loading-tip">
<text>加载中...</text>
</view>
<!-- 严格遍历产品过滤列表 -->
<view class="warehouse-item" v-for="product in filteredProducts" :key="'product_' + product.productId" @click="selectProduct(product)" v-else>
<text class="warehouse-name">{{ product.productName }}{{ product.specification || '暂无规格' }}{{ product.material }}</text>
<text class="warehouse-check" v-if="itemId === product.productId"></text>
</view>
<view class="empty-tip" v-if="!loadingProducts && (!filteredProducts || filteredProducts.length === 0)">
<text>未找到匹配的产品</text>
</view>
</scroll-view>
</view>
</uni-popup>
<!-- 原材料选择弹窗仅显示原材料 -->
<uni-popup ref="rawMaterialPopup" type="bottom">
<view class="warehouse-popup">
<view class="popup-header">
<text class="popup-title">选择原材料</text>
<text class="popup-close" @click="closeRawMaterialPicker"></text>
</view>
<view class="popup-search">
<input v-model="rawMaterialSearchKeyword" @input="filterRawMaterials" placeholder="搜索原材料名称" class="search-input" />
</view>
<scroll-view scroll-y class="popup-body">
<view v-if="loadingRawMaterials" class="loading-tip">
<text>加载中...</text>
</view>
<!-- 严格遍历原材料过滤列表 -->
<view class="warehouse-item" v-for="material in filteredRawMaterials" :key="'material_' + material.rawMaterialId" @click="selectRawMaterial(material)" v-else>
<text class="warehouse-name">{{ material.rawMaterialName }}{{ material.specification || '暂无规格' }}{{ material.material }}</text>
<text class="warehouse-check" v-if="itemId === material.rawMaterialId"></text>
</view>
<view class="empty-tip" v-if="!loadingRawMaterials && (!filteredRawMaterials || filteredRawMaterials.length === 0)">
<text>未找到匹配的原材料</text>
</view>
</scroll-view>
</view>
</uni-popup>
<!-- 物品类型选择弹窗 -->
<uni-popup ref="itemTypePopup" type="bottom">
<view class="warehouse-popup">
<view class="popup-header">
<text class="popup-title">选择物品类型</text>
<text class="popup-close" @click="closeItemTypePicker"></text>
</view>
<scroll-view class="popup-body" scroll-y>
<view class="warehouse-item" @click="selectItemType('product')">
<text class="warehouse-name">成品</text>
<text class="warehouse-check" v-if="itemType === 'product'"></text>
</view>
<view class="warehouse-item" @click="selectItemType('raw_material')">
<text class="warehouse-name">原料</text>
<text class="warehouse-check" v-if="itemType === 'raw_material'"></text>
</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script>
import { listProduct } from '@/api/wms/product.js'
import { listRawMaterial } from '@/api/wms/rawMaterial.js'
export default {
name: 'ItemSelector',
props: {
itemType: {
type: String,
default: ''
},
itemId: {
type: [String, Number],
default: undefined
},
materialType: {
type: String,
default: undefined
},
disabled: {
type: Boolean,
default: false
},
pageSize: {
type: Number,
default: 1000
}
},
data() {
return {
// 严格隔离的数据源
products: [], // 仅存放产品数据
rawMaterials: [], // 仅存放原材料数据
loadingProducts: false,
loadingRawMaterials: false,
selectedName: '',
// 独立的搜索和过滤结果
productSearchKeyword: '',
filteredProducts: [],
rawMaterialSearchKeyword: '',
filteredRawMaterials: []
};
},
created() {
this.loadProducts()
this.loadRawMaterials()
},
watch: {
itemId: {
immediate: true,
handler(newVal) {
if (!newVal) {
this.selectedName = '';
return;
}
// 严格根据类型匹配名称
if (this.itemType === 'product') {
const product = this.products.find(p => p.productId === newVal);
this.selectedName = product?.productName || '';
} else if (this.itemType === 'raw_material') {
const material = this.rawMaterials.find(m => m.rawMaterialId === newVal);
this.selectedName = material?.rawMaterialName || '';
}
}
},
itemType(newVal) {
if (newVal === 'product') {
this.productSearchKeyword = ''
this.filteredProducts = [...this.products]
} else if (newVal === 'raw_material') {
this.rawMaterialSearchKeyword = ''
this.filteredRawMaterials = [...this.rawMaterials]
}
}
},
methods: {
// 加载产品数据(严格过滤非产品数据)
async loadProducts() {
if (this.loadingProducts) return
this.loadingProducts = true
try {
const res = await listProduct({
pageNum: 1,
pageSize: this.pageSize
});
if (res.code === 200) {
// 原始数据
const originData = res.rows || res.data || [];
console.log('产品原始数据', originData)
// 过滤出真正的产品数据通过是否包含productId标识
this.products = originData.filter(item => item.productId !== undefined && item.productId !== null);
// 初始化过滤列表
this.filteredProducts = [...this.products];
console.log('产品列表加载完成,数量:', this.products.length);
} else {
console.error('产品加载失败:', res.msg);
uni.showToast({ title: '产品数据加载失败', icon: 'none' });
}
} catch (err) {
console.error('产品加载异常:', err);
uni.showToast({ title: '产品数据加载异常', icon: 'none' });
} finally {
this.loadingProducts = false;
}
},
// 加载原材料数据(严格过滤非原材料数据)
async loadRawMaterials() {
if (this.loadingRawMaterials) return
this.loadingRawMaterials = true
try {
const res = await listRawMaterial({
pageNum: 1,
pageSize: this.pageSize
});
if (res.code === 200) {
// 原始数据
const originData = res.rows || res.data || [];
// 过滤出真正的原材料数据通过是否包含rawMaterialId标识
this.rawMaterials = originData.filter(item => item.rawMaterialId !== undefined && item.rawMaterialId !== null);
// 初始化过滤列表
this.filteredRawMaterials = [...this.rawMaterials];
console.log('原材料列表加载完成,数量:', this.rawMaterials.length);
} else {
console.error('原材料加载失败:', res.msg);
uni.showToast({ title: '原材料数据加载失败', icon: 'none' });
}
} catch (err) {
console.error('原材料加载异常:', err);
uni.showToast({ title: '原材料数据加载异常', icon: 'none' });
} finally {
this.loadingRawMaterials = false;
}
},
// 产品过滤(仅基于产品列表)
filterProducts() {
const keyword = this.productSearchKeyword.trim().toLowerCase();
this.filteredProducts = keyword
? this.products.filter(p => {
// 只基于产品名称过滤,且确保是产品数据
return (p.productName && p.productName.toLowerCase().includes(keyword))
|| p?.specification?.toLowerCase()?.includes(keyword)
})
: [...this.products];
},
// 原材料过滤(仅基于原材料列表)
filterRawMaterials() {
const keyword = this.rawMaterialSearchKeyword.trim().toLowerCase();
this.filteredRawMaterials = keyword
? this.rawMaterials.filter(m => {
// 只基于原材料名称过滤,且确保是原材料数据
return (m.rawMaterialName && m.rawMaterialName.toLowerCase().includes(keyword))
|| m?.specification?.toLowerCase()?.includes(keyword)
})
: [...this.rawMaterials];
},
// 选择产品强制类型为product
selectProduct(product) {
// 额外校验是否为产品数据
if (!product.productId) return;
this.$emit('update:itemId', product.productId);
this.$emit('update:itemType', 'product');
this.$emit('update:materialType', '成品')
this.selectedName = product.productName;
uni.showToast({ title: `已选择产品:${product.productName}`, icon: 'success' });
this.closeProductPicker();
},
// 选择原材料强制类型为raw_material
selectRawMaterial(material) {
// 额外校验是否为原材料数据
if (!material.rawMaterialId) return;
this.$emit('update:itemId', material.rawMaterialId);
this.$emit('update:itemType', 'raw_material');
this.$emit('update:materialType', '原料')
this.selectedName = material.rawMaterialName;
uni.showToast({ title: `已选择原材料:${material.rawMaterialName}`, icon: 'success' });
this.closeRawMaterialPicker();
},
// 类型选择逻辑
selectItemType(type) {
this.$emit('update:itemType', type);
this.$emit('update:itemId', undefined);
this.closeItemTypePicker();
if (type === 'product' && !this.loadingProducts) {
this.showProductPicker()
} else if (type === 'raw_material' && !this.loadingRawMaterials) {
this.showRawMaterialPicker()
}
},
// 弹窗控制
showItemTypePicker() {
this.$refs.itemTypePopup.open();
},
closeItemTypePicker() {
this.$refs.itemTypePopup.close();
},
showProductPicker() {
this.$refs.productPopup.open();
},
closeProductPicker() {
this.$refs.productPopup.close();
},
showRawMaterialPicker() {
this.$refs.rawMaterialPopup.open();
},
closeRawMaterialPicker() {
this.$refs.rawMaterialPopup.close();
},
// ✅ 新增核心清除方法1清空物品类型 + 所有关联数据(完全重置)
handleClearItemType() {
this.$emit('update:itemType', '');
this.$emit('update:itemId', undefined);
this.$emit('update:materialType', undefined);
this.selectedName = '';
uni.showToast({ title: '已清空物品类型', icon: 'none' });
},
// ✅ 新增核心清除方法2仅清空选中的产品保留物品类型为成品
handleClearProduct() {
this.$emit('update:itemId', undefined);
this.selectedName = '';
uni.showToast({ title: '已清空产品选择', icon: 'none' });
},
// ✅ 新增核心清除方法3仅清空选中的原材料保留物品类型为原料
handleClearRawMaterial() {
this.$emit('update:itemId', undefined);
this.selectedName = '';
uni.showToast({ title: '已清空原材料选择', icon: 'none' });
}
}
};
</script>
<style scoped lang="scss">
/* 样式保持不变 + 新增清除按钮样式 */
.form-item-optional {
margin-bottom: 30rpx;
.form-label-optional {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
font-weight: 500;
}
}
.picker-input {
width: 100%;
height: 88rpx;
padding: 0 24rpx;
padding-right: 60rpx; // ✅ 新增:给清除按钮/箭头预留空间,防止文本遮挡
background: #f8f9fa;
border: 2rpx solid #e8e8e8;
border-radius: 12rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.3s;
&:active {
background: #fff;
border-color: #007aff;
}
&.picker-input-disabled {
background: #f5f5f5;
color: #999;
cursor: not-allowed;
}
.picker-text {
flex: 1;
font-size: 28rpx;
color: #333;
&.picker-placeholder {
color: #999;
}
}
.picker-arrow {
font-size: 24rpx;
color: #999;
margin-left: 10rpx;
}
// ✅ 新增:清除按钮样式,与箭头风格统一,点击有按压反馈
.picker-clear {
font-size: 28rpx;
color: #999;
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 8rpx;
border-radius: 50%;
&:active {
background-color: #e8e8e8;
color: #666;
}
}
}
.warehouse-popup {
background: #fff;
border-radius: 24rpx 24rpx 0 0;
max-height: 70vh;
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
.popup-title {
font-size: 32rpx;
color: #333;
font-weight: 600;
}
.popup-close {
font-size: 40rpx;
color: #999;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
.popup-search {
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
.search-input {
width: 100%;
height: 80rpx;
padding: 0 20rpx;
background: #f8f9fa;
border: 2rpx solid #e8e8e8;
border-radius: 10rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
}
}
.popup-body {
max-height: 400rpx;
.loading-tip {
text-align: center;
padding: 60rpx 0;
color: #666;
font-size: 28rpx;
}
.warehouse-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #f5f5f5;
&:active {
background: #f0f7ff;
}
.warehouse-name {
flex: 1;
font-size: 28rpx;
color: #333;
}
.warehouse-check {
font-size: 32rpx;
color: #007aff;
font-weight: bold;
}
}
.empty-tip {
text-align: center;
padding: 60rpx 0;
color: #999;
font-size: 28rpx;
}
}
}
</style>