Selaa lähdekoodia

景聚庭-商品管理新版需求完善

huangguoce 3 viikkoa sitten
vanhempi
commit
ea1ffb03c1

+ 7 - 7
pages/psiManagement/collect/CollectForm.vue

@@ -39,8 +39,11 @@
                     <u-form-item label="领用人部门" :prop="'detailInfos[' + index + '].recipientOffice'">
                         <u--input v-model="inputForm.detailInfos[index].recipientOffice" disabled></u--input>
                     </u-form-item> -->
-					<collect-goods-selector ref="goodsSelect" :rowIndex="index" :inputForm="inputForm"
-						:disabled="nodeFlag" @selected="handleGoodsSelected"></collect-goods-selector>
+					<psi-goods-selector :row-index="index" :input-form="inputForm" row-path="detailInfos"
+						:value-fields="['collectType', 'goodsName']" prop-field="goodsName" label="商品名称"
+						placeholder="请选择商品" title="选择商品" search-placeholder="搜索商品" empty-text="暂无可选商品"
+						:show-stock="true" :show-add-goods="true" :disabled="nodeFlag"
+						@selected="handleGoodsSelected"></psi-goods-selector>
 					<u-form-item label="领用数量" :prop="'detailInfos[' + index + '].collectNumber'" :required="true" labelWidth="68px">
 						<u--input v-model="inputForm.detailInfos[index].collectNumber" placeholder="请输入领用数量"
 							:disabled="nodeFlag" @blur="handleCollectNumberBlur(index)"></u--input>
@@ -148,7 +151,7 @@
 	import WareHouseService from '@/api/psi/WareHouseService'
 	import CollectService from '@/api/psi/CollectService'
 	import CommonApi from '@/api/common/CommonApi'
-	import CollectGoodsSelector from './CollectGoodsSelector.vue'
+	import PsiGoodsSelector from '../components/PsiGoodsSelector.vue'
 
 	export default {
 		name: 'PsiCollectForm',
@@ -156,7 +159,7 @@
 			userSelect,
 			baTreePicker,
 			UploadComponent,
-			CollectGoodsSelector
+			PsiGoodsSelector
 		},
 		computed: mapState({
 			userInfo: (state) => state.user.userInfo
@@ -828,9 +831,6 @@
 					this.loading = false
 				}
 			},
-			async openPicker(index) {
-				await this.$refs.goodsSelect.loadGoodsList()
-			},
 		}
 	}
 </script>

+ 0 - 235
pages/psiManagement/collect/CollectGoodsSelector.vue

@@ -1,235 +0,0 @@
-<template>
-    <view>
-        <u-form-item label="物品名称" :prop="'detailInfos[' + rowIndex + '].goodsName'" :required="true" labelWidth="68px">
-            <u--input :value="currentValue" placeholder="请选择物品名称" :disabled="disabled" @focus="openPicker"></u--input>
-        </u-form-item>
-
-        <view class="selector-mask" :class="{ show: visible }" @tap="closePicker"></view>
-        <view class="selector-panel" :class="{ show: visible }">
-            <view class="selector-header">
-                <view class="selector-action" @tap="closePicker">取消</view>
-                <view class="selector-title">选择物品</view>
-                <view class="selector-action selector-action-placeholder">取消</view>
-            </view>
-            <view class="selector-search">
-                <u-search :show-action="false" v-model="keyword" placeholder="搜索物品名称"></u-search>
-            </view>
-            <scroll-view scroll-y class="selector-list">
-                <view v-if="loading" class="selector-state">加载中...</view>
-                <view v-else-if="filteredList.length === 0" class="selector-state">暂无可选物品</view>
-                <view v-else class="selector-list-inner">
-                    <view class="selector-card" v-for="item in filteredList" :key="item.id || item.tradeName"
-                        @tap="selectItem(item)">
-                        <view class="selector-name">{{ item.tradeName || '-' }}</view>
-                        <view class="selector-meta">
-                            <text>库存:{{ item.currentInventory || item.surplusNumber || '0' }}</text>
-                            <text v-if="item.company">单位:{{ item.company }}</text>
-                        </view>
-                        <view class="selector-meta" v-if="item.brand || item.specification">
-                            <text v-if="item.brand">品牌:{{ item.brand }}</text>
-                            <text v-if="item.specification">规格:{{ item.specification }}</text>
-                        </view>
-                    </view>
-                </view>
-            </scroll-view>
-        </view>
-    </view>
-</template>
-
-<script>
-import MaterialTypeService from '@/api/psi/MaterialTypeService'
-
-export default {
-    name: 'PsiCollectGoodsSelector',
-    props: {
-        rowIndex: Number,
-        inputForm: Object,
-        disabled: Boolean
-    },
-    data() {
-        return {
-            visible: false,
-            loading: false,
-            keyword: '',
-            goodsList: [],
-            materialTypeService: null
-        }
-    },
-    computed: {
-        currentRow() {
-            return (this.inputForm.detailInfos || [])[this.rowIndex] || {}
-        },
-        currentValue() {
-            return this.currentRow.collectType || this.currentRow.goodsName || ''
-        },
-        filteredList() {
-            const keyword = (this.keyword || '').trim().toLowerCase()
-            if (!keyword) {
-                return this.goodsList
-            }
-            return this.goodsList.filter(item => (item.tradeName || '').toLowerCase().includes(keyword))
-        }
-    },
-    created() {
-        this.materialTypeService = new MaterialTypeService()
-    },
-    methods: {
-        isEmpty(value) {
-            if (value === null || value === undefined) {
-                return true
-            }
-            if (typeof value === 'string' && value.trim() === '') {
-                return true
-            }
-            if (Array.isArray(value) && value.length === 0) {
-                return true
-            }
-            return false
-        },
-        async openPicker() {
-            if (this.disabled) {
-                return
-            }
-            this.visible = true
-            this.keyword = ''
-            await this.loadGoodsList()
-        },
-        closePicker() {
-            this.visible = false
-        },
-        async loadGoodsList() {
-            this.loading = true
-            try {
-                const data = await this.materialTypeService.cgList()
-                this.goodsList = (data || []).filter(item => {
-                    return String(item.infoType || item.info_type || '') === '1'
-                }).map(item => ({
-                    ...item,
-                    tradeName: item.tradeName || item.name || ''
-                }))
-            } catch (e) {
-                this.goodsList = []
-                uni.showToast({
-                    title: '物品数据加载失败',
-                    icon: 'none'
-                })
-            } finally {
-                this.loading = false
-            }
-        },
-        selectItem(item) {
-            this.$emit('selected', {
-                index: this.rowIndex,
-                item
-            })
-            this.closePicker()
-        }
-    }
-}
-</script>
-
-<style scoped>
-.selector-mask {
-    position: fixed;
-    inset: 0;
-    background: rgba(0, 0, 0, 0.35);
-    opacity: 0;
-    visibility: hidden;
-    transition: all 0.25s ease;
-    z-index: 1000;
-}
-
-.selector-mask.show {
-    opacity: 1;
-    visibility: visible;
-}
-
-.selector-panel {
-    position: fixed;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    height: 68vh;
-    background: #f7f9fc;
-    border-radius: 28rpx 28rpx 0 0;
-    transform: translateY(100%);
-    transition: transform 0.25s ease;
-    z-index: 1001;
-    display: flex;
-    flex-direction: column;
-}
-
-.selector-panel.show {
-    transform: translateY(0);
-}
-
-.selector-header {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    padding: 28rpx 32rpx 16rpx;
-    background: #fff;
-    border-bottom: 1rpx solid #eef2f7;
-}
-
-.selector-title {
-    font-size: 32rpx;
-    font-weight: 600;
-    color: #1f2937;
-}
-
-.selector-action {
-    min-width: 80rpx;
-    font-size: 28rpx;
-    color: #2979ff;
-}
-
-.selector-action-placeholder {
-    opacity: 0;
-}
-
-.selector-search {
-    padding: 20rpx 24rpx 8rpx;
-    background: #fff;
-}
-
-.selector-list {
-    flex: 1;
-    padding: 16rpx 24rpx 32rpx;
-}
-
-.selector-list-inner {
-    display: flex;
-    flex-direction: column;
-    gap: 16rpx;
-}
-
-.selector-state {
-    padding-top: 120rpx;
-    text-align: center;
-    font-size: 28rpx;
-    color: #94a3b8;
-}
-
-.selector-card {
-    background: #fff;
-    border-radius: 20rpx;
-    padding: 24rpx;
-    box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.06);
-}
-
-.selector-name {
-    font-size: 30rpx;
-    font-weight: 600;
-    color: #0f172a;
-}
-
-.selector-meta {
-    display: flex;
-    gap: 16rpx;
-    margin-top: 10rpx;
-    font-size: 24rpx;
-    color: #64748b;
-    flex-wrap: wrap;
-}
-</style>

+ 681 - 0
pages/psiManagement/components/PsiGoodsSelector.vue

@@ -0,0 +1,681 @@
+<template>
+    <view>
+        <u-form-item :label="label" :prop="inputProp" :required="required" :labelWidth="labelWidth">
+            <view class="selector-trigger" @tap.stop="openPicker" @click.stop="openPicker">
+                <u-input class="selector-input" :value="currentValue" :placeholder="displayPlaceholder" :readonly="readonly" :disabled="disabled"
+                    @focus="openPicker" @click="openPicker" @tap="openPicker"></u-input>
+            </view>
+        </u-form-item>
+
+        <view class="selector-mask" :class="{ show: visible }" @tap="closePicker"></view>
+        <view class="selector-panel" :class="{ show: visible }">
+            <view class="selector-header">
+                <view class="selector-action" @tap="closePicker">取消</view>
+                <view class="selector-title">{{ displayTitle }}</view>
+                <view v-if="showAddGoods" class="selector-action selector-action-wide" @tap.stop="openAddGoods">新增商品</view>
+                <view v-else class="selector-action selector-action-placeholder">取消</view>
+            </view>
+            <view class="selector-search">
+                <u-search :show-action="false" v-model="keyword" :placeholder="displaySearchPlaceholder"></u-search>
+            </view>
+            <scroll-view :scroll-y="true" class="selector-list">
+                <view v-if="loading" class="selector-state">加载中...</view>
+                <view v-else-if="filteredList.length === 0" class="selector-state">{{ emptyText }}</view>
+                <view v-else class="selector-list-inner">
+                    <view class="selector-card" v-for="item in filteredList" :key="item.id || item.tradeName"
+                        @tap="selectItem(item)">
+                        <view class="selector-name">{{ item.tradeName || '-' }}</view>
+                        <view class="selector-meta" v-if="showStock">
+                            <text>库存:{{ displayStock(item) }}</text>
+                            <text v-if="item.company">单位:{{ item.company }}</text>
+                        </view>
+                        <view class="selector-meta" v-if="item.brand || item.specification || (!showStock && item.company)">
+                            <text v-if="item.brand">品牌:{{ item.brand }}</text>
+                            <text v-if="item.specification">规格:{{ item.specification }}</text>
+                            <text v-if="!showStock && item.company">单位:{{ item.company }}</text>
+                        </view>
+                    </view>
+                </view>
+            </scroll-view>
+        </view>
+
+        <view class="add-mask" :class="{ show: addVisible }" @tap="closeAddGoods"></view>
+        <view class="add-panel" :class="{ show: addVisible }">
+            <view class="selector-header">
+                <view class="selector-action" @tap="closeAddGoods">取消</view>
+                <view class="selector-title">新增商品</view>
+                <view class="selector-action" @tap.stop="submitAddGoods">{{ addLoading ? '保存中' : '保存' }}</view>
+            </view>
+            <scroll-view :scroll-y="true" class="add-body">
+                <u-form-item label="所属分类" :required="true" labelWidth="76px">
+                    <view class="selector-trigger" @tap.stop="toggleCategoryList" @click.stop="toggleCategoryList">
+                        <u-input class="selector-input" :value="addForm.parentName" placeholder="请选择所属分类"
+                            readonly :disabled="isAddParentLocked"></u-input>
+                    </view>
+                </u-form-item>
+                <u-form-item label="名称" :required="true" labelWidth="76px">
+                    <u-input v-model="addForm.name" placeholder="请填写商品名称"></u-input>
+                </u-form-item>
+                <u-form-item label="品牌" labelWidth="76px">
+                    <u-input v-model="addForm.brand" placeholder="请填写品牌"></u-input>
+                </u-form-item>
+                <u-form-item label="规格" labelWidth="76px">
+                    <u-input v-model="addForm.specification" placeholder="请填写规格"></u-input>
+                </u-form-item>
+                <u-form-item label="单位" labelWidth="76px">
+                    <u-input v-model="addForm.company" placeholder="请填写单位"></u-input>
+                </u-form-item>
+            </scroll-view>
+        </view>
+
+        <view class="category-mask" :class="{ show: categoryListVisible }" @tap="closeCategoryList"></view>
+        <view class="category-panel" :class="{ show: categoryListVisible }">
+            <view class="selector-header">
+                <view class="selector-action" @tap="closeCategoryList">取消</view>
+                <view class="selector-title">选择分类</view>
+                <view class="selector-action selector-action-placeholder">取消</view>
+            </view>
+            <view class="selector-search">
+                <u-search :show-action="false" v-model="categoryKeyword" placeholder="搜索分类"></u-search>
+            </view>
+            <scroll-view :scroll-y="true" class="category-list">
+                <view v-if="categoryOptions.length === 0" class="selector-state category-state">暂无可选分类</view>
+                <view v-for="item in categoryOptions" :key="item.id" class="category-item"
+                    @tap="selectCategory(item)">
+                    {{ item.name || '-' }}
+                </view>
+            </scroll-view>
+        </view>
+    </view>
+</template>
+
+<script>
+import MaterialTypeService from '@/api/psi/MaterialTypeService'
+
+export default {
+    name: 'PsiGoodsSelector',
+    props: {
+        rowIndex: Number,
+        indexExperience: Number,
+        inputForm: {
+            type: Object,
+            default: () => ({})
+        },
+        rowPath: {
+            type: String,
+            default: 'detailInfos'
+        },
+        valueFields: {
+            type: Array,
+            default: () => ['tradeName']
+        },
+        propField: {
+            type: String,
+            default: ''
+        },
+        label: {
+            type: String,
+            default: '物品名称'
+        },
+        labelWidth: {
+            type: String,
+            default: '68px'
+        },
+        placeholder: {
+            type: String,
+            default: ''
+        },
+        title: {
+            type: String,
+            default: ''
+        },
+        searchPlaceholder: {
+            type: String,
+            default: ''
+        },
+        emptyText: {
+            type: String,
+            default: '暂无可选物品'
+        },
+        parentIdField: {
+            type: String,
+            default: ''
+        },
+        parentNameField: {
+            type: String,
+            default: ''
+        },
+        parentRequiredText: {
+            type: String,
+            default: '请先选择分类'
+        },
+        requireParent: {
+            type: Boolean,
+            default: false
+        },
+        showStock: {
+            type: Boolean,
+            default: true
+        },
+        showAddGoods: {
+            type: Boolean,
+            default: false
+        },
+        required: {
+            type: Boolean,
+            default: true
+        },
+        readonly: {
+            type: Boolean,
+            default: true
+        },
+        disabled: Boolean
+    },
+    data() {
+        return {
+            visible: false,
+            loading: false,
+            addVisible: false,
+            addLoading: false,
+            categoryListVisible: false,
+            keyword: '',
+            categoryKeyword: '',
+            rawMaterialList: [],
+            goodsList: [],
+            addForm: this.createAddForm(),
+            materialTypeService: null
+        }
+    },
+    computed: {
+        actualIndex() {
+            if (this.rowIndex !== undefined && this.rowIndex !== null) {
+                return this.rowIndex
+            }
+            return this.indexExperience
+        },
+        rows() {
+            return this.inputForm[this.rowPath] || []
+        },
+        currentRow() {
+            return this.rows[this.actualIndex] || {}
+        },
+        inputProp() {
+            const field = this.propField || this.valueFields[0] || ''
+            return field ? `${this.rowPath}[${this.actualIndex}].${field}` : ''
+        },
+        currentValue() {
+            for (let i = 0; i < this.valueFields.length; i++) {
+                const value = this.currentRow[this.valueFields[i]]
+                if (!this.isEmpty(value)) {
+                    return value
+                }
+            }
+            return ''
+        },
+        parentId() {
+            return this.parentIdField ? this.currentRow[this.parentIdField] : ''
+        },
+        currentParentName() {
+            if (this.parentNameField && !this.isEmpty(this.currentRow[this.parentNameField])) {
+                return this.currentRow[this.parentNameField]
+            }
+            return this.currentRow.procurementType || this.currentRow.typeName || this.currentRow.categoryName || ''
+        },
+        isAddParentLocked() {
+            return !!this.parentIdField && !this.isEmpty(this.parentId)
+        },
+        displayPlaceholder() {
+            return this.placeholder || `请选择${this.label}`
+        },
+        displayTitle() {
+            return this.title || `选择${this.label}`
+        },
+        displaySearchPlaceholder() {
+            return this.searchPlaceholder || `搜索${this.label}`
+        },
+        filteredList() {
+            const keyword = (this.keyword || '').trim().toLowerCase()
+            if (!keyword) {
+                return this.goodsList
+            }
+            return this.goodsList.filter(item => (item.tradeName || '').toLowerCase().includes(keyword))
+        },
+        categoryOptions() {
+            const keyword = (this.categoryKeyword || '').trim().toLowerCase()
+            return this.flattenNodes(this.rawMaterialList).filter(item => {
+                const isGoods = String(item.infoType || item.info_type || '') === '1'
+                const name = item.name || item.tradeName || ''
+                return !isGoods && (!keyword || name.toLowerCase().includes(keyword))
+            }).map(item => ({
+                ...item,
+                name: item.name || item.tradeName || ''
+            }))
+        }
+    },
+    created() {
+        this.materialTypeService = new MaterialTypeService()
+    },
+    methods: {
+        createAddForm() {
+            return {
+                businessType: '',
+                infoType: '1',
+                name: '',
+                brand: '',
+                specification: '',
+                company: '',
+                parentId: '',
+                parentName: ''
+            }
+        },
+        isEmpty(value) {
+            if (value === null || value === undefined) {
+                return true
+            }
+            if (typeof value === 'string' && value.trim() === '') {
+                return true
+            }
+            if (Array.isArray(value) && value.length === 0) {
+                return true
+            }
+            return false
+        },
+        displayStock(item) {
+            if (!this.isEmpty(item.currentInventory)) {
+                return item.currentInventory
+            }
+            if (!this.isEmpty(item.surplusNumber)) {
+                return item.surplusNumber
+            }
+            return '0'
+        },
+        async openPicker() {
+            if (this.disabled) {
+                return
+            }
+            if (this.visible) {
+                return
+            }
+            if (this.requireParent && this.isEmpty(this.parentId)) {
+                uni.showToast({
+                    title: this.parentRequiredText,
+                    icon: 'none'
+                })
+                return
+            }
+            this.visible = true
+            this.keyword = ''
+            await this.loadGoodsList()
+        },
+        closePicker() {
+            this.visible = false
+            this.closeAddGoods()
+        },
+        async loadGoodsList() {
+            this.loading = true
+            try {
+                const data = await this.materialTypeService.cgList()
+                this.rawMaterialList = data || []
+                this.goodsList = this.flattenNodes(this.rawMaterialList).filter(item => {
+                    const itemParentId = item.parentId || item.parent_id || ''
+                    const isGoods = String(item.infoType || item.info_type || '') === '1'
+                    const matchParent = !this.parentIdField || this.isEmpty(this.parentId) ||
+                        String(itemParentId) === String(this.parentId)
+                    return isGoods && matchParent
+                }).map(item => ({
+                    ...item,
+                    tradeName: item.tradeName || item.name || ''
+                }))
+            } catch (e) {
+                this.goodsList = []
+                uni.showToast({
+                    title: `${this.label}数据加载失败`,
+                    icon: 'none'
+                })
+            } finally {
+                this.loading = false
+            }
+        },
+        flattenNodes(nodes, result = []) {
+            ;(nodes || []).forEach(node => {
+                result.push(node)
+                if (node.children && node.children.length) {
+                    this.flattenNodes(node.children, result)
+                }
+            })
+            return result
+        },
+        selectItem(item) {
+            this.$emit('selected', {
+                index: this.actualIndex,
+                item
+            })
+            this.closePicker()
+        },
+        async openAddGoods() {
+            if (this.addLoading) {
+                return
+            }
+            this.addForm = this.createAddForm()
+            if (this.isAddParentLocked) {
+                this.addForm.parentId = this.parentId
+                this.addForm.parentName = this.currentParentName
+            }
+            this.categoryKeyword = ''
+            this.categoryListVisible = false
+            this.addVisible = true
+            if (this.rawMaterialList.length === 0) {
+                await this.loadMaterialListForAdd()
+            }
+        },
+        closeAddGoods() {
+            if (this.addLoading) {
+                return
+            }
+            this.addVisible = false
+            this.categoryListVisible = false
+        },
+        async loadMaterialListForAdd() {
+            try {
+                const data = await this.materialTypeService.cgList()
+                this.rawMaterialList = data || []
+            } catch (e) {
+                this.rawMaterialList = []
+                uni.showToast({
+                    title: '分类数据加载失败',
+                    icon: 'none'
+                })
+            }
+        },
+        toggleCategoryList() {
+            if (this.isAddParentLocked) {
+                return
+            }
+            this.categoryListVisible = !this.categoryListVisible
+        },
+        closeCategoryList() {
+            this.categoryListVisible = false
+        },
+        selectCategory(item) {
+            this.addForm.parentId = item.id
+            this.addForm.parentName = item.name || item.tradeName || ''
+            this.categoryListVisible = false
+        },
+        async submitAddGoods() {
+            if (this.addLoading) {
+                return
+            }
+            if (this.isEmpty(this.addForm.parentId)) {
+                uni.showToast({
+                    title: '请选择所属分类',
+                    icon: 'none'
+                })
+                return
+            }
+            if (this.isEmpty(this.addForm.name)) {
+                uni.showToast({
+                    title: '请填写商品名称',
+                    icon: 'none'
+                })
+                return
+            }
+            const submitData = {
+                ...this.addForm,
+                infoType: '1'
+            }
+            delete submitData.parentName
+            this.addLoading = true
+            try {
+                const data = await this.materialTypeService.save(submitData)
+                const result = this.getResponseData(data)
+                if (typeof result === 'string' && result.indexOf('不能新增') > -1) {
+                    uni.showToast({
+                        title: result,
+                        icon: 'none'
+                    })
+                    return
+                }
+                uni.showToast({
+                    title: '保存成功',
+                    icon: 'success'
+                })
+                this.addVisible = false
+                await this.loadGoodsList()
+            } catch (e) {
+                uni.showToast({
+                    title: '商品保存失败',
+                    icon: 'none'
+                })
+            } finally {
+                this.addLoading = false
+            }
+        },
+        getResponseData(data) {
+            if (data && Object.prototype.hasOwnProperty.call(data, 'data')) {
+                return data.data
+            }
+            return data
+        }
+    }
+}
+</script>
+
+<style scoped>
+.selector-mask {
+    position: fixed;
+    inset: 0;
+    background: rgba(0, 0, 0, 0.35);
+    opacity: 0;
+    visibility: hidden;
+    transition: all 0.25s ease;
+    z-index: 1000;
+}
+
+.selector-trigger {
+    width: 100%;
+}
+
+.selector-input {
+    pointer-events: none;
+}
+
+.selector-mask.show {
+    opacity: 1;
+    visibility: visible;
+}
+
+.add-mask {
+    position: fixed;
+    inset: 0;
+    background: rgba(0, 0, 0, 0.35);
+    opacity: 0;
+    visibility: hidden;
+    transition: all 0.25s ease;
+    z-index: 1002;
+}
+
+.add-mask.show {
+    opacity: 1;
+    visibility: visible;
+}
+
+.selector-panel {
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    height: 68vh;
+    background: #f7f9fc;
+    border-radius: 28rpx 28rpx 0 0;
+    transform: translateY(100%);
+    transition: transform 0.25s ease;
+    z-index: 1001;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+}
+
+.selector-panel.show {
+    transform: translateY(0);
+}
+
+.add-panel {
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    height: 78vh;
+    background: #fff;
+    border-radius: 28rpx 28rpx 0 0;
+    transform: translateY(100%);
+    transition: transform 0.25s ease;
+    z-index: 1003;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+}
+
+.add-panel.show {
+    transform: translateY(0);
+}
+
+.add-body {
+    flex: 1;
+    height: 0;
+    padding: 18rpx 28rpx 40rpx;
+    box-sizing: border-box;
+}
+
+.category-mask {
+    position: fixed;
+    inset: 0;
+    background: rgba(0, 0, 0, 0.35);
+    opacity: 0;
+    visibility: hidden;
+    transition: all 0.25s ease;
+    z-index: 1004;
+}
+
+.category-mask.show {
+    opacity: 1;
+    visibility: visible;
+}
+
+.category-panel {
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    height: 62vh;
+    background: #f7f9fc;
+    border-radius: 28rpx 28rpx 0 0;
+    transform: translateY(100%);
+    transition: transform 0.25s ease;
+    z-index: 1005;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+}
+
+.category-panel.show {
+    transform: translateY(0);
+}
+
+.category-list {
+    flex: 1;
+    height: 0;
+    padding: 16rpx 24rpx 32rpx;
+    box-sizing: border-box;
+}
+
+.category-item {
+    margin-bottom: 14rpx;
+    padding: 22rpx 24rpx;
+    background: #fff;
+    border-radius: 16rpx;
+    font-size: 28rpx;
+    color: #1f2937;
+    box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.06);
+}
+
+.category-state {
+    padding-top: 120rpx;
+}
+
+.selector-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 28rpx 32rpx 16rpx;
+    background: #fff;
+    border-bottom: 1rpx solid #eef2f7;
+}
+
+.selector-title {
+    font-size: 32rpx;
+    font-weight: 600;
+    color: #1f2937;
+}
+
+.selector-action {
+    min-width: 80rpx;
+    font-size: 28rpx;
+    color: #2979ff;
+}
+
+.selector-action-wide {
+    min-width: 128rpx;
+    text-align: right;
+}
+
+.selector-action-placeholder {
+    opacity: 0;
+}
+
+.selector-search {
+    padding: 20rpx 24rpx 8rpx;
+    background: #fff;
+}
+
+.selector-list {
+    flex: 1;
+    height: 0;
+    padding: 16rpx 24rpx 32rpx;
+    box-sizing: border-box;
+}
+
+.selector-list-inner {
+    display: flex;
+    flex-direction: column;
+    gap: 16rpx;
+}
+
+.selector-state {
+    padding-top: 120rpx;
+    text-align: center;
+    font-size: 28rpx;
+    color: #94a3b8;
+}
+
+.selector-card {
+    background: #fff;
+    border-radius: 20rpx;
+    padding: 24rpx;
+    box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.06);
+}
+
+.selector-name {
+    font-size: 30rpx;
+    font-weight: 600;
+    color: #0f172a;
+}
+
+.selector-meta {
+    display: flex;
+    gap: 16rpx;
+    margin-top: 10rpx;
+    font-size: 24rpx;
+    color: #64748b;
+    flex-wrap: wrap;
+}
+</style>

+ 0 - 244
pages/psiManagement/purchase/GoodsSelector.vue

@@ -1,244 +0,0 @@
-<template>
-    <view>
-        <u-form-item label="商品名称" :prop="'detailInfos[' + index_experience + '].tradeName'" :required="true">
-            <u--input :value="currentValue" placeholder="请选择商品名称" readonly :disabled="disabled"
-                @focus="openPicker"></u--input>
-        </u-form-item>
-
-        <view class="selector-mask" :class="{ show: visible }" @tap="closePicker"></view>
-        <view class="selector-panel" :class="{ show: visible }">
-            <view class="selector-header">
-                <view class="selector-action" @tap="closePicker">取消</view>
-                <view class="selector-title">选择商品</view>
-                <view class="selector-action selector-action-placeholder">取消</view>
-            </view>
-            <view class="selector-search">
-                <u-search :show-action="false" v-model="keyword" placeholder="搜索商品名称"></u-search>
-            </view>
-            <scroll-view scroll-y class="selector-list">
-                <view v-if="loading" class="selector-state">加载中...</view>
-                <view v-else-if="filteredList.length === 0" class="selector-state">暂无可选商品</view>
-                <view v-else class="selector-list-inner">
-                    <view class="goods-card" v-for="item in filteredList" :key="item.id || item.tradeName"
-                        @tap="selectItem(item)">
-                        <view class="goods-name">{{ item.tradeName }}</view>
-                        <view class="goods-meta" v-if="item.brand || item.specification || item.company">
-                            <text v-if="item.brand">品牌:{{ item.brand }}</text>
-                            <text v-if="item.specification">规格:{{ item.specification }}</text>
-                            <text v-if="item.company">单位:{{ item.company }}</text>
-                        </view>
-                    </view>
-                </view>
-            </scroll-view>
-        </view>
-    </view>
-</template>
-
-<script>
-import MaterialTypeService from '@/api/psi/MaterialTypeService'
-
-export default {
-    name: 'GoodsSelector',
-    props: {
-        index_experience: Number,
-        inputForm: Object,
-        disabled: Boolean
-    },
-    data() {
-        return {
-            visible: false,
-            loading: false,
-            keyword: '',
-            goodsList: [],
-            materialTypeService: null
-        }
-    },
-    computed: {
-        currentRow() {
-            return (this.inputForm.detailInfos || [])[this.index_experience] || {}
-        },
-        currentValue() {
-            return this.currentRow.tradeName || ''
-        },
-        filteredList() {
-            const keyword = (this.keyword || '').trim().toLowerCase()
-            if (!keyword) {
-                return this.goodsList
-            }
-            return this.goodsList.filter(item => (item.tradeName || '').toLowerCase().includes(keyword))
-        }
-    },
-    created() {
-        this.materialTypeService = new MaterialTypeService()
-    },
-    methods: {
-        isEmpty(value) {
-            let result = false
-            if (value == null || value == undefined) {
-                result = true
-            }
-            if (typeof value === 'string' && (value.replace(/\s+/g, '') === '' || value === '')) {
-                result = true
-            }
-            if (typeof value === 'object' && value instanceof Array && value.length === 0) {
-                result = true
-            }
-            return result
-        },
-        async openPicker() {
-            if (this.disabled) {
-                return
-            }
-            if (this.isEmpty(this.currentRow.procurementTypeId)) {
-                uni.showToast({
-                    title: '请先选择采购类型',
-                    icon: 'none'
-                })
-                return
-            }
-            this.visible = true
-            this.keyword = ''
-            await this.loadGoodsList()
-        },
-        closePicker() {
-            this.visible = false
-        },
-        async loadGoodsList() {
-            this.loading = true
-            try {
-                const data = await this.materialTypeService.cgList()
-                this.goodsList = (data || []).filter(item => {
-                    const parentId = item.parentId || item.parent_id || ''
-                    return String(item.infoType || item.info_type || '') === '1' &&
-                        (!this.currentRow.procurementTypeId || parentId === this.currentRow.procurementTypeId)
-                }).map(item => ({
-                    ...item,
-                    tradeName: item.tradeName || item.name || ''
-                }))
-            } catch (e) {
-                this.goodsList = []
-                uni.showToast({
-                    title: '商品数据加载失败',
-                    icon: 'none'
-                })
-            } finally {
-                this.loading = false
-            }
-        },
-        async selectItem(item) {
-            this.$emit('selected', {
-                index: this.index_experience,
-                item
-            })
-            this.closePicker()
-        }
-    }
-}
-</script>
-
-<style scoped>
-.selector-mask {
-    position: fixed;
-    inset: 0;
-    background: rgba(0, 0, 0, 0.35);
-    opacity: 0;
-    visibility: hidden;
-    transition: all 0.25s ease;
-    z-index: 1000;
-}
-
-.selector-mask.show {
-    opacity: 1;
-    visibility: visible;
-}
-
-.selector-panel {
-    position: fixed;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    height: 68vh;
-    background: #f7f9fc;
-    border-radius: 28rpx 28rpx 0 0;
-    transform: translateY(100%);
-    transition: transform 0.25s ease;
-    z-index: 1001;
-    display: flex;
-    flex-direction: column;
-}
-
-.selector-panel.show {
-    transform: translateY(0);
-}
-
-.selector-header {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    padding: 28rpx 32rpx 16rpx;
-    background: #fff;
-    border-bottom: 1rpx solid #eef2f7;
-}
-
-.selector-title {
-    font-size: 32rpx;
-    font-weight: 600;
-    color: #1f2937;
-}
-
-.selector-action {
-    min-width: 80rpx;
-    font-size: 28rpx;
-    color: #2979ff;
-}
-
-.selector-action-placeholder {
-    opacity: 0;
-}
-
-.selector-search {
-    padding: 20rpx 24rpx 8rpx;
-    background: #fff;
-}
-
-.selector-list {
-    flex: 1;
-    padding: 16rpx 24rpx 32rpx;
-}
-
-.selector-list-inner {
-    display: flex;
-    flex-direction: column;
-    gap: 16rpx;
-}
-
-.selector-state {
-    padding-top: 120rpx;
-    text-align: center;
-    font-size: 28rpx;
-    color: #94a3b8;
-}
-
-.goods-card {
-    background: #fff;
-    border-radius: 20rpx;
-    padding: 24rpx;
-    box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.06);
-}
-
-.goods-name {
-    font-size: 30rpx;
-    font-weight: 600;
-    line-height: 1.45;
-    color: #0f172a;
-}
-
-.goods-meta {
-    display: flex;
-    gap: 20rpx;
-    margin-top: 10rpx;
-    font-size: 24rpx;
-    color: #64748b;
-    flex-wrap: wrap;
-}
-</style>

+ 13 - 6
pages/psiManagement/purchase/PurchaseForm.vue

@@ -60,8 +60,11 @@
                         <u--input v-model="inputForm.detailInfos[index_experience].procurementType"
                             placeholder="请选择采购类型" @focus="showPicker(index_experience)" clearable></u--input>
                     </u-form-item>
-                    <goods-selector :index_experience="index_experience" :inputForm="inputForm"
-                        @selected="handleGoodsSelected"></goods-selector>
+                    <psi-goods-selector :row-index="index_experience" :input-form="inputForm" row-path="detailInfos"
+                        :value-fields="['tradeName']" prop-field="tradeName" label="商品名称" placeholder="请选择商品名称"
+                        title="选择商品" search-placeholder="搜索商品名称" empty-text="暂无可选商品" :show-stock="false"
+                        :require-parent="true" parent-id-field="procurementTypeId" parent-name-field="procurementType"
+                        parent-required-text="请先选择采购类型" @selected="handleGoodsSelected"></psi-goods-selector>
                     <!--<u-form-item label="商品名称" :prop="'detailInfos[' + index_experience + '].tradeName'"
                                  :rules="[
 				     ]">
@@ -219,8 +222,12 @@
                         <el-input v-model="inputForm.detailInfos[index_experience].procurementType" :disabled="true"
                             placeholder="请选择采购类型" @focus="showPicker(index_experience)" clearable></el-input>
                     </u-form-item>
-                    <goods-selector :index_experience="index_experience" :disabled="true" :inputForm="inputForm"
-                        @selected="handleGoodsSelected"></goods-selector>
+                    <psi-goods-selector :row-index="index_experience" :disabled="true" :input-form="inputForm"
+                        row-path="detailInfos" :value-fields="['tradeName']" prop-field="tradeName" label="商品名称"
+                        placeholder="请选择商品名称" title="选择商品" search-placeholder="搜索商品名称" empty-text="暂无可选商品"
+                        :show-stock="false" :require-parent="true" parent-id-field="procurementTypeId"
+                        parent-name-field="procurementType" parent-required-text="请先选择采购类型"
+                        @selected="handleGoodsSelected"></psi-goods-selector>
                     <!--                    <u-form-item label="商品名称" :prop="'detailInfos[' + index_experience + '].tradeName'" -->
                     <!--                                 :rules="[-->
                     <!--				     ]">-->
@@ -326,7 +333,7 @@
 <script>
 import userSelect from '@/components/user-select/user-select-radio.vue'
 import OSSService from "@/api/sys/OSSService"
-import GoodsSelector from './GoodsSelector.vue';
+import PsiGoodsSelector from '../components/PsiGoodsSelector.vue';
 import MaterialTypeService from '@/api/psi/MaterialTypeService'
 import MaterialManagementService from '@/api/psi/MaterialManagementService'
 import CommonApi from '@/api/common/CommonApi'
@@ -339,7 +346,7 @@ export default {
     components: {
         userSelect,
         baTreePicker,
-        GoodsSelector,
+        PsiGoodsSelector,
         UploadComponent
     },
     computed: mapState({

+ 8 - 5
pages/psiManagement/wareHouse/WareHouseAddForm.vue

@@ -101,10 +101,13 @@
                     <u-form-item label="入库部门" :prop="'wareHouse[' + index + '].wareHouseManOffice'">
                         <u--input v-model="inputForm.wareHouse[index].wareHouseManOffice" disabled></u--input>
                     </u-form-item> -->
-                    <ware-house-goods-selector :rowIndex="index" :inputForm="inputForm" :disabled="nodeFlag"
-                        @selected="handleGoodsSelected"></ware-house-goods-selector>
+                    <psi-goods-selector :row-index="index" :input-form="inputForm" row-path="wareHouse"
+                        :value-fields="['wareHouseType', 'tradeName']" prop-field="tradeName" label="商品名称"
+                        placeholder="请选择商品" title="选择商品" search-placeholder="搜索商品" empty-text="暂无可选商品"
+                        :show-stock="true" :show-add-goods="true" :disabled="nodeFlag"
+                        @selected="handleGoodsSelected"></psi-goods-selector>
 						<u-form-item label="入库数量" :prop="'wareHouse[' + index + '].tradeNumber'" :required="true"  labelWidth="68px">
-						    <u--input v-model="inputForm.wareHouse[index].tradeNumber" placeholder="请输入商品数量"
+						    <u--input v-model="inputForm.wareHouse[index].tradeNumber" placeholder="请输入入库数量"
 						        :disabled="nodeFlag" @blur="onRowInputBlur(index, 'tradeNumber')"></u--input>
 						</u-form-item>
 
@@ -208,7 +211,7 @@ import MaterialTypeService from '@/api/psi/MaterialTypeService'
 import MaterialManagementService from '@/api/psi/MaterialManagementService'
 import WareHouseService from '@/api/psi/WareHouseService'
 import PurchaseSelector from './PurchaseSelector.vue'
-import WareHouseGoodsSelector from './WareHouseGoodsSelector.vue'
+import PsiGoodsSelector from '../components/PsiGoodsSelector.vue'
 
 export default {
     name: 'PsiWareHouseAddForm',
@@ -217,7 +220,7 @@ export default {
         baTreePicker,
         UploadComponent,
         PurchaseSelector,
-        WareHouseGoodsSelector
+        PsiGoodsSelector
     },
     computed: mapState({
         userInfo: (state) => state.user.userInfo

+ 0 - 235
pages/psiManagement/wareHouse/WareHouseGoodsSelector.vue

@@ -1,235 +0,0 @@
-<template>
-    <view>
-        <u-form-item label="物品名称" :prop="'wareHouse[' + rowIndex + '].tradeName'" :required="true" labelWidth="68px">
-            <u--input :value="currentValue" placeholder="请选择物品名称" :disabled="disabled" @focus="openPicker"></u--input>
-        </u-form-item>
-
-        <view class="selector-mask" :class="{ show: visible }" @tap="closePicker"></view>
-        <view class="selector-panel" :class="{ show: visible }">
-            <view class="selector-header">
-                <view class="selector-action" @tap="closePicker">取消</view>
-                <view class="selector-title">选择物品</view>
-                <view class="selector-action selector-action-placeholder">取消</view>
-            </view>
-            <view class="selector-search">
-                <u-search :show-action="false" v-model="keyword" placeholder="搜索物品名称"></u-search>
-            </view>
-            <scroll-view scroll-y class="selector-list">
-                <view v-if="loading" class="selector-state">加载中...</view>
-                <view v-else-if="filteredList.length === 0" class="selector-state">暂无可选物品</view>
-                <view v-else class="selector-list-inner">
-                    <view class="selector-card" v-for="item in filteredList" :key="item.id || item.tradeName"
-                        @tap="selectItem(item)">
-                        <view class="selector-name">{{ item.tradeName || '-' }}</view>
-                        <view class="selector-meta">
-                            <text>库存:{{ item.currentInventory || item.surplusNumber || '0' }}</text>
-                            <text v-if="item.company">单位:{{ item.company }}</text>
-                        </view>
-                        <view class="selector-meta" v-if="item.brand || item.specification">
-                            <text v-if="item.brand">品牌:{{ item.brand }}</text>
-                            <text v-if="item.specification">规格:{{ item.specification }}</text>
-                        </view>
-                    </view>
-                </view>
-            </scroll-view>
-        </view>
-    </view>
-</template>
-
-<script>
-import MaterialTypeService from '@/api/psi/MaterialTypeService'
-
-export default {
-    name: 'WareHouseGoodsSelector',
-    props: {
-        rowIndex: Number,
-        inputForm: Object,
-        disabled: Boolean
-    },
-    data() {
-        return {
-            visible: false,
-            loading: false,
-            keyword: '',
-            goodsList: [],
-            materialTypeService: null
-        }
-    },
-    computed: {
-        currentRow() {
-            return (this.inputForm.wareHouse || [])[this.rowIndex] || {}
-        },
-        currentValue() {
-            return this.currentRow.wareHouseType || this.currentRow.tradeName || ''
-        },
-        filteredList() {
-            const keyword = (this.keyword || '').trim().toLowerCase()
-            if (!keyword) {
-                return this.goodsList
-            }
-            return this.goodsList.filter(item => (item.tradeName || '').toLowerCase().includes(keyword))
-        }
-    },
-    created() {
-        this.materialTypeService = new MaterialTypeService()
-    },
-    methods: {
-        isEmpty(value) {
-            if (value === null || value === undefined) {
-                return true
-            }
-            if (typeof value === 'string' && value.trim() === '') {
-                return true
-            }
-            if (Array.isArray(value) && value.length === 0) {
-                return true
-            }
-            return false
-        },
-        async openPicker() {
-            if (this.disabled) {
-                return
-            }
-            this.visible = true
-            this.keyword = ''
-            await this.loadGoodsList()
-        },
-        closePicker() {
-            this.visible = false
-        },
-        async loadGoodsList() {
-            this.loading = true
-            try {
-                const data = await this.materialTypeService.cgList()
-                this.goodsList = (data || []).filter(item => {
-                    return String(item.infoType || item.info_type || '') === '1'
-                }).map(item => ({
-                    ...item,
-                    tradeName: item.tradeName || item.name || ''
-                }))
-            } catch (e) {
-                this.goodsList = []
-                uni.showToast({
-                    title: '商品数据加载失败',
-                    icon: 'none'
-                })
-            } finally {
-                this.loading = false
-            }
-        },
-        selectItem(item) {
-            this.$emit('selected', {
-                index: this.rowIndex,
-                item
-            })
-            this.closePicker()
-        }
-    }
-}
-</script>
-
-<style scoped>
-.selector-mask {
-    position: fixed;
-    inset: 0;
-    background: rgba(0, 0, 0, 0.35);
-    opacity: 0;
-    visibility: hidden;
-    transition: all 0.25s ease;
-    z-index: 1000;
-}
-
-.selector-mask.show {
-    opacity: 1;
-    visibility: visible;
-}
-
-.selector-panel {
-    position: fixed;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    height: 68vh;
-    background: #f7f9fc;
-    border-radius: 28rpx 28rpx 0 0;
-    transform: translateY(100%);
-    transition: transform 0.25s ease;
-    z-index: 1001;
-    display: flex;
-    flex-direction: column;
-}
-
-.selector-panel.show {
-    transform: translateY(0);
-}
-
-.selector-header {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    padding: 28rpx 32rpx 16rpx;
-    background: #fff;
-    border-bottom: 1rpx solid #eef2f7;
-}
-
-.selector-title {
-    font-size: 32rpx;
-    font-weight: 600;
-    color: #1f2937;
-}
-
-.selector-action {
-    min-width: 80rpx;
-    font-size: 28rpx;
-    color: #2979ff;
-}
-
-.selector-action-placeholder {
-    opacity: 0;
-}
-
-.selector-search {
-    padding: 20rpx 24rpx 8rpx;
-    background: #fff;
-}
-
-.selector-list {
-    flex: 1;
-    padding: 16rpx 24rpx 32rpx;
-}
-
-.selector-list-inner {
-    display: flex;
-    flex-direction: column;
-    gap: 16rpx;
-}
-
-.selector-state {
-    padding-top: 120rpx;
-    text-align: center;
-    font-size: 28rpx;
-    color: #94a3b8;
-}
-
-.selector-card {
-    background: #fff;
-    border-radius: 20rpx;
-    padding: 24rpx;
-    box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.06);
-}
-
-.selector-name {
-    font-size: 30rpx;
-    font-weight: 600;
-    color: #0f172a;
-}
-
-.selector-meta {
-    display: flex;
-    gap: 16rpx;
-    margin-top: 10rpx;
-    font-size: 24rpx;
-    color: #64748b;
-    flex-wrap: wrap;
-}
-</style>

+ 7 - 5
pages/psiManagement/wareHouse/WareHouseUpdateForm.vue

@@ -95,8 +95,10 @@
                     <u-form-item label="入库部门" :prop="'wareHouse[' + index + '].wareHouseManOffice'">
                         <u--input v-model="inputForm.wareHouse[index].wareHouseManOffice" disabled></u--input>
                     </u-form-item> -->
-					<ware-house-goods-selector :rowIndex="index" :inputForm="inputForm" :disabled="nodeFlag"
-						@selected="handleGoodsSelected"></ware-house-goods-selector>
+					<psi-goods-selector :row-index="index" :input-form="inputForm" row-path="wareHouse"
+						:value-fields="['wareHouseType', 'tradeName']" prop-field="tradeName" label="商品名称"
+						placeholder="请选择商品" title="选择商品" search-placeholder="搜索商品" empty-text="暂无可选商品"
+						:show-stock="true" :disabled="nodeFlag" @selected="handleGoodsSelected"></psi-goods-selector>
 					<u-form-item label="商品数量" :prop="'wareHouse[' + index + '].tradeNumber'" :required="true" labelWidth="68px">
 						<u--input v-model="inputForm.wareHouse[index].tradeNumber" placeholder="请输入商品数量"
 							:disabled="nodeFlag" @blur="onRowInputBlur(index, 'tradeNumber')"></u--input>
@@ -205,7 +207,7 @@
 	import WareHouseService from '@/api/psi/WareHouseService'
 	import CommonApi from '@/api/common/CommonApi'
 	import PurchaseSelector from './PurchaseSelector.vue'
-	import WareHouseGoodsSelector from './WareHouseGoodsSelector.vue'
+	import PsiGoodsSelector from '../components/PsiGoodsSelector.vue'
 
 	export default {
 		name: 'PsiWareHouseUpdateForm',
@@ -214,7 +216,7 @@
 			baTreePicker,
 			UploadComponent,
 			PurchaseSelector,
-			WareHouseGoodsSelector
+			PsiGoodsSelector
 		},
 		computed: mapState({
 			userInfo: (state) => state.user.userInfo
@@ -1113,4 +1115,4 @@
 			grid-template-columns: repeat(1, minmax(0, 1fr));
 		}
 	}
-</style>
+</style>