123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152 |
- <template>
- <!-- 物料明细表格容器 -->
- <div class="material-detail-container">
- <!-- 表格头部操作区域 -->
- <div class="material-detail-header">
- <div class="header-left">
- <span class="section-title">物料明细</span>
- <el-tag
- v-if="materialDetails.length > 0"
- type="info"
- size="small"
- class="count-tag"
- >
- 共 {{ materialDetails.length }} 条
- </el-tag>
- </div>
- <div class="header-right">
- <!-- 物料选择区域 -->
- <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>
- <!-- 物料明细表格 -->
- <div class="material-detail-content">
- <avue-crud
- ref="materialDetailCrud"
- :data="currentPageData"
- :option="tableOption"
- :page.sync="page"
- @refresh-change="handleRefresh"
- @current-change="handleCurrentChange"
- @size-change="handleSizeChange"
- @row-del="handleRowDelete"
- @row-update="handleRowUpdate"
- >
- <!-- 可用数量自定义模板 -->
- <template slot="availableQuantity" slot-scope="scope">
- <span>{{ formatFloatNumber(scope.row.availableQuantity) }}</span>
- </template>
- <!-- 确认数量自定义模板 -->
- <template slot="confirmQuantity" slot-scope="scope">
- <span>{{ formatFloatNumber(scope.row.confirmQuantity) }}</span>
- </template>
- <!-- 单价自定义模板 -->
- <template slot="unitPrice" slot-scope="scope">
- <el-input
- v-if="editMode && isRowEditable(scope.row)"
- v-model="scope.row.unitPrice"
- size="mini"
- style="width: 100%"
- placeholder="请输入单价"
- @input="validateFloatInput($event, scope.row, 'unitPrice')"
- @blur="handleUnitPriceBlur(scope.row, scope.$index)"
- />
- <span v-else>{{ formatUnitPrice(scope.row.unitPrice) }}</span>
- </template>
- <!-- 订单数量自定义模板 -->
- <template slot="orderQuantity" slot-scope="scope">
- <el-input
- v-if="editMode && isRowEditable(scope.row)"
- v-model="scope.row.orderQuantity"
- size="mini"
- style="width: 100%"
- placeholder="请输入订单数量"
- @input="validateIntegerInput($event, scope.row, 'orderQuantity')"
- @blur="handleQuantityBlur(scope.row, scope.$index)"
- />
- <span v-else>{{ formatIntegerNumber(scope.row.orderQuantity) }}</span>
- </template>
- <!-- 税率自定义模板 -->
- <template slot="taxRate" slot-scope="scope">
- <el-input
- v-if="editMode && isRowEditable(scope.row)"
- v-model="scope.row.taxRate"
- size="mini"
- style="width: 100%"
- placeholder="请输入税率"
- @input="validateFloatInput($event, scope.row, 'taxRate', 0, 100)"
- @blur="validateAndFormatFloatOnBlur(scope.row, 'taxRate', 0, 100); handleTaxRateChange(scope.row, scope.$index)"
- />
- <span v-else>{{ formatTaxRate(scope.row.taxRate, false) }}%</span>
- </template>
- <!-- 税额自定义模板 -->
- <template slot="taxAmount" slot-scope="scope">
- <el-input
- v-if="editMode && isRowEditable(scope.row)"
- v-model="scope.row.taxAmount"
- size="mini"
- style="width: 100%"
- placeholder="请输入税额"
- @input="validateFloatInput($event, scope.row, 'taxAmount')"
- @blur="validateAndFormatFloatOnBlur(scope.row, 'taxAmount'); handleTaxAmountChange(scope.row, scope.$index)"
- />
- <span v-else>{{ formatAmount(scope.row.taxAmount, false) }}</span>
- </template>
- <!-- 总金额自定义模板 -->
- <template slot="totalAmount" slot-scope="scope">
- <el-input
- v-if="editMode && isRowEditable(scope.row)"
- v-model="scope.row.totalAmount"
- size="mini"
- style="width: 100%"
- placeholder="请输入总金额"
- @input="validateFloatInput($event, scope.row, 'totalAmount')"
- @blur="validateAndFormatFloatOnBlur(scope.row, 'totalAmount'); handleTotalAmountChange(scope.row, scope.$index)"
- />
- <span v-else>{{ formatAmount(scope.row.totalAmount, false) }}</span>
- </template>
- <!-- 明细状态列自定义渲染 -->
- <template slot="status" slot-scope="{ row }">
- <el-tag
- :type="getStatusTagType(row.itemStatus)"
- size="mini"
- >
- {{ getStatusText(row.itemStatus) }}
- </el-tag>
- </template>
- <!-- 操作列自定义渲染 -->
- <template slot="menu" slot-scope="{ row, index }">
- <el-button
- v-if="row.isDeletable"
- type="text"
- size="mini"
- icon="el-icon-delete"
- class="delete-btn"
- @click="handleDeleteMaterial(row, index)"
- >
- 删除
- </el-button>
- </template>
- </avue-crud>
- </div>
- </div>
- </template>
- <script>
- /**
- * @fileoverview 物料明细表格组件
- * @description 基于AvueJS的物料明细数据展示和操作组件,支持分页、搜索、删除等功能
- */
- 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 { getItemList } from '@/api/common'
- 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} 组件响应式数据对象
- */
- 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
- }
- },
- /**
- * 计算属性
- */
- 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)
- }
- },
- /**
- * 监听器
- */
- watch: {
- /**
- * 监听物料明细变化
- * @param {MaterialDetailRecord[]} newVal - 新的物料明细列表
- * @returns {void}
- */
- materialDetails: {
- handler(newVal) {
- this.page.total = newVal.length
- },
- immediate: true
- }
- },
- /**
- * 组件方法
- */
- 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 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 将选中的物料导入到物料明细表中
- * @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
- }
-
- // 构造物料明细数据
- const materialDetail = this.prepareMaterialDetailData(selectedMaterial)
-
- // 触发导入事件
- this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_IMPORT, [materialDetail])
-
- // 清空选择
- this.selectedMaterialId = null
- this.materialOptions = []
- },
- /**
- * 准备物料明细数据
- * @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 触发刷新事件,通知父组件重新加载数据
- * @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
- }
- }
- }
- </script>
- <style scoped>
- .material-detail-container {
- padding: 20px;
- }
- .material-detail-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 16px;
- padding-bottom: 12px;
- border-bottom: 1px solid #ebeef5;
- }
- .header-left {
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .section-title {
- font-size: 16px;
- font-weight: 600;
- color: #303133;
- }
- .count-tag {
- font-size: 12px;
- }
- .header-right {
- display: flex;
- 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;
- }
- .delete-btn {
- color: #f56c6c;
- }
- .delete-btn:hover {
- color: #f78989;
- }
- .amount-text {
- font-weight: 600;
- color: #409eff;
- }
- .dialog-footer {
- text-align: right;
- }
- /* 响应式设计 */
- @media (max-width: 768px) {
- .material-detail-container {
- padding: 12px;
- }
- .material-detail-header {
- flex-direction: column;
- gap: 12px;
- align-items: flex-start;
- }
- .header-right {
- width: 100%;
- justify-content: flex-end;
- }
- }
- </style>
|