feat(hand-factory): 添加物品类型选择组件

实现成品和原材料的选择器组件,包含类型选择、搜索过滤和弹窗交互功能
This commit is contained in:
砂糖
2025-11-04 17:41:42 +08:00
parent 157bf7559c
commit f3d27a2baa
4 changed files with 489 additions and 1205 deletions

View File

@@ -0,0 +1,466 @@
<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-arrow" v-if="!disabled"></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-arrow" v-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-arrow" v-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 || '暂无规格' }}</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 || '暂无规格' }}</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
},
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)
})
: [...this.products];
},
// 原材料过滤(仅基于原材料列表)
filterRawMaterials() {
const keyword = this.rawMaterialSearchKeyword.trim().toLowerCase();
this.filteredRawMaterials = keyword
? this.rawMaterials.filter(m => {
// 只基于原材料名称过滤,且确保是原材料数据
return m.rawMaterialName && m.rawMaterialName.toLowerCase().includes(keyword)
})
: [...this.rawMaterials];
},
// 选择产品强制类型为product
selectProduct(product) {
// 额外校验是否为产品数据
if (!product.productId) return;
this.$emit('update:itemId', product.productId);
this.$emit('update:itemType', 'product');
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.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();
}
}
};
</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;
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;
}
}
.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>