|
@@ -15,14 +15,40 @@
|
|
|
</el-tag>
|
|
|
</div>
|
|
|
<div class="header-right">
|
|
|
- <el-button
|
|
|
- type="primary"
|
|
|
- icon="el-icon-download"
|
|
|
- size="small"
|
|
|
- @click="handleImportMaterial"
|
|
|
- >
|
|
|
- 导入物料
|
|
|
- </el-button>
|
|
|
+ <!-- 物料选择区域 -->
|
|
|
+ <div class="material-select-container">
|
|
|
+ <el-select
|
|
|
+ v-model="selectedMaterialId"
|
|
|
+ placeholder="请选择物料"
|
|
|
+ filterable
|
|
|
+ remote
|
|
|
+ reserve-keyword
|
|
|
+ :remote-method="remoteSearchMaterial"
|
|
|
+ :loading="materialLoading"
|
|
|
+ size="small"
|
|
|
+ style="width: 300px; margin-right: 8px;"
|
|
|
+ clearable
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in materialOptions"
|
|
|
+ :key="item.id"
|
|
|
+ :label="`${item.itemName} (${item.itemCode})`"
|
|
|
+ :value="item.id"
|
|
|
+ >
|
|
|
+ <span style="float: left">{{ item.itemName }}</span>
|
|
|
+ <span style="float: right; color: #8492a6; font-size: 13px">{{ item.itemCode }}</span>
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ icon="el-icon-plus"
|
|
|
+ size="small"
|
|
|
+ :disabled="!selectedMaterialId"
|
|
|
+ @click="handleImportSelectedMaterial"
|
|
|
+ >
|
|
|
+ 导入
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
@@ -144,13 +170,7 @@
|
|
|
</avue-crud>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 物料导入弹窗 -->
|
|
|
- <MaterialImportDialog
|
|
|
- ref="materialImportDialog"
|
|
|
- :visible.sync="importDialogVisible"
|
|
|
- @confirm="handleImportConfirm"
|
|
|
- @cancel="handleImportCancel"
|
|
|
- />
|
|
|
+
|
|
|
|
|
|
|
|
|
</div>
|
|
@@ -171,7 +191,7 @@ import {
|
|
|
MaterialDetailDataSource
|
|
|
} from '@/constants/order'
|
|
|
import { MATERIAL_DETAIL_EVENTS, DIALOG_EVENTS } from './events'
|
|
|
-import MaterialImportDialog from './material-import-dialog.vue'
|
|
|
+import { getItemList } from '@/api/common'
|
|
|
import {
|
|
|
formatAmount,
|
|
|
formatFloatNumber,
|
|
@@ -220,9 +240,7 @@ import {
|
|
|
*/
|
|
|
export default {
|
|
|
name: 'MaterialDetailTable',
|
|
|
- components: {
|
|
|
- MaterialImportDialog
|
|
|
- },
|
|
|
+ components: {},
|
|
|
|
|
|
/**
|
|
|
* 组件属性定义
|
|
@@ -286,10 +304,28 @@ export default {
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
- * 导入弹窗显示状态 - 控制物料导入弹窗的显示/隐藏
|
|
|
+ * 选中的物料ID - 当前在下拉框中选中的物料ID
|
|
|
+ * @type {string|null}
|
|
|
+ */
|
|
|
+ selectedMaterialId: null,
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 物料选项列表 - 远程搜索返回的物料选项
|
|
|
+ * @type {ItemRecord[]}
|
|
|
+ */
|
|
|
+ materialOptions: [],
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 物料搜索加载状态 - 控制远程搜索时的加载状态
|
|
|
* @type {boolean}
|
|
|
*/
|
|
|
- importDialogVisible: false,
|
|
|
+ materialLoading: false,
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 搜索防抖定时器 - 用于防抖处理远程搜索
|
|
|
+ * @type {number|null}
|
|
|
+ */
|
|
|
+ searchTimer: null,
|
|
|
|
|
|
/**
|
|
|
* 事件常量
|
|
@@ -479,49 +515,169 @@ export default {
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
- * 处理导入物料按钮点击事件
|
|
|
- * @description 打开物料导入弹窗,允许用户选择和导入物料
|
|
|
+ * 远程搜索物料
|
|
|
+ * @description 根据关键词远程搜索物料数据,支持防抖处理
|
|
|
+ * @param {string} query - 搜索关键词
|
|
|
* @returns {void}
|
|
|
*/
|
|
|
- handleImportMaterial() {
|
|
|
- this.importDialogVisible = true
|
|
|
+ remoteSearchMaterial(query) {
|
|
|
+ // 清除之前的定时器
|
|
|
+ if (this.searchTimer) {
|
|
|
+ clearTimeout(this.searchTimer)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果查询为空,清空选项
|
|
|
+ if (!query) {
|
|
|
+ this.materialOptions = []
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置防抖定时器
|
|
|
+ this.searchTimer = setTimeout(async () => {
|
|
|
+ await this.searchMaterials(query)
|
|
|
+ }, 300)
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
- * 处理表格刷新事件
|
|
|
- * @description 触发刷新事件,通知父组件重新加载数据
|
|
|
- * @returns {void}
|
|
|
- * @emits refresh
|
|
|
+ * 搜索物料数据
|
|
|
+ * @description 调用API搜索物料数据
|
|
|
+ * @param {string} keyword - 搜索关键词
|
|
|
+ * @returns {Promise<void>}
|
|
|
+ * @throws {Error} 当API调用失败时抛出异常
|
|
|
*/
|
|
|
- handleRefresh() {
|
|
|
- this.$emit(MATERIAL_DETAIL_EVENTS.REFRESH)
|
|
|
+ async searchMaterials(keyword) {
|
|
|
+ try {
|
|
|
+ this.materialLoading = true
|
|
|
+
|
|
|
+ const response = await getItemList(1, 20, {
|
|
|
+ itemName: keyword
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response?.data?.success && response.data.data?.records) {
|
|
|
+ // 转换API返回的字段名称为组件所需的格式
|
|
|
+ this.materialOptions = response.data.data.records.map(item => ({
|
|
|
+ id: item.Item_ID || item.id,
|
|
|
+ itemId: item.Item_ID,
|
|
|
+ itemCode: item.Item_Code || item.itemCode,
|
|
|
+ itemName: item.Item_Name || item.itemName,
|
|
|
+ specs: item.Item_PECS || item.specs,
|
|
|
+ unit: item.Item_Unit || item.unit,
|
|
|
+ category: item.Item_Category || item.category,
|
|
|
+ brand: item.Item_Brand || item.brand,
|
|
|
+ model: item.Item_Model || item.model,
|
|
|
+ description: item.Item_Description || item.description,
|
|
|
+ unitPrice: item.Item_UnitPrice || item.unitPrice || '0',
|
|
|
+ remark: item.Item_Remark || item.remark || '',
|
|
|
+ // 保留原始数据以备后用
|
|
|
+ _raw: item
|
|
|
+ }))
|
|
|
+ } else {
|
|
|
+ this.materialOptions = []
|
|
|
+ const errorMsg = response?.data?.msg || '搜索物料失败'
|
|
|
+ this.$message.warning(errorMsg)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.materialOptions = []
|
|
|
+ this.$message.error('网络错误,搜索物料失败')
|
|
|
+ } finally {
|
|
|
+ this.materialLoading = false
|
|
|
+ }
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
- * 处理物料导入确认事件
|
|
|
- * @description 处理物料导入确认,将导入的物料数据传递给父组件并关闭弹窗
|
|
|
- * @param {MaterialDetailRecord[]} importedMaterials - 导入的物料列表,必须为有效的物料明细数组
|
|
|
+ * 处理导入选中物料
|
|
|
+ * @description 将选中的物料导入到物料明细表中
|
|
|
* @returns {void}
|
|
|
- * @emits material-import
|
|
|
*/
|
|
|
- handleImportConfirm(importedMaterials) {
|
|
|
- if (!Array.isArray(importedMaterials)) {
|
|
|
- this.$message.warning('导入的物料数据格式不正确')
|
|
|
+ handleImportSelectedMaterial() {
|
|
|
+ if (!this.selectedMaterialId) {
|
|
|
+ this.$message.warning('请先选择要导入的物料')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查找选中的物料数据
|
|
|
+ const selectedMaterial = this.materialOptions.find(item => item.id === this.selectedMaterialId)
|
|
|
+ if (!selectedMaterial) {
|
|
|
+ this.$message.warning('未找到选中的物料数据')
|
|
|
return
|
|
|
}
|
|
|
- this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_IMPORT, importedMaterials)
|
|
|
- this.importDialogVisible = false
|
|
|
+
|
|
|
+ // 检查是否已存在相同物料
|
|
|
+ const existingMaterial = this.materialDetails.find(item => item.itemCode === selectedMaterial.itemCode)
|
|
|
+ if (existingMaterial) {
|
|
|
+ this.$message.warning(`物料 ${selectedMaterial.itemName} 已存在,请勿重复导入`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构造物料明细数据
|
|
|
+ const materialDetail = this.prepareMaterialDetailData(selectedMaterial)
|
|
|
+
|
|
|
+ // 触发导入事件
|
|
|
+ this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_IMPORT, [materialDetail])
|
|
|
+
|
|
|
+ // 清空选择
|
|
|
+ this.selectedMaterialId = null
|
|
|
+ this.materialOptions = []
|
|
|
+
|
|
|
+ this.$message.success(`物料 ${selectedMaterial.itemName} 导入成功`)
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 准备物料明细数据
|
|
|
+ * @description 将选中的物料数据转换为物料明细表所需的格式
|
|
|
+ * @param {ItemRecord} material - 物料数据
|
|
|
+ * @returns {MaterialDetailRecord} 格式化后的物料明细数据
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ prepareMaterialDetailData(material) {
|
|
|
+ return {
|
|
|
+ id: this.generateUniqueId(),
|
|
|
+ itemId: material.itemId || material.id,
|
|
|
+ itemCode: material.itemCode,
|
|
|
+ itemName: material.itemName,
|
|
|
+ specs: material.specs || '',
|
|
|
+ specification: material.specs || '',
|
|
|
+ unit: material.unit || '',
|
|
|
+ category: material.category || '',
|
|
|
+ brand: material.brand || '',
|
|
|
+ model: material.model || '',
|
|
|
+ description: material.description || '',
|
|
|
+ unitPrice: parseFloat(material.unitPrice) || 0,
|
|
|
+ orderQuantity: 1,
|
|
|
+ confirmQuantity: 1,
|
|
|
+ availableQuantity: 0,
|
|
|
+ taxRate: 0,
|
|
|
+ taxAmount: 0,
|
|
|
+ totalAmount: 0,
|
|
|
+ itemStatus: MaterialDetailStatus.UNCONFIRMED,
|
|
|
+ dataSource: MaterialDetailDataSource.REMOTE,
|
|
|
+ isDeletable: true,
|
|
|
+ remark: material.remark || ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成唯一ID
|
|
|
+ * @description 生成物料明细的唯一标识符
|
|
|
+ * @returns {string} 唯一ID
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ generateUniqueId() {
|
|
|
+ return 'material_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
- * 处理物料导入取消事件
|
|
|
- * @description 取消物料导入操作,关闭导入弹窗
|
|
|
+ * 处理表格刷新事件
|
|
|
+ * @description 触发刷新事件,通知父组件重新加载数据
|
|
|
* @returns {void}
|
|
|
+ * @emits refresh
|
|
|
*/
|
|
|
- handleImportCancel() {
|
|
|
- this.importDialogVisible = false
|
|
|
+ handleRefresh() {
|
|
|
+ this.$emit(MATERIAL_DETAIL_EVENTS.REFRESH)
|
|
|
},
|
|
|
|
|
|
+
|
|
|
+
|
|
|
/**
|
|
|
* 处理分页页码变化事件
|
|
|
* @description 当用户切换页码时触发,更新当前页码
|
|
@@ -587,7 +743,6 @@ export default {
|
|
|
* getStatusTagType(1) // 返回 'success'
|
|
|
*/
|
|
|
getStatusTagType(itemStatus) {
|
|
|
- console.log(itemStatus.length)
|
|
|
return getMaterialDetailStatusTagType(itemStatus)
|
|
|
},
|
|
|
|
|
@@ -893,6 +1048,17 @@ export default {
|
|
|
|
|
|
return index >= 0 ? index : -1
|
|
|
}
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 组件销毁前的清理工作
|
|
|
+ * @description 清除定时器,避免内存泄漏
|
|
|
+ */
|
|
|
+ beforeDestroy() {
|
|
|
+ if (this.searchTimer) {
|
|
|
+ clearTimeout(this.searchTimer)
|
|
|
+ this.searchTimer = null
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
@@ -932,6 +1098,20 @@ export default {
|
|
|
gap: 8px;
|
|
|
}
|
|
|
|
|
|
+.material-select-container {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.material-select-container .el-select {
|
|
|
+ min-width: 300px;
|
|
|
+}
|
|
|
+
|
|
|
+.material-select-container .el-button {
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
.material-detail-content {
|
|
|
background-color: #ffffff;
|
|
|
border-radius: 4px;
|