| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681 |
- <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>
|