LossGoodsSelector.vue 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. <template>
  2. <view>
  3. <u-form-item label="库存批次" :prop="'detailInfos[' + rowIndex + '].goodsName'" :required="true">
  4. <u--input :value="currentValue" placeholder="请选择报损库存批次" readonly :disabled="disabled"
  5. @focus="openPicker"></u--input>
  6. </u-form-item>
  7. <view class="selector-mask" :class="{ show: visible }" @tap="closePicker"></view>
  8. <view class="selector-panel" :class="{ show: visible }">
  9. <view class="selector-header">
  10. <view class="selector-action" @tap="closePicker">取消</view>
  11. <view class="selector-title">选择库存批次</view>
  12. <view class="selector-action selector-action-placeholder">取消</view>
  13. </view>
  14. <view class="selector-search">
  15. <u-search :show-action="false" v-model="keyword" placeholder="搜索商品名称"></u-search>
  16. </view>
  17. <scroll-view scroll-y class="selector-list">
  18. <view v-if="loading" class="selector-state">加载中...</view>
  19. <view v-else-if="filteredList.length === 0" class="selector-state">暂无可选批次</view>
  20. <view v-else class="selector-list-inner">
  21. <view class="selector-card" v-for="item in filteredList" :key="item.id || item.tradeName + item.produceDate"
  22. @tap="selectItem(item)">
  23. <view class="selector-name">{{ item.tradeName || '-' }}</view>
  24. <view class="selector-meta">
  25. <text>入库批次:{{ item.wareHouseNumber || '-' }}</text>
  26. <text>入库日期:{{ formatDisplayDate(item.wareHouseDate) || '-' }}</text>
  27. </view>
  28. <view class="selector-meta">
  29. <text>库存:{{ item.stockBottleCount || '0' }}</text>
  30. <text v-if="item.company">单位:{{ item.company }}</text>
  31. </view>
  32. <view class="selector-meta">
  33. <text>品牌:{{ item.brand || '-' }}</text>
  34. <text>规格:{{ item.specification || '-' }}</text>
  35. </view>
  36. </view>
  37. </view>
  38. </scroll-view>
  39. </view>
  40. </view>
  41. </template>
  42. <script>
  43. import WareHouseService from '@/api/psi/WareHouseService'
  44. export default {
  45. name: 'LossGoodsSelector',
  46. props: {
  47. rowIndex: Number,
  48. inputForm: Object,
  49. disabled: Boolean
  50. },
  51. data() {
  52. return {
  53. visible: false,
  54. loading: false,
  55. keyword: '',
  56. goodsList: [],
  57. wareHouseService: null
  58. }
  59. },
  60. computed: {
  61. currentRow() {
  62. return (this.inputForm.detailInfos || [])[this.rowIndex] || {}
  63. },
  64. currentValue() {
  65. return this.currentRow.goodsName || ''
  66. },
  67. filteredList() {
  68. const keyword = (this.keyword || '').trim().toLowerCase()
  69. if (!keyword) {
  70. return this.goodsList
  71. }
  72. return this.goodsList.filter(item => (item.tradeName || '').toLowerCase().includes(keyword))
  73. }
  74. },
  75. created() {
  76. this.wareHouseService = new WareHouseService()
  77. },
  78. methods: {
  79. isEmpty(value) {
  80. if (value === null || value === undefined) {
  81. return true
  82. }
  83. if (typeof value === 'string' && value.trim() === '') {
  84. return true
  85. }
  86. if (Array.isArray(value) && value.length === 0) {
  87. return true
  88. }
  89. return false
  90. },
  91. formatDisplayDate(date) {
  92. if (this.isEmpty(date)) {
  93. return ''
  94. }
  95. const dateValue = new Date(date)
  96. if (Number.isNaN(dateValue.getTime())) {
  97. return ''
  98. }
  99. const year = dateValue.getFullYear()
  100. const month = `${dateValue.getMonth() + 1}`.padStart(2, '0')
  101. const day = `${dateValue.getDate()}`.padStart(2, '0')
  102. return `${year}-${month}-${day}`
  103. },
  104. formatNumber(value) {
  105. const numberValue = Number(value || 0)
  106. if (Number.isNaN(numberValue)) {
  107. return '0'
  108. }
  109. return numberValue % 1 === 0 ? String(numberValue) : numberValue.toFixed(2).replace(/\.?0+$/, '')
  110. },
  111. getStockBottleCount(item) {
  112. const currentInventory = Number(item.currentInventory || 0)
  113. const spec = Number(item.spec || 1)
  114. return this.formatNumber(currentInventory * (spec > 0 ? spec : 1))
  115. },
  116. async openPicker() {
  117. if (this.disabled) {
  118. return
  119. }
  120. this.visible = true
  121. this.keyword = ''
  122. await this.loadGoodsList()
  123. },
  124. closePicker() {
  125. this.visible = false
  126. },
  127. async loadGoodsList() {
  128. this.loading = true
  129. try {
  130. const data = await this.wareHouseService.getByProduceDateNotMerge({
  131. current: 1,
  132. size: 1000
  133. })
  134. const records = (data && data.records) || []
  135. this.goodsList = records.filter(item => Number(item.currentInventory || 0) > 0).map(item => ({
  136. ...item,
  137. stockBottleCount: this.getStockBottleCount(item)
  138. }))
  139. } catch (e) {
  140. this.goodsList = []
  141. uni.showToast({
  142. title: '物品数据加载失败',
  143. icon: 'none'
  144. })
  145. } finally {
  146. this.loading = false
  147. }
  148. },
  149. selectItem(item) {
  150. this.$emit('selected', {
  151. index: this.rowIndex,
  152. item
  153. })
  154. this.closePicker()
  155. }
  156. }
  157. }
  158. </script>
  159. <style scoped>
  160. .selector-mask {
  161. position: fixed;
  162. inset: 0;
  163. background: rgba(0, 0, 0, 0.35);
  164. opacity: 0;
  165. visibility: hidden;
  166. transition: all 0.25s ease;
  167. z-index: 1000;
  168. }
  169. .selector-mask.show {
  170. opacity: 1;
  171. visibility: visible;
  172. }
  173. .selector-panel {
  174. position: fixed;
  175. left: 0;
  176. right: 0;
  177. bottom: 0;
  178. height: 70vh;
  179. background: #f7f9fc;
  180. border-radius: 28rpx 28rpx 0 0;
  181. transform: translateY(100%);
  182. transition: transform 0.25s ease;
  183. z-index: 1001;
  184. display: flex;
  185. flex-direction: column;
  186. }
  187. .selector-panel.show {
  188. transform: translateY(0);
  189. }
  190. .selector-header {
  191. display: flex;
  192. align-items: center;
  193. justify-content: space-between;
  194. padding: 28rpx 32rpx 16rpx;
  195. background: #fff;
  196. border-bottom: 1rpx solid #eef2f7;
  197. }
  198. .selector-title {
  199. font-size: 32rpx;
  200. font-weight: 600;
  201. color: #1f2937;
  202. }
  203. .selector-action {
  204. min-width: 80rpx;
  205. font-size: 28rpx;
  206. color: #2979ff;
  207. }
  208. .selector-action-placeholder {
  209. opacity: 0;
  210. }
  211. .selector-search {
  212. padding: 20rpx 24rpx 8rpx;
  213. background: #fff;
  214. }
  215. .selector-list {
  216. flex: 1;
  217. padding: 16rpx 24rpx 32rpx;
  218. }
  219. .selector-list-inner {
  220. display: flex;
  221. flex-direction: column;
  222. gap: 16rpx;
  223. }
  224. .selector-state {
  225. padding-top: 120rpx;
  226. text-align: center;
  227. font-size: 28rpx;
  228. color: #94a3b8;
  229. }
  230. .selector-card {
  231. background: #fff;
  232. border-radius: 20rpx;
  233. padding: 24rpx;
  234. box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.06);
  235. }
  236. .selector-name {
  237. font-size: 30rpx;
  238. font-weight: 600;
  239. color: #0f172a;
  240. }
  241. .selector-meta {
  242. display: flex;
  243. justify-content: space-between;
  244. gap: 16rpx;
  245. margin-top: 10rpx;
  246. font-size: 24rpx;
  247. color: #64748b;
  248. }
  249. </style>