|
@@ -181,908 +181,11 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-/**
|
|
|
- * @fileoverview 物料明细表格组件
|
|
|
- * @description 基于AvueJS的物料明细数据展示和操作组件,支持分页、搜索、删除等功能
|
|
|
- * @typedef {import('./types').MaterialDetailTableComponent} MaterialDetailTableComponent
|
|
|
- */
|
|
|
+import materialDetailMixin from './material-detail-mixin';
|
|
|
|
|
|
-import { getMaterialDetailOption, DEFAULT_PAGINATION_CONFIG } from './material-detail-option'
|
|
|
-import {
|
|
|
- MaterialDetailStatus,
|
|
|
- getOrderItemStatusLabel as getMaterialDetailStatusLabel,
|
|
|
- getOrderItemStatusTagType as getMaterialDetailStatusTagType,
|
|
|
- getOrderItemStatusColor as getMaterialDetailStatusColor,
|
|
|
- MaterialDetailDataSource
|
|
|
-} from '@/constants/order'
|
|
|
-import { MATERIAL_DETAIL_EVENTS, DIALOG_EVENTS } from './events'
|
|
|
-import { getMaterialFullList } from '@/api/order/sales-order'
|
|
|
-import {
|
|
|
- formatAmount,
|
|
|
- formatFloatNumber,
|
|
|
- formatIntegerNumber,
|
|
|
- formatUnitPrice,
|
|
|
- formatTaxRate,
|
|
|
- preciseMultiply,
|
|
|
- preciseDivide,
|
|
|
- preciseRound,
|
|
|
- validateNumber,
|
|
|
- NUMBER_TYPES
|
|
|
-} from './number-format-utils'
|
|
|
-
|
|
|
-/**
|
|
|
- * @typedef {import('./types').MaterialDetailRecord} MaterialDetailRecord
|
|
|
- * @typedef {import('./types').MaterialUpdateEventData} MaterialUpdateEventData
|
|
|
- * @typedef {import('./types').MaterialDeleteEventData} MaterialDeleteEventData
|
|
|
- * @typedef {import('./types').MaterialDetailQueryParams} MaterialDetailQueryParams
|
|
|
- * @typedef {import('smallwei__avue/crud').AvueCrudOption} AvueCrudOption
|
|
|
- * @typedef {import('smallwei__avue/crud').AvueCrudColumn} AvueCrudColumn
|
|
|
- * @typedef {import('smallwei__avue/crud').PageOption} PageOption
|
|
|
- */
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-// 使用@types/smallwei__avue/crud中的PageOption类型代替PaginationConfig
|
|
|
-
|
|
|
-/**
|
|
|
- * 组件数据类型定义
|
|
|
- * @typedef {Object} MaterialDetailTableData
|
|
|
- * @property {Partial<MaterialDetailRecord>} formData - 表单数据
|
|
|
- * @property {PageOption} page - 分页配置
|
|
|
- * @property {boolean} importDialogVisible - 导入弹窗显示状态
|
|
|
- */
|
|
|
-
|
|
|
-// 状态处理已统一使用明细管理中的工具函数,无需本地映射常量
|
|
|
-
|
|
|
-/**
|
|
|
- * 物料明细表格组件
|
|
|
- * @description 用于展示和编辑订单的物料明细信息,支持物料导入和实时编辑功能
|
|
|
- * 当物料数量、单价、税率等字段变更时,自动计算总金额和税额,并触发父组件重新计算订单总计
|
|
|
- * @emits {MaterialDetailRecord[]} material-import - 物料导入事件
|
|
|
- * @emits {Object} material-update - 物料明细更新事件,包含更新的行数据和索引
|
|
|
- * @emits {Object} material-delete - 物料明细删除事件
|
|
|
- * @emits {void} refresh - 刷新事件
|
|
|
- */
|
|
|
export default {
|
|
|
name: 'MaterialDetailTable',
|
|
|
- components: {},
|
|
|
-
|
|
|
- /**
|
|
|
- * 组件属性定义
|
|
|
- * @description 定义组件接收的外部属性
|
|
|
- */
|
|
|
- props: {
|
|
|
- /**
|
|
|
- * 是否为编辑模式 - 控制表格是否可编辑
|
|
|
- * @type {boolean}
|
|
|
- */
|
|
|
- editMode: {
|
|
|
- type: Boolean,
|
|
|
- default: false
|
|
|
- },
|
|
|
- /**
|
|
|
- * 订单ID - 关联的订单唯一标识符
|
|
|
- * @type {string|number|null}
|
|
|
- */
|
|
|
- orderId: {
|
|
|
- type: [String, Number],
|
|
|
- default: null,
|
|
|
- validator: (value) => value === null || value === undefined || (typeof value === 'string' && value.length > 0) || (typeof value === 'number' && value > 0)
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 物料明细列表 - 要展示的物料明细数据
|
|
|
- * @type {MaterialDetailRecord[]} 物料明细数据数组,每个元素包含物料的详细信息
|
|
|
- */
|
|
|
- materialDetails: {
|
|
|
- type: Array,
|
|
|
- required: true,
|
|
|
- default: () => [],
|
|
|
- validator: (value) => Array.isArray(value) && value.every(item =>
|
|
|
- typeof item === 'object' && item !== null &&
|
|
|
- typeof item.id === 'string' &&
|
|
|
- typeof item.itemCode === 'string'
|
|
|
- )
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 组件数据
|
|
|
- * @returns {MaterialDetailTableData} 组件响应式数据对象
|
|
|
- * @this {MaterialDetailTableComponent}
|
|
|
- */
|
|
|
- data() {
|
|
|
- return {
|
|
|
- /**
|
|
|
- * 表单数据 - 当前编辑行的数据
|
|
|
- * @type {Partial<MaterialDetailRecord>} 物料明细表单数据对象
|
|
|
- */
|
|
|
- formData: {},
|
|
|
-
|
|
|
- /**
|
|
|
- * 分页配置 - AvueJS表格分页相关配置
|
|
|
- * @type {PaginationConfig} 包含currentPage、pageSize、total等属性的分页配置对象
|
|
|
- */
|
|
|
- page: {
|
|
|
- currentPage: 1,
|
|
|
- pageSize: DEFAULT_PAGINATION_CONFIG.pageSize,
|
|
|
- total: 0
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 选中的物料ID - 当前在下拉框中选中的物料ID
|
|
|
- * @type {string|null}
|
|
|
- */
|
|
|
- selectedMaterialId: null,
|
|
|
-
|
|
|
- /**
|
|
|
- * 物料选项列表 - 远程搜索返回的物料选项
|
|
|
- * @type {ItemRecord[]}
|
|
|
- */
|
|
|
- materialOptions: [],
|
|
|
-
|
|
|
- /**
|
|
|
- * 物料搜索加载状态 - 控制远程搜索时的加载状态
|
|
|
- * @type {boolean}
|
|
|
- */
|
|
|
- materialLoading: false,
|
|
|
-
|
|
|
- /**
|
|
|
- * 搜索防抖定时器 - 用于防抖处理远程搜索
|
|
|
- * @type {number|null}
|
|
|
- */
|
|
|
- searchTimer: null,
|
|
|
-
|
|
|
- /**
|
|
|
- * 事件常量
|
|
|
- */
|
|
|
- DIALOG_EVENTS,
|
|
|
-
|
|
|
- /**
|
|
|
- * 正在编辑的行数据 - 用于记录编辑前的状态
|
|
|
- * @type {MaterialDetailRecord|null}
|
|
|
- */
|
|
|
- editingRow: null,
|
|
|
-
|
|
|
- /**
|
|
|
- * 正在编辑的属性名 - 用于记录当前编辑的字段
|
|
|
- * @type {string|null}
|
|
|
- */
|
|
|
- editingProp: null
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 计算属性
|
|
|
- * @this {MaterialDetailTableComponent}
|
|
|
- */
|
|
|
- computed: {
|
|
|
- /**
|
|
|
- * 表格配置选项 - 获取AvueJS表格的配置对象
|
|
|
- * @returns {AvueCrudOption} AvueJS表格配置对象,根据编辑模式配置
|
|
|
- */
|
|
|
- tableOption() {
|
|
|
- return getMaterialDetailOption(this.editMode)
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 当前页显示的数据 - 根据分页配置计算当前页应显示的数据
|
|
|
- * @returns {MaterialDetailRecord[]} 当前页的物料明细数据
|
|
|
- */
|
|
|
- currentPageData() {
|
|
|
- const { currentPage, pageSize } = this.page
|
|
|
- const startIndex = (currentPage - 1) * pageSize
|
|
|
- const endIndex = startIndex + pageSize
|
|
|
- return this.materialDetails.slice(startIndex, endIndex)
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 监听器
|
|
|
- * @this {MaterialDetailTableComponent}
|
|
|
- */
|
|
|
- watch: {
|
|
|
- /**
|
|
|
- * 监听物料明细变化
|
|
|
- * @param {MaterialDetailRecord[]} newVal - 新的物料明细列表
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- materialDetails: {
|
|
|
- handler(newVal) {
|
|
|
- this.page.total = newVal.length
|
|
|
- },
|
|
|
- immediate: true
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 组件方法
|
|
|
- * @this {MaterialDetailTableComponent}
|
|
|
- */
|
|
|
- methods: {
|
|
|
- /**
|
|
|
- * 验证整数输入
|
|
|
- * @param {string} value - 输入值
|
|
|
- * @param {Object} row - 当前行数据
|
|
|
- * @param {string} field - 字段名
|
|
|
- * @param {number} min - 最小值
|
|
|
- * @param {number} max - 最大值
|
|
|
- */
|
|
|
- validateIntegerInput(value, row, field, min = 0, max = 999999) {
|
|
|
- // 允许空值和部分输入(如正在输入的数字)
|
|
|
- if (value === '' || value === '-') {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // 移除所有非数字字符(除了负号)
|
|
|
- let cleanValue = value.replace(/[^-\d]/g, '')
|
|
|
-
|
|
|
- // 确保负号只能在开头
|
|
|
- if (cleanValue.indexOf('-') > 0) {
|
|
|
- cleanValue = cleanValue.replace(/-/g, '')
|
|
|
- }
|
|
|
-
|
|
|
- // 如果有值,转换为整数
|
|
|
- if (cleanValue !== '' && cleanValue !== '-') {
|
|
|
- const numValue = parseInt(cleanValue, 10)
|
|
|
-
|
|
|
- // 检查范围
|
|
|
- if (numValue < min) {
|
|
|
- row[field] = min
|
|
|
- } else if (numValue > max) {
|
|
|
- row[field] = max
|
|
|
- } else {
|
|
|
- row[field] = numValue
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 验证浮点数输入(输入时验证)
|
|
|
- * @param {string} value - 输入值
|
|
|
- * @param {Object} row - 当前行数据
|
|
|
- * @param {string} field - 字段名
|
|
|
- * @param {number} min - 最小值
|
|
|
- * @param {number} max - 最大值
|
|
|
- */
|
|
|
- validateFloatInput(value, row, field, min = 0, max = 999999.99) {
|
|
|
- // 允许空值和部分输入(包括单独的小数点、负号等)
|
|
|
- if (value === '' || value === '-' || value === '.' || value === '-.') {
|
|
|
- row[field] = value
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // 移除无效字符,只保留数字、小数点和负号
|
|
|
- let cleanValue = value.replace(/[^-\d.]/g, '')
|
|
|
-
|
|
|
- // 确保负号只能在开头
|
|
|
- if (cleanValue.indexOf('-') > 0) {
|
|
|
- cleanValue = cleanValue.replace(/-/g, '')
|
|
|
- }
|
|
|
-
|
|
|
- // 确保只有一个小数点
|
|
|
- const parts = cleanValue.split('.')
|
|
|
- if (parts.length > 2) {
|
|
|
- cleanValue = parts[0] + '.' + parts.slice(1).join('')
|
|
|
- }
|
|
|
-
|
|
|
- // 限制小数位数为2位(但允许继续输入)
|
|
|
- if (parts.length === 2 && parts[1].length > 2) {
|
|
|
- cleanValue = parts[0] + '.' + parts[1].substring(0, 2)
|
|
|
- }
|
|
|
-
|
|
|
- // 更新字段值,但不进行范围检查(留到blur时处理)
|
|
|
- row[field] = cleanValue
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 验证并格式化浮点数(失焦时验证)
|
|
|
- * @param {Object} row - 当前行数据
|
|
|
- * @param {string} field - 字段名
|
|
|
- * @param {number} min - 最小值
|
|
|
- * @param {number} max - 最大值
|
|
|
- * @param {number} precision - 小数位数,默认2位
|
|
|
- */
|
|
|
- validateAndFormatFloatOnBlur(row, field, min = 0, max = 999999.99, precision = 2) {
|
|
|
- const value = row[field]
|
|
|
-
|
|
|
- // 如果是空值或无效输入,设置为最小值
|
|
|
- if (value === '' || value === '.' || value === '-' || value === '-.' || isNaN(parseFloat(value))) {
|
|
|
- row[field] = min
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const numValue = parseFloat(value)
|
|
|
-
|
|
|
- // 范围检查
|
|
|
- if (numValue < min) {
|
|
|
- row[field] = min
|
|
|
- } else if (numValue > max) {
|
|
|
- row[field] = max
|
|
|
- } else {
|
|
|
- // 格式化为指定小数位数
|
|
|
- const multiplier = Math.pow(10, precision)
|
|
|
- const roundedValue = Math.round(numValue * multiplier) / multiplier
|
|
|
- row[field] = roundedValue
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 判断行是否可编辑
|
|
|
- * @description 根据数据来源判断物料明细行是否允许编辑,远程数据(订单ID获取)不可编辑
|
|
|
- * @param {MaterialDetailRecord} row - 物料明细行数据
|
|
|
- * @returns {boolean} 是否可编辑,true表示可编辑,false表示不可编辑
|
|
|
- */
|
|
|
- isRowEditable(row) {
|
|
|
- // 如果没有数据来源信息,默认可编辑
|
|
|
- if (!row || !row.dataSource) {
|
|
|
- return true
|
|
|
- }
|
|
|
-
|
|
|
- // 只有导入的物料可以编辑,远程数据(订单ID获取)不可编辑
|
|
|
- return row.dataSource === MaterialDetailDataSource.IMPORTED
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 远程搜索物料
|
|
|
- * @description 根据关键词远程搜索物料数据,支持防抖处理
|
|
|
- * @param {string} query - 搜索关键词
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- remoteSearchMaterial(query) {
|
|
|
- // 清除之前的定时器
|
|
|
- if (this.searchTimer) {
|
|
|
- clearTimeout(this.searchTimer)
|
|
|
- }
|
|
|
-
|
|
|
- // 如果查询为空,清空选项
|
|
|
- if (!query) {
|
|
|
- this.materialOptions = []
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // 设置防抖定时器
|
|
|
- this.searchTimer = setTimeout(async () => {
|
|
|
- await this.searchMaterials(query)
|
|
|
- }, 300)
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 搜索物料数据
|
|
|
- * @description 调用API搜索物料数据
|
|
|
- * @param {string} keyword - 搜索关键词
|
|
|
- * @returns {Promise<void>}
|
|
|
- * @throws {Error} 当API调用失败时抛出异常
|
|
|
- */
|
|
|
- async searchMaterials(keyword) {
|
|
|
- try {
|
|
|
- this.materialLoading = true
|
|
|
-
|
|
|
- const response = await getMaterialFullList({
|
|
|
- itemName: keyword
|
|
|
- })
|
|
|
-
|
|
|
- if (response?.data?.success && response.data.data) {
|
|
|
- // 转换API返回的字段名称为组件所需的格式
|
|
|
- // getMaterialFullList返回的是SalesOrderItemListRecord[]数组
|
|
|
- this.materialOptions = response.data.data.map(item => ({
|
|
|
- id: item.id,
|
|
|
- itemId: item.Item_ID,
|
|
|
- itemCode: item.Item_Code,
|
|
|
- itemName: item.Item_Name,
|
|
|
- specs: item.Item_PECS || '',
|
|
|
- unit: item.InventoryInfo_Name,
|
|
|
- mainItemCategoryName: item.MainItemCategory_Name,
|
|
|
- mainItemCategoryId: item.MainItemCategory_ID,
|
|
|
- mainItemCategoryCode: item.MainItemCategory_Code,
|
|
|
- unitPrice: item.Item_Price || '0',
|
|
|
- description: item.Item_Description || '',
|
|
|
- warehouseId: item.Warehouse_ID,
|
|
|
- warehouseCode: item.Warehouse_Code,
|
|
|
- warehouseName: item.Warehouse_Name,
|
|
|
- orgId: item.ORG_ID,
|
|
|
- orgCode: item.ORG_CODE,
|
|
|
- orgName: item.ORG_NAME,
|
|
|
- // 保留原始数据以备后用
|
|
|
- _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 将选中的物料导入到物料明细表中
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- handleImportSelectedMaterial() {
|
|
|
- if (!this.selectedMaterialId) {
|
|
|
- this.$message.warning('请先选择要导入的物料')
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // 查找选中的物料数据
|
|
|
- const selectedMaterial = this.materialOptions.find(item => item.id === this.selectedMaterialId)
|
|
|
- if (!selectedMaterial) {
|
|
|
- this.$message.warning('未找到选中的物料数据')
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // 检查是否已存在相同物料
|
|
|
- const existingMaterial = this.materialDetails.find(item => item.itemCode === selectedMaterial.itemCode)
|
|
|
- if (existingMaterial) {
|
|
|
- this.$message.warning(`物料 ${selectedMaterial.itemName} 已存在,请勿重复导入`)
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // 构造物料明细数据
|
|
|
- let materialDetail = this.prepareMaterialDetailData(selectedMaterial)
|
|
|
-
|
|
|
- // 导入时自动计算金额
|
|
|
- materialDetail = this.calculateAmounts(materialDetail)
|
|
|
-
|
|
|
- // 触发导入事件
|
|
|
- this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_IMPORT, [materialDetail])
|
|
|
- this.$emit(MATERIAL_DETAIL_EVENTS.REFRESH)
|
|
|
-
|
|
|
- // 清空选择
|
|
|
- this.selectedMaterialId = null
|
|
|
- this.materialOptions = []
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 准备物料明细数据
|
|
|
- * @description 将选中的物料数据转换为物料明细表所需的格式
|
|
|
- * @param {import('@/api/types/order').SalesOrderItemListRecord} material - 物料数据(来自getMaterialFullList API)
|
|
|
- * @returns {MaterialDetailRecord} 格式化后的物料明细数据
|
|
|
- * @private
|
|
|
- */
|
|
|
- prepareMaterialDetailData(material) {
|
|
|
- return {
|
|
|
- id: this.generateUniqueId(),
|
|
|
- itemId: material.itemId,
|
|
|
- itemCode: material.itemCode,
|
|
|
- itemName: material.itemName,
|
|
|
- specs: material.specs || '',
|
|
|
- specification: material.specs || '',
|
|
|
- unit: material.unit || '',
|
|
|
- mainItemCategoryName: material.mainItemCategoryName || material.MainItemCategory_Name || '',
|
|
|
- mainItemCategoryId: material.mainItemCategoryId || material.MainItemCategory_ID,
|
|
|
- mainItemCategoryCode: material.mainItemCategoryCode || material.MainItemCategory_Code || '',
|
|
|
- description: material.description || '',
|
|
|
- warehouseId: material.warehouseId,
|
|
|
- warehouseCode: material.warehouseCode,
|
|
|
- warehouseName: material.warehouseName,
|
|
|
- orgId: material.orgId,
|
|
|
- orgCode: material.orgCode,
|
|
|
- orgName: material.orgName,
|
|
|
- unitPrice: material.unitPrice || 0,
|
|
|
- orderQuantity: 1,
|
|
|
- confirmQuantity: 1,
|
|
|
- availableQuantity: 0,
|
|
|
- taxRate: 0,
|
|
|
- taxAmount: 0,
|
|
|
- totalAmount: 0,
|
|
|
- itemStatus: MaterialDetailStatus.UNCONFIRMED,
|
|
|
- dataSource: MaterialDetailDataSource.IMPORTED,
|
|
|
- isDeletable: true,
|
|
|
- remark: ''
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成唯一ID
|
|
|
- * @description 生成物料明细的唯一标识符
|
|
|
- * @returns {string} 唯一ID
|
|
|
- * @private
|
|
|
- */
|
|
|
- generateUniqueId() {
|
|
|
- return 'material_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理表格刷新事件
|
|
|
- * @description 触发刷新事件,通知父组件重新加载数据
|
|
|
- * @returns {void}
|
|
|
- * @emits refresh
|
|
|
- */
|
|
|
- handleRefresh() {
|
|
|
- this.$emit(MATERIAL_DETAIL_EVENTS.REFRESH)
|
|
|
- },
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理分页页码变化事件
|
|
|
- * @description 当用户切换页码时触发,更新当前页码
|
|
|
- * @param {number} currentPage - 新的页码,从1开始
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- handleCurrentChange(currentPage) {
|
|
|
- this.page.currentPage = currentPage
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理分页大小变化事件
|
|
|
- * @description 当用户改变每页显示条数时触发,重置到第一页
|
|
|
- * @param {number} pageSize - 新的每页显示条数
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- handleSizeChange(pageSize) {
|
|
|
- this.page.pageSize = pageSize
|
|
|
- this.page.currentPage = 1
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 格式化浮点数显示
|
|
|
- * @description 格式化浮点数为4位小数的字符串
|
|
|
- * @param {number|string|null|undefined} value - 数值
|
|
|
- * @returns {string} 格式化后的字符串
|
|
|
- */
|
|
|
- formatFloatNumber(value) {
|
|
|
- return formatFloatNumber(value)
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 格式化金额显示
|
|
|
- * @description 格式化金额为带货币符号的字符串
|
|
|
- * @param {number|string|null|undefined} amount - 金额数值
|
|
|
- * @param {boolean} withSymbol - 是否显示货币符号
|
|
|
- * @returns {string} 格式化后的金额字符串
|
|
|
- */
|
|
|
- formatAmount(amount, withSymbol = true) {
|
|
|
- return formatAmount(amount, withSymbol)
|
|
|
- },
|
|
|
-
|
|
|
- formatUnitPrice,
|
|
|
- formatTaxRate,
|
|
|
-
|
|
|
- /**
|
|
|
- * 格式化整数显示
|
|
|
- * @description 格式化整数为字符串
|
|
|
- * @param {number|string|null|undefined} value - 整数数值
|
|
|
- * @returns {string} 格式化后的整数字符串
|
|
|
- */
|
|
|
- formatIntegerNumber(value) {
|
|
|
- return formatIntegerNumber(value)
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取状态标签类型
|
|
|
- * @description 根据物料明细状态值返回对应的Element UI标签类型
|
|
|
- * @param {typeof MaterialDetailStatus[keyof typeof MaterialDetailStatus]} itemStatus - 物料明细状态值
|
|
|
- * @returns {string} Element UI标签类型
|
|
|
- * @example
|
|
|
- * getStatusTagType(0) // 返回 'warning'
|
|
|
- * getStatusTagType(1) // 返回 'success'
|
|
|
- */
|
|
|
- getStatusTagType(itemStatus) {
|
|
|
- return getMaterialDetailStatusTagType(itemStatus)
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取状态文本
|
|
|
- * @description 根据物料明细状态值返回对应的中文描述
|
|
|
- * @param {number} itemStatus - 物料明细状态值
|
|
|
- * @returns {string} 状态的中文描述文本
|
|
|
- * @example
|
|
|
- * getStatusText(0) // 返回 '待确认'
|
|
|
- * getStatusText(1) // 返回 '已确认'
|
|
|
- */
|
|
|
- getStatusText(itemStatus) {
|
|
|
- return getMaterialDetailStatusLabel(itemStatus)
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理删除物料操作
|
|
|
- * @description 删除指定的物料明细记录,仅允许删除可删除的物料
|
|
|
- * @param {MaterialDetailRecord} row - 要删除的物料明细记录
|
|
|
- * @param {number} index - 记录在当前页的索引位置
|
|
|
- * @returns {void}
|
|
|
- * @emits material-delete
|
|
|
- */
|
|
|
- async handleDeleteMaterial(row, index) {
|
|
|
- try {
|
|
|
- await this.$confirm(
|
|
|
- `确定要删除物料 "${row.itemName}" 吗?`,
|
|
|
- '删除确认',
|
|
|
- {
|
|
|
- confirmButtonText: '确定',
|
|
|
- cancelButtonText: '取消',
|
|
|
- type: 'warning'
|
|
|
- }
|
|
|
- )
|
|
|
-
|
|
|
- // 触发删除事件,传递物料记录和索引
|
|
|
- this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_DELETE, { row, index })
|
|
|
- this.$message.success('物料删除成功')
|
|
|
- } catch (error) {
|
|
|
- // 用户取消删除操作
|
|
|
- if (error !== 'cancel') {
|
|
|
- this.$message.error('删除操作失败')
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理表格行删除事件
|
|
|
- * @description AvueJS表格的删除事件处理器,委托给自定义删除方法
|
|
|
- * @param {MaterialDetailRecord} row - 要删除的行数据
|
|
|
- * @param {number} index - 行索引
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- handleRowDelete(row, index) {
|
|
|
- this.handleDeleteMaterial(row, index)
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理行更新事件
|
|
|
- * @description 当用户编辑表格行数据时触发,执行自动计算逻辑
|
|
|
- * @param {MaterialDetailRecord} row - 更新后的行数据
|
|
|
- * @param {number} index - 行索引
|
|
|
- * @param {boolean} done - 完成回调函数
|
|
|
- * @returns {void}
|
|
|
- * @emits material-update
|
|
|
- */
|
|
|
- async handleRowUpdate(row, index, done) {
|
|
|
- try {
|
|
|
- // 执行自动计算
|
|
|
- const calculatedRow = this.calculateAmounts(row)
|
|
|
-
|
|
|
- // 触发更新事件,传递计算后的数据
|
|
|
- this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_UPDATE, { row: calculatedRow, index })
|
|
|
-
|
|
|
- // 完成编辑
|
|
|
- done(calculatedRow)
|
|
|
-
|
|
|
- this.$message.success('物料明细更新成功')
|
|
|
- } catch (error) {
|
|
|
- this.$message.error('更新失败:' + error.message)
|
|
|
- done(false)
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理订单数量失焦事件
|
|
|
- * @description 当订单数量输入框失焦时,触发数量变更处理
|
|
|
- * @param {MaterialDetailRecord} row - 行数据
|
|
|
- * @param {number} index - 行索引
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- handleQuantityBlur(row, index) {
|
|
|
- // 如果 index 无效,尝试通过 row 数据找到正确的索引
|
|
|
- const actualIndex = this.findRowIndex(row, index)
|
|
|
- this.handleQuantityChange(row, actualIndex)
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理订单数量变更
|
|
|
- * @description 当订单数量发生变化时,自动计算总金额和税额,并触发父组件重新计算订单总计
|
|
|
- * @param {MaterialDetailRecord} row - 行数据
|
|
|
- * @param {number} index - 行索引
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- handleQuantityChange(row, index) {
|
|
|
- const calculatedRow = this.calculateAmounts(row)
|
|
|
- Object.assign(row, calculatedRow)
|
|
|
- this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_UPDATE, { row, index })
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理税率变更
|
|
|
- * @description 当税率发生变化时,重新计算税额,并触发父组件重新计算订单总计
|
|
|
- * @param {MaterialDetailRecord} row - 行数据
|
|
|
- * @param {number} index - 行索引
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- handleTaxRateChange(row, index) {
|
|
|
- this.calculateTaxAmount(row)
|
|
|
- this.$emit('material-update', { row, index })
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理单价失焦事件
|
|
|
- * @description 当单价输入框失焦时,先格式化数值,再计算总金额和税额,并触发父组件重新计算订单总计
|
|
|
- * @param {MaterialDetailRecord} row - 行数据
|
|
|
- * @param {number} index - 行索引
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- handleUnitPriceBlur(row, index) {
|
|
|
- // 先格式化数值
|
|
|
- this.validateAndFormatFloatOnBlur(row, 'unitPrice')
|
|
|
- // 如果 index 无效,尝试通过 row 数据找到正确的索引
|
|
|
- const actualIndex = this.findRowIndex(row, index)
|
|
|
- // 再处理单价变更
|
|
|
- this.handleUnitPriceChange(row, actualIndex)
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理单价变更
|
|
|
- * @description 当单价发生变化时,自动计算总金额和税额,并触发父组件重新计算订单总计
|
|
|
- * @param {MaterialDetailRecord} row - 行数据
|
|
|
- * @param {number} index - 行索引
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- handleUnitPriceChange(row, index) {
|
|
|
- const calculatedRow = this.calculateAmounts(row)
|
|
|
- Object.assign(row, calculatedRow)
|
|
|
- this.$emit('material-update', { row, index })
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理税额变更
|
|
|
- * @description 当税额手动修改时,反推税率,并触发父组件重新计算订单总计
|
|
|
- * @param {MaterialDetailRecord} row - 行数据
|
|
|
- * @param {number} index - 行索引
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- handleTaxAmountChange(row, index) {
|
|
|
- // 当税额手动修改时,反推税率
|
|
|
- if (row.totalAmount && row.totalAmount > 0) {
|
|
|
- row.taxRate = ((row.taxAmount || 0) / row.totalAmount * 100).toFixed(2)
|
|
|
- }
|
|
|
- this.$emit('material-update', { row, index })
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理总金额变更
|
|
|
- * @description 当总金额手动修改时,重新计算税额,并触发父组件重新计算订单总计
|
|
|
- * @param {MaterialDetailRecord} row - 行数据
|
|
|
- * @param {number} index - 行索引
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- handleTotalAmountChange(row, index) {
|
|
|
- // 当总金额手动修改时,重新计算税额
|
|
|
- this.calculateTaxAmount(row)
|
|
|
- this.$emit('material-update', { row, index })
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理单元格编辑开始事件
|
|
|
- * @description 当用户开始编辑单元格时触发
|
|
|
- * @param {MaterialDetailRecord} row - 编辑的行数据
|
|
|
- * @param {string} prop - 编辑的属性名
|
|
|
- * @param {*} value - 当前值
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- handleCellEditStart(row, prop, value) {
|
|
|
- // 记录编辑前的值,用于计算变化
|
|
|
- this.editingRow = { ...row }
|
|
|
- this.editingProp = prop
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理单元格编辑结束事件
|
|
|
- * @description 当用户结束编辑单元格时触发,执行实时计算
|
|
|
- * @param {MaterialDetailRecord} row - 编辑后的行数据
|
|
|
- * @param {string} prop - 编辑的属性名
|
|
|
- * @param {*} value - 新值
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- handleCellEditEnd(row, prop, value) {
|
|
|
- // 如果编辑的是影响计算的字段,执行自动计算
|
|
|
- if (['orderQuantity', 'unitPrice', 'taxRate'].includes(prop)) {
|
|
|
- const calculatedRow = this.calculateAmounts(row)
|
|
|
-
|
|
|
- // 更新行数据
|
|
|
- Object.assign(row, calculatedRow)
|
|
|
-
|
|
|
- // 触发更新事件
|
|
|
- this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_UPDATE, { row: calculatedRow, index: this.getCurrentRowIndex(row) })
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 自动计算金额
|
|
|
- * @description 根据订单数量、单价和税率自动计算总金额和税额,使用精确计算避免浮点数精度问题
|
|
|
- * @param {MaterialDetailRecord} row - 物料明细记录
|
|
|
- * @returns {MaterialDetailRecord} 计算后的物料明细记录
|
|
|
- */
|
|
|
- calculateAmounts(row) {
|
|
|
- const calculatedRow = { ...row }
|
|
|
-
|
|
|
- // 验证并获取数值
|
|
|
- const quantityValidation = validateNumber(calculatedRow.orderQuantity)
|
|
|
- const priceValidation = validateNumber(calculatedRow.unitPrice)
|
|
|
- const rateValidation = validateNumber(calculatedRow.taxRate)
|
|
|
-
|
|
|
- const orderQuantity = quantityValidation.isValid ? Math.round(quantityValidation.value) : 0
|
|
|
- const unitPrice = priceValidation.isValid ? priceValidation.value : 0
|
|
|
- const taxRate = rateValidation.isValid ? rateValidation.value : 0
|
|
|
-
|
|
|
- // 使用精确计算:订单数量 * 单价
|
|
|
- const totalAmount = preciseMultiply(orderQuantity, unitPrice)
|
|
|
- calculatedRow.totalAmount = preciseRound(totalAmount, 2)
|
|
|
-
|
|
|
- // 使用精确计算:总金额 * 税率 / 100
|
|
|
- const taxAmount = preciseMultiply(totalAmount, preciseDivide(taxRate, 100))
|
|
|
- calculatedRow.taxAmount = preciseRound(taxAmount, 2)
|
|
|
-
|
|
|
- return calculatedRow
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 计算税额
|
|
|
- * @description 根据总金额和税率计算税额,使用精确计算避免浮点数精度问题
|
|
|
- * @param {MaterialDetailRecord} row - 物料明细记录
|
|
|
- * @returns {void}
|
|
|
- */
|
|
|
- calculateTaxAmount(row) {
|
|
|
- const amountValidation = validateNumber(row.totalAmount)
|
|
|
- const rateValidation = validateNumber(row.taxRate)
|
|
|
-
|
|
|
- if (amountValidation.isValid && rateValidation.isValid) {
|
|
|
- const totalAmount = amountValidation.value
|
|
|
- const taxRate = rateValidation.value
|
|
|
-
|
|
|
- // 使用精确计算:总金额 * 税率 / 100
|
|
|
- const taxAmount = preciseMultiply(totalAmount, preciseDivide(taxRate, 100))
|
|
|
- row.taxAmount = preciseRound(taxAmount, 2)
|
|
|
- } else {
|
|
|
- row.taxAmount = 0
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取当前行索引
|
|
|
- * @description 根据行数据获取在当前页中的索引
|
|
|
- * @param {MaterialDetailRecord} row - 行数据
|
|
|
- * @returns {number} 行索引
|
|
|
- */
|
|
|
- getCurrentRowIndex(row) {
|
|
|
- return this.currentPageData.findIndex(item => item.id === row.id)
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 查找行索引
|
|
|
- * @description 根据行数据查找在物料明细列表中的正确索引
|
|
|
- * @param {MaterialDetailRecord} row - 行数据
|
|
|
- * @param {number} providedIndex - 提供的索引
|
|
|
- * @returns {number} 实际索引
|
|
|
- */
|
|
|
- findRowIndex(row, providedIndex) {
|
|
|
- // 如果提供的索引有效,直接使用
|
|
|
- if (providedIndex >= 0 && providedIndex < this.materialDetails.length) {
|
|
|
- return providedIndex
|
|
|
- }
|
|
|
-
|
|
|
- // 否则通过行数据查找索引
|
|
|
- const index = this.materialDetails.findIndex(item => {
|
|
|
- // 优先使用 id 进行匹配
|
|
|
- if (row.id && item.id) {
|
|
|
- return row.id === item.id
|
|
|
- }
|
|
|
- // 如果没有 id,使用物料编码进行匹配
|
|
|
- if (row.itemCode && item.itemCode) {
|
|
|
- return row.itemCode === item.itemCode
|
|
|
- }
|
|
|
- // 最后使用对象引用进行匹配
|
|
|
- return row === item
|
|
|
- })
|
|
|
-
|
|
|
- return index >= 0 ? index : -1
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 组件销毁前的清理工作
|
|
|
- * @description 清除定时器,避免内存泄漏
|
|
|
- */
|
|
|
- beforeDestroy() {
|
|
|
- if (this.searchTimer) {
|
|
|
- clearTimeout(this.searchTimer)
|
|
|
- this.searchTimer = null
|
|
|
- }
|
|
|
- }
|
|
|
+ mixins: [materialDetailMixin],
|
|
|
}
|
|
|
</script>
|
|
|
|