You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

362 lines
8.9 KiB

  1. <template>
  2. <div>
  3. <div class="status-bar">
  4. <div class="goBack" @click="handleBack"><i class="el-icon-arrow-left"></i>上一页</div>
  5. <div class="goBack">{{ functionTitle }}</div>
  6. <div class="network" style="color: #fff" @click="$router.push({ path: '/' })">🏠首页</div>
  7. </div>
  8. <div class="input-section">
  9. <!-- PO号输入 -->
  10. <div v-if="processFlag === 1">
  11. <div class="input-group">
  12. <div class="input-group">
  13. <label>PO号</label>
  14. <div class="input-with-scan">
  15. <input v-model="poNo" placeholder="请输入或扫描PO号" @keyup.enter="loadMaterials" />
  16. </div>
  17. </div>
  18. <div class="input-group">
  19. <button @click="loadMaterials" class="scan-btn">确认</button>
  20. </div>
  21. </div>
  22. <div class="materials-section" v-if="materialList.length">
  23. <div class="material-list">
  24. <div v-for="(material, index) in materialList" :key="index" class="material-item"
  25. :class="{ selected: selectedMaterial && selectedMaterial.partNo === material.partNo }"
  26. @click="goToDetail(material)">
  27. <div class="material-info">
  28. <div class="part-no">{{ material.partNo }}</div>
  29. <div class="part-desc">{{ material.desc }}</div>
  30. <div class="qty-info">
  31. 需求: {{ material.qty }} | 已发: {{ material.recvQty }} | 剩余: {{ material.thisRecvQty }}
  32. </div>
  33. </div>
  34. <div class="material-status">
  35. <span v-if="material.thisRecvQty > 0" class="status-pending">待发料</span>
  36. <span v-else class="status-complete">已完成</span>
  37. </div>
  38. </div>
  39. </div>
  40. </div>
  41. </div>
  42. <!-- 扫描标签/发料详情 -->
  43. <div class="scan-section" v-if="processFlag === 2">
  44. <div class="label-info" v-if="labelInfo">
  45. <div class="info-row"><span class="label">物料编码:</span><span class="value">{{ labelInfo.partNo }}</span></div>
  46. <div class="info-row"><span class="label">批次号:</span><span class="value">{{ labelInfo.batchNo }}</span></div>
  47. <div class="info-row"><span class="label">可用数量:</span><span class="value">{{ labelInfo.availableQty }}</span>
  48. </div>
  49. </div>
  50. <div class="qty-input" v-if="labelInfo">
  51. <div class="input-group">
  52. <label>发料数量</label>
  53. <input v-model="issueQty" type="number"
  54. :max="Math.min(selectedMaterial.thisRecvQty, labelInfo.availableQty)" placeholder="请输入发料数量" />
  55. </div>
  56. <div class="input-group">
  57. <label>备注</label>
  58. <el-input type="textarea" v-model="remark" placeholder="可选" />
  59. </div>
  60. <button @click="confirmIssue" class="confirm-btn" :disabled="!issueQty">确认发料</button>
  61. </div>
  62. </div>
  63. <!-- 消息提示 -->
  64. <div class="message" v-if="message" :class="messageType">{{ message }}</div>
  65. </div>
  66. </div>
  67. </template>
  68. <script>
  69. import { getPoList } from '@/api/po/po.js'
  70. export default {
  71. name: 'DirectIssue',
  72. props: {
  73. functionTitle: {
  74. type: String,
  75. default: '',
  76. },
  77. },
  78. data() {
  79. return {
  80. processFlag: 1, // 1=PO号输入, 2=物料列表, 3=扫描标签/发料详情
  81. poNo: '',
  82. materialList: [],
  83. selectedMaterial: null,
  84. scannedLabel: '',
  85. labelInfo: null,
  86. issueQty: null,
  87. remark: '',
  88. message: '',
  89. messageType: 'info',
  90. }
  91. },
  92. methods: {
  93. handleBack() {
  94. if (this.processFlag === 2) {
  95. this.processFlag = 1
  96. } else if (this.processFlag === 1) {
  97. this.$emit('back')
  98. }
  99. },
  100. async loadMaterials() {
  101. if (!this.poNo) {
  102. this.showMessage('请输入PO号', 'error')
  103. return
  104. }
  105. // 调用API获取PO物料
  106. try {
  107. const { data } = await getPoList({
  108. poNumber: this.poNo,
  109. site: localStorage.getItem('site'),
  110. })
  111. if (data.code === 0 && data.rows && data.rows.length > 0) {
  112. this.materialList = data.rows
  113. } else {
  114. this.showMessage(data.msg || '未找到PO物料', 'warning')
  115. }
  116. } catch (e) {
  117. this.showMessage('网络错误', 'error')
  118. }
  119. },
  120. goToDetail(material) {
  121. if (material.thisRecvQty <= 0) {
  122. this.showMessage('该物料已发料完成', 'warning')
  123. return
  124. }
  125. this.selectedMaterial = material
  126. this.scannedLabel = ''
  127. this.labelInfo = {
  128. partNo: this.selectedMaterial.partNo,
  129. batchNo: 'BATCH001',
  130. availableQty: 100,
  131. }
  132. this.issueQty = null
  133. this.processFlag = 2
  134. },
  135. resetPO() {
  136. this.poNo = ''
  137. this.materialList = []
  138. this.selectedMaterial = null
  139. this.scannedLabel = ''
  140. this.labelInfo = null
  141. this.issueQty = null
  142. this.processFlag = 1
  143. },
  144. parseMaterialLabel() {
  145. // TODO: 调用解析标签API,获取labelInfo
  146. this.labelInfo = {
  147. partNo: this.selectedMaterial.partNo,
  148. batchNo: 'BATCH001',
  149. availableQty: 100,
  150. }
  151. this.issueQty = null
  152. this.showMessage('标签解析成功', 'success')
  153. },
  154. confirmIssue() {
  155. if (!this.issueQty || this.issueQty <= 0) {
  156. this.showMessage('请输入有效的发料数量', 'error')
  157. return
  158. }
  159. // TODO: 调用发料API
  160. this.showMessage('发料成功', 'success')
  161. // 刷新物料列表
  162. this.loadMaterials()
  163. this.selectedMaterial = null
  164. this.scannedLabel = ''
  165. this.labelInfo = null
  166. this.issueQty = null
  167. this.processFlag = 1
  168. },
  169. showMessage(text, type = 'info') {
  170. this.message = text
  171. this.messageType = type
  172. setTimeout(() => {
  173. this.message = ''
  174. }, 2000)
  175. },
  176. },
  177. }
  178. </script>
  179. <style scoped>
  180. .input-section {
  181. background: white;
  182. border-radius: 8px;
  183. padding: 15px;
  184. margin-bottom: 15px;
  185. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  186. }
  187. .section-header {
  188. display: flex;
  189. justify-content: space-between;
  190. align-items: center;
  191. margin-bottom: 15px;
  192. padding-bottom: 10px;
  193. border-bottom: 1px solid #eee;
  194. }
  195. .reset-btn {
  196. background: #17b3a3;
  197. color: white;
  198. border: none;
  199. padding: 6px 12px;
  200. border-radius: 4px;
  201. cursor: pointer;
  202. font-size: 12px;
  203. }
  204. .input-group {
  205. margin-bottom: 15px;
  206. }
  207. .input-group label {
  208. display: block;
  209. margin-bottom: 5px;
  210. font-weight: bold;
  211. color: #333;
  212. }
  213. .input-with-scan {
  214. display: flex;
  215. gap: 10px;
  216. }
  217. .input-with-scan input {
  218. flex: 1;
  219. padding: 10px;
  220. border: 1px solid #ddd;
  221. border-radius: 4px;
  222. font-size: 16px;
  223. }
  224. .scan-btn,
  225. .confirm-btn {
  226. background: #17b3a3;
  227. color: white;
  228. border: none;
  229. padding: 10px 15px;
  230. border-radius: 4px;
  231. cursor: pointer;
  232. font-size: 14px;
  233. white-space: nowrap;
  234. }
  235. .scan-btn:hover,
  236. .confirm-btn:hover {
  237. background: #13998c;
  238. }
  239. .confirm-btn:disabled {
  240. background: #6c757d;
  241. cursor: not-allowed;
  242. }
  243. .material-list {
  244. display: flex;
  245. flex-direction: column;
  246. gap: 10px;
  247. }
  248. .material-item {
  249. display: flex;
  250. justify-content: space-between;
  251. align-items: center;
  252. padding: 15px;
  253. border: 1px solid #ddd;
  254. border-radius: 6px;
  255. cursor: pointer;
  256. transition: all 0.3s;
  257. }
  258. .material-item.selected {
  259. border-color: #007bff;
  260. background-color: #e3f2fd;
  261. }
  262. .material-info {
  263. flex: 1;
  264. }
  265. .part-no {
  266. font-weight: bold;
  267. font-size: 16px;
  268. color: #333;
  269. margin-bottom: 4px;
  270. }
  271. .part-desc {
  272. color: #666;
  273. font-size: 14px;
  274. margin-bottom: 4px;
  275. }
  276. .qty-info {
  277. font-size: 12px;
  278. color: #666;
  279. }
  280. .material-status {
  281. text-align: right;
  282. }
  283. .status-pending {
  284. background: #ffc107;
  285. color: #212529;
  286. padding: 4px 8px;
  287. border-radius: 12px;
  288. font-size: 12px;
  289. }
  290. .status-complete {
  291. background: #28a745;
  292. color: white;
  293. padding: 4px 8px;
  294. border-radius: 12px;
  295. font-size: 12px;
  296. }
  297. .label-info {
  298. background: #f8f9fa;
  299. border-radius: 6px;
  300. padding: 15px;
  301. margin-bottom: 15px;
  302. }
  303. .info-row {
  304. display: flex;
  305. justify-content: space-between;
  306. margin-bottom: 8px;
  307. padding: 5px 0;
  308. border-bottom: 1px solid #eee;
  309. }
  310. .info-row:last-child {
  311. border-bottom: none;
  312. margin-bottom: 0;
  313. }
  314. .info-row .label {
  315. font-weight: bold;
  316. color: #666;
  317. }
  318. .info-row .value {
  319. color: #333;
  320. }
  321. .qty-input {
  322. margin-top: 15px;
  323. }
  324. .qty-input input[type='number'] {
  325. width: 100%;
  326. padding: 10px;
  327. border: 1px solid #ddd;
  328. border-radius: 4px;
  329. font-size: 16px;
  330. }
  331. .message {
  332. margin-top: 10px;
  333. padding: 8px 12px;
  334. border-radius: 4px;
  335. font-weight: bold;
  336. }
  337. .message.success {
  338. background: #d4edda;
  339. color: #155724;
  340. border: 1px solid #c3e6cb;
  341. }
  342. .message.error {
  343. background: #f8d7da;
  344. color: #721c24;
  345. border: 1px solid #f5c6cb;
  346. }
  347. .message.warning {
  348. background: #fff3cd;
  349. color: #856404;
  350. border: 1px solid #ffeaa7;
  351. }
  352. .message.info {
  353. background: #d1ecf1;
  354. color: #0c5460;
  355. border: 1px solid #bee5eb;
  356. }
  357. .confirm-btn {
  358. width: 100%;
  359. }
  360. .scan-btn {
  361. width: 100%;
  362. }
  363. </style>