material-import-dialog.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. <template>
  2. <!-- 物料导入弹窗 -->
  3. <el-dialog
  4. title="导入物料"
  5. :visible.sync="dialogVisible"
  6. width="1400px"
  7. top="5vh"
  8. append-to-body
  9. :close-on-click-modal="false"
  10. class="material-import-dialog"
  11. @close="handleDialogClose"
  12. >
  13. <!-- 弹窗内容 -->
  14. <div class="import-dialog-content">
  15. <!-- 搜索区域 -->
  16. <div class="search-section">
  17. <el-form
  18. ref="searchForm"
  19. :model="searchParams"
  20. :inline="true"
  21. class="search-form"
  22. >
  23. <el-form-item label="物料名称" prop="itemName">
  24. <el-input
  25. v-model="searchParams.itemName"
  26. placeholder="请输入物料名称"
  27. clearable
  28. style="width: 200px;"
  29. @keyup.enter.native="handleSearch"
  30. />
  31. </el-form-item>
  32. <el-form-item label="物料编码" prop="itemCode">
  33. <el-input
  34. v-model="searchParams.itemCode"
  35. placeholder="请输入物料编码"
  36. clearable
  37. style="width: 200px;"
  38. @keyup.enter.native="handleSearch"
  39. />
  40. </el-form-item>
  41. <el-form-item label="规格型号" prop="specification">
  42. <el-input
  43. v-model="searchParams.specification"
  44. placeholder="请输入规格型号"
  45. clearable
  46. style="width: 200px;"
  47. @keyup.enter.native="handleSearch"
  48. />
  49. </el-form-item>
  50. <el-form-item>
  51. <el-button
  52. type="primary"
  53. icon="el-icon-search"
  54. :loading="searchLoading"
  55. @click="handleSearch"
  56. >
  57. 搜索
  58. </el-button>
  59. <el-button
  60. icon="el-icon-refresh"
  61. @click="handleReset"
  62. >
  63. 重置
  64. </el-button>
  65. </el-form-item>
  66. </el-form>
  67. </div>
  68. <!-- 统计信息 -->
  69. <div class="statistics-section">
  70. <div class="stats-item">
  71. <span class="stats-label">搜索结果:</span>
  72. <span class="stats-value">{{ materialList.length }} 条</span>
  73. </div>
  74. <div class="stats-item">
  75. <span class="stats-label">已选择:</span>
  76. <span class="stats-value selected">{{ selectedMaterials.length }} 条</span>
  77. </div>
  78. </div>
  79. <!-- 物料列表表格 -->
  80. <div class="material-list-section">
  81. <avue-crud
  82. ref="materialCrud"
  83. :data="materialList"
  84. :option="tableOption"
  85. :page.sync="page"
  86. :loading="tableLoading"
  87. @selection-change="handleSelectionChange"
  88. @current-change="handleCurrentChange"
  89. @size-change="handleSizeChange"
  90. @refresh-change="handleRefresh"
  91. >
  92. <!-- 自定义可用数量列 -->
  93. <template slot="availableQuantity" slot-scope="{ row }">
  94. <span :class="{ 'low-stock': row.availableQuantity < 10 }">
  95. {{ row.availableQuantity }}
  96. </span>
  97. </template>
  98. </avue-crud>
  99. </div>
  100. <!-- 已选择物料预览 -->
  101. <div v-if="selectedMaterials.length > 0" class="selected-preview-section">
  102. <div class="preview-header">
  103. <span class="preview-title">已选择物料预览</span>
  104. <el-button
  105. type="text"
  106. size="small"
  107. icon="el-icon-delete"
  108. @click="handleClearSelection"
  109. >
  110. 清空选择
  111. </el-button>
  112. </div>
  113. <div class="preview-content">
  114. <el-tag
  115. v-for="material in selectedMaterials"
  116. :key="material.id"
  117. closable
  118. class="material-tag"
  119. @close="handleRemoveSelection(material)"
  120. >
  121. {{ material.itemName }} ({{ material.itemCode }})
  122. </el-tag>
  123. </div>
  124. </div>
  125. </div>
  126. <!-- 底部操作按钮 -->
  127. <div slot="footer" class="dialog-footer">
  128. <el-button @click="handleCancel">取消</el-button>
  129. <el-button
  130. type="primary"
  131. :disabled="selectedMaterials.length === 0"
  132. :loading="confirmLoading"
  133. @click="handleConfirm"
  134. >
  135. 确认导入 ({{ selectedMaterials.length }})
  136. </el-button>
  137. </div>
  138. </el-dialog>
  139. </template>
  140. <script>
  141. import { getMaterialImportOption, DEFAULT_QUERY_PARAMS } from './material-detail-option'
  142. import { MaterialDetailStatus } from './constants'
  143. import { getItemList } from '@/api/common'
  144. import { generateUniqueId } from './utils'
  145. /**
  146. * @typedef {import('./material-detail-option').MaterialDetailItem} MaterialDetailItem
  147. * @typedef {import('./material-detail-option').MaterialDetailQueryParams} MaterialDetailQueryParams
  148. */
  149. /**
  150. * API返回的原始物料数据项类型
  151. * @typedef {Object} RawItemRecord
  152. * @property {number} Item_ID - 物料ID
  153. * @property {string} Item_Code - 物料编码
  154. * @property {string} Item_Name - 物料名称
  155. * @property {string} Item_PECS - 物料规格
  156. * @property {number} MainItemCategory_ID - 主物料分类ID
  157. * @property {string} MainItemCategory_Name - 主物料分类名称
  158. * @property {number|null} Warehouse_ID - 仓库ID
  159. * @property {string|null} Warehouse_Name - 仓库名称
  160. * @property {number} [availableQuantity] - 可用数量
  161. */
  162. /**
  163. * 转换后的物料数据项类型
  164. * @typedef {Object} TransformedMaterialItem
  165. * @property {string|number} id - 物料唯一标识
  166. * @property {number} itemId - 物料ID
  167. * @property {string} itemCode - 物料编码
  168. * @property {string} itemName - 物料名称
  169. * @property {string} specification - 规格型号
  170. * @property {string} itemDescription - 物料描述
  171. * @property {number} mainCategoryId - 主物料分类ID
  172. * @property {string} mainCategoryName - 主物料分类名称
  173. * @property {string} mainCategoryCode - 主物料分类编码
  174. * @property {number} inventoryInfoId - 库存信息ID
  175. * @property {string} inventoryInfoName - 库存信息名称(单位)
  176. * @property {number|null} warehouseId - 仓库ID
  177. * @property {string|null} warehouseName - 仓库名称
  178. * @property {string} warehouseCode - 仓库编码
  179. * @property {number} saleserId - 销售员ID
  180. * @property {string} saleserName - 销售员名称
  181. * @property {number} shipmentWarehouseId - 发货仓库ID
  182. * @property {string} shipmentWarehouseName - 发货仓库名称
  183. * @property {number} orgId - 组织ID
  184. * @property {string} orgName - 组织名称
  185. * @property {string} createTime - 创建时间
  186. * @property {number} availableQuantity - 可用数量
  187. * @property {RawItemRecord} _raw - 原始数据备份
  188. */
  189. /**
  190. * 物料导入弹窗组件
  191. * @description 用于从明细管理接口搜索和选择物料进行导入
  192. * @author 系统开发团队
  193. * @version 1.0.0
  194. * @since 2024-01-15
  195. */
  196. export default {
  197. name: 'MaterialImportDialog',
  198. /**
  199. * 组件属性定义
  200. */
  201. props: {
  202. /**
  203. * 弹窗显示状态
  204. * @type {boolean}
  205. */
  206. visible: {
  207. type: Boolean,
  208. default: false
  209. }
  210. },
  211. /**
  212. * 组件数据
  213. */
  214. data() {
  215. return {
  216. /**
  217. * 弹窗显示状态(内部)
  218. * @type {boolean}
  219. */
  220. dialogVisible: false,
  221. /**
  222. * 搜索参数
  223. * @type {MaterialDetailQueryParams}
  224. */
  225. searchParams: { ...DEFAULT_QUERY_PARAMS },
  226. /**
  227. * 物料列表
  228. * @type {Array<TransformedMaterialItem>}
  229. */
  230. materialList: [],
  231. /**
  232. * 选中的物料列表
  233. * @type {Array<TransformedMaterialItem>}
  234. */
  235. selectedMaterials: [],
  236. /**
  237. * 分页配置
  238. * @type {Object}
  239. */
  240. page: {
  241. currentPage: 1,
  242. pageSize: 10,
  243. total: 0
  244. },
  245. /**
  246. * 搜索加载状态
  247. * @type {boolean}
  248. */
  249. searchLoading: false,
  250. /**
  251. * 表格加载状态
  252. * @type {boolean}
  253. */
  254. tableLoading: false,
  255. /**
  256. * 确认加载状态
  257. * @type {boolean}
  258. */
  259. confirmLoading: false
  260. }
  261. },
  262. /**
  263. * 计算属性
  264. */
  265. computed: {
  266. /**
  267. * 表格配置选项
  268. * @returns {Object} AvueJS表格配置
  269. */
  270. tableOption() {
  271. return getMaterialImportOption()
  272. }
  273. },
  274. /**
  275. * 监听器
  276. */
  277. watch: {
  278. /**
  279. * 监听弹窗显示状态
  280. * @param {boolean} newVal - 新的显示状态
  281. */
  282. visible: {
  283. handler(newVal) {
  284. this.dialogVisible = newVal
  285. if (newVal) {
  286. this.initDialog()
  287. }
  288. },
  289. immediate: true
  290. },
  291. /**
  292. * 监听内部弹窗状态
  293. * @param {boolean} newVal - 新的显示状态
  294. */
  295. dialogVisible(newVal) {
  296. this.$emit('update:visible', newVal)
  297. }
  298. },
  299. /**
  300. * 组件方法
  301. */
  302. methods: {
  303. /**
  304. * 初始化弹窗
  305. * @description 重置搜索参数和选中状态,不自动加载数据
  306. */
  307. initDialog() {
  308. this.resetSearchParams()
  309. this.selectedMaterials = []
  310. this.materialList = []
  311. this.page.total = 0
  312. },
  313. /**
  314. * 重置搜索参数
  315. */
  316. resetSearchParams() {
  317. this.searchParams = { ...DEFAULT_QUERY_PARAMS }
  318. this.page.currentPage = 1
  319. },
  320. /**
  321. * 加载物料列表
  322. * @description 调用明细管理接口获取物料数据,并转换字段格式
  323. */
  324. async loadMaterialList() {
  325. try {
  326. this.tableLoading = true
  327. const params = {
  328. ...this.searchParams,
  329. current: this.page.currentPage,
  330. size: this.page.pageSize
  331. }
  332. const response = await getItemList(params.current, params.size, {
  333. itemName: params.itemName,
  334. itemCode: params.itemCode,
  335. specification: params.specification
  336. })
  337. if (response && response.data && response.data.data) {
  338. const rawRecords = response.data.data.records || []
  339. // 转换API返回的字段名称为表格所需的格式
  340. this.materialList = rawRecords.map(item => ({
  341. id: item.Item_ID || item.id,
  342. itemId: item.Item_ID,
  343. itemCode: item.Item_Code,
  344. itemName: item.Item_Name,
  345. specification: item.Item_PECS || item.specification,
  346. itemDescription: item.Item_Description,
  347. mainCategoryId: item.MainItemCategory_ID,
  348. mainCategoryName: item.MainItemCategory_Name,
  349. mainCategoryCode: item.MainItemCategory_Code,
  350. inventoryInfoId: item.InventoryInfo_ID,
  351. inventoryInfoName: item.InventoryInfo_Name,
  352. warehouseId: item.Warehouse_ID,
  353. warehouseName: item.Warehouse_Name,
  354. warehouseCode: item.Warehouse_Code,
  355. saleserId: item.Saleser_ID,
  356. saleserName: item.Saleser_NAME,
  357. shipmentWarehouseId: item.ShipmentWarehouse_ID,
  358. shipmentWarehouseName: item.ShipmentWarehouse_Name,
  359. orgId: item.ORG_ID,
  360. orgName: item.ORG_NAME,
  361. createTime: item.createTime,
  362. availableQuantity: item.availableQuantity || 0,
  363. // 保留原始数据以备后用
  364. _raw: item
  365. }))
  366. this.page.total = response.data.data.total || 0
  367. } else {
  368. this.materialList = []
  369. this.page.total = 0
  370. }
  371. } catch (error) {
  372. this.$message.error('加载物料列表失败,请重试')
  373. this.materialList = []
  374. this.page.total = 0
  375. } finally {
  376. this.tableLoading = false
  377. }
  378. },
  379. /**
  380. * 处理搜索
  381. */
  382. async handleSearch() {
  383. this.searchLoading = true
  384. this.page.currentPage = 1
  385. await this.loadMaterialList()
  386. this.searchLoading = false
  387. },
  388. /**
  389. * 处理重置
  390. * @description 重置搜索参数并清空表格数据
  391. */
  392. handleReset() {
  393. this.resetSearchParams()
  394. this.materialList = []
  395. this.page.total = 0
  396. this.selectedMaterials = []
  397. },
  398. /**
  399. * 处理选择变化
  400. * @param {Array<TransformedMaterialItem>} selection - 选中的行数据
  401. */
  402. handleSelectionChange(selection) {
  403. this.selectedMaterials = selection
  404. },
  405. /**
  406. * 处理页码变化
  407. * @param {number} currentPage - 当前页码
  408. */
  409. async handleCurrentChange(currentPage) {
  410. this.page.currentPage = currentPage
  411. await this.loadMaterialList()
  412. },
  413. /**
  414. * 处理页大小变化
  415. * @param {number} pageSize - 页大小
  416. */
  417. async handleSizeChange(pageSize) {
  418. this.page.pageSize = pageSize
  419. this.page.currentPage = 1
  420. await this.loadMaterialList()
  421. },
  422. /**
  423. * 处理刷新
  424. */
  425. async handleRefresh() {
  426. await this.loadMaterialList()
  427. },
  428. /**
  429. * 处理清空选择
  430. */
  431. handleClearSelection() {
  432. this.selectedMaterials = []
  433. this.$refs.materialCrud.toggleSelection()
  434. },
  435. /**
  436. * 处理移除单个选择
  437. * @param {TransformedMaterialItem} material - 要移除的物料
  438. */
  439. handleRemoveSelection(material) {
  440. const index = this.selectedMaterials.findIndex(item => item.id === material.id)
  441. if (index > -1) {
  442. this.selectedMaterials.splice(index, 1)
  443. this.$refs.materialCrud.toggleRowSelection(material, false)
  444. }
  445. },
  446. /**
  447. * 处理确认导入
  448. */
  449. async handleConfirm() {
  450. if (this.selectedMaterials.length === 0) {
  451. this.$message.warning('请选择要导入的物料')
  452. return
  453. }
  454. try {
  455. this.confirmLoading = true
  456. // 转换为物料明细格式
  457. const importedMaterials = this.selectedMaterials.map((material) => ({
  458. id: generateUniqueId(),
  459. itemId: material.itemId || material.id,
  460. itemCode: material.itemCode,
  461. itemName: material.itemName,
  462. specification: material.specification,
  463. mainCategoryId: material.mainCategoryId,
  464. mainCategoryName: material.mainCategoryName,
  465. warehouseId: material.warehouseId,
  466. warehouseName: material.warehouseName,
  467. availableQuantity: material.availableQuantity || 0,
  468. orderQuantity: 1, // 默认订单数量为1
  469. confirmQuantity: 0,
  470. unitPrice: 0,
  471. taxRate: 0,
  472. taxAmount: 0,
  473. totalAmount: 0,
  474. detailStatus: MaterialDetailStatus.PENDING, // 默认状态为待确认
  475. createTime: new Date().toISOString(),
  476. updateTime: new Date().toISOString()
  477. }))
  478. this.$emit('confirm', importedMaterials)
  479. this.dialogVisible = false
  480. } catch (error) {
  481. this.$message.error('导入物料失败,请重试')
  482. } finally {
  483. this.confirmLoading = false
  484. }
  485. },
  486. /**
  487. * 处理取消
  488. */
  489. handleCancel() {
  490. this.dialogVisible = false
  491. this.$emit('cancel')
  492. },
  493. /**
  494. * 处理弹窗关闭
  495. */
  496. handleDialogClose() {
  497. this.selectedMaterials = []
  498. this.materialList = []
  499. this.resetSearchParams()
  500. }
  501. }
  502. }
  503. </script>
  504. <style scoped>
  505. .material-import-dialog {
  506. .el-dialog__body {
  507. padding: 20px;
  508. }
  509. }
  510. .import-dialog-content {
  511. max-height: 70vh;
  512. overflow-y: auto;
  513. }
  514. .search-section {
  515. background-color: #f8f9fa;
  516. padding: 16px;
  517. border-radius: 4px;
  518. margin-bottom: 16px;
  519. }
  520. .search-form {
  521. margin-bottom: 0;
  522. }
  523. .statistics-section {
  524. display: flex;
  525. gap: 24px;
  526. margin-bottom: 16px;
  527. padding: 12px 16px;
  528. background-color: #f0f9ff;
  529. border-radius: 4px;
  530. border-left: 4px solid #409eff;
  531. }
  532. .stats-item {
  533. display: flex;
  534. align-items: center;
  535. gap: 4px;
  536. }
  537. .stats-label {
  538. font-size: 14px;
  539. color: #606266;
  540. }
  541. .stats-value {
  542. font-size: 14px;
  543. font-weight: 600;
  544. color: #303133;
  545. }
  546. .stats-value.selected {
  547. color: #409eff;
  548. }
  549. .material-list-section {
  550. margin-bottom: 16px;
  551. }
  552. .low-stock {
  553. color: #f56c6c;
  554. font-weight: 600;
  555. }
  556. .selected-preview-section {
  557. border-top: 1px solid #ebeef5;
  558. padding-top: 16px;
  559. }
  560. .preview-header {
  561. display: flex;
  562. justify-content: space-between;
  563. align-items: center;
  564. margin-bottom: 12px;
  565. }
  566. .preview-title {
  567. font-size: 14px;
  568. font-weight: 600;
  569. color: #303133;
  570. }
  571. .preview-content {
  572. display: flex;
  573. flex-wrap: wrap;
  574. gap: 8px;
  575. max-height: 120px;
  576. overflow-y: auto;
  577. padding: 8px;
  578. background-color: #f8f9fa;
  579. border-radius: 4px;
  580. }
  581. .material-tag {
  582. margin: 0;
  583. }
  584. .dialog-footer {
  585. text-align: right;
  586. padding-top: 16px;
  587. border-top: 1px solid #ebeef5;
  588. }
  589. /* 响应式设计 */
  590. @media (max-width: 768px) {
  591. .search-form {
  592. .el-form-item {
  593. margin-bottom: 12px;
  594. }
  595. }
  596. .statistics-section {
  597. flex-direction: column;
  598. gap: 8px;
  599. }
  600. .preview-content {
  601. max-height: 80px;
  602. }
  603. }
  604. </style>