|
@@ -0,0 +1,2438 @@
|
|
|
+<template>
|
|
|
+ <ContentWrap v-loading="formLoading">
|
|
|
+ <el-form
|
|
|
+ ref="formRef"
|
|
|
+ :model="formData"
|
|
|
+ :rules="formRules"
|
|
|
+ v-loading="formLoading"
|
|
|
+ style="margin-right: 4em; margin-left: 0.5em; margin-top: 1em"
|
|
|
+ label-width="130px"
|
|
|
+ >
|
|
|
+ <div class="base-expandable-content">
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item :label="t('bomList.name')" prop="name">
|
|
|
+ <el-input type="text" v-model="formData.name" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item :label="t('mainPlan.MaintenanceMethod')" prop="type">
|
|
|
+ <el-select v-model="formData.outsourcingFlag" :placeholder="t('faultForm.choose')" clearable>
|
|
|
+ <el-option
|
|
|
+ v-for="dict in getIntDictOptions(DICT_TYPE.PMS_ORDER_PROCESS_MODE)"
|
|
|
+ :key="dict.value"
|
|
|
+ :label="dict.label"
|
|
|
+ :value="dict.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item :label="t('mainPlan.MaintenanceCost')" prop="cost">
|
|
|
+ <el-input
|
|
|
+ v-model="formData.cost"
|
|
|
+ placeholder="根据物料消耗自动生成"
|
|
|
+ disabled
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item :label="t('fault.start')" prop="actualStartTime">
|
|
|
+ <el-date-picker
|
|
|
+ style="width: 150%"
|
|
|
+ v-model="formData.actualStartTime"
|
|
|
+ type="datetime"
|
|
|
+ value-format="x"
|
|
|
+ :placeholder="t('fault.start')"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item :label="t('fault.end')" prop="actualEndTime">
|
|
|
+ <el-date-picker
|
|
|
+ style="width: 150%"
|
|
|
+ v-model="formData.actualEndTime"
|
|
|
+ type="datetime"
|
|
|
+ value-format="x"
|
|
|
+ :placeholder="t('fault.end')"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item :label="t('mainPlan.otherCost')" prop="otherCost">
|
|
|
+ <el-input
|
|
|
+ v-model="formData.otherCost"
|
|
|
+ @input="handleInput(formData.otherCost, 'otherCost')"
|
|
|
+ placeholder="其他费用"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item :label="t('iotDevice.code')" prop="deviceCode">
|
|
|
+ <el-input type="text" v-model="formData.deviceCode" disabled/>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item :label="t('iotDevice.name')" prop="deviceName">
|
|
|
+ <el-input type="text" v-model="formData.deviceName" disabled/>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item :label="t('faultForm.remark')" prop="remark">
|
|
|
+ <el-input v-model="formData.remark" type="textarea" :placeholder="t('faultForm.rHolder')" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+ </el-form>
|
|
|
+ </ContentWrap>
|
|
|
+ <ContentWrap>
|
|
|
+ <!-- 列表 -->
|
|
|
+ <ContentWrap>
|
|
|
+ <el-table v-loading="loading" :data="paginatedList" :stripe="true" @row-click="handleRowClick" :row-class-name="tableRowClassName"
|
|
|
+ :show-overflow-tooltip="true" :header-cell-style="tableHeaderStyle" :span-method="handleSpanMethod">
|
|
|
+ <!-- 序号列 -->
|
|
|
+ <el-table-column
|
|
|
+ type="index"
|
|
|
+ :label="t('maintain.serial')"
|
|
|
+ align="center"
|
|
|
+ prop="serial"
|
|
|
+ :width="columnWidths.serial"
|
|
|
+ fixed="left"
|
|
|
+ />
|
|
|
+ <el-table-column :label="t('bomList.bomNode')" align="center" prop="bomNodeId" v-if="false"/>
|
|
|
+ <el-table-column :label="t('iotDevice.code')" align="center" prop="deviceCode" :width="columnWidths.deviceCode" fixed="left" v-if="false">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="full-content-cell"> <!-- 自定义样式 -->
|
|
|
+ {{ row.deviceCode }}
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column :label="t('iotDevice.name')" align="center" prop="deviceName" :width="columnWidths.deviceName" fixed="left" v-if="false">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="full-content-cell"> <!-- 自定义样式 -->
|
|
|
+ {{ row.deviceName }}
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <!-- 保养项组列 -->
|
|
|
+ <el-table-column
|
|
|
+ v-if="hasGroupInCurrentPage"
|
|
|
+ :label="t('mainPlan.MaintItemsGroup')"
|
|
|
+ align="center"
|
|
|
+ prop="group"
|
|
|
+ fixed="left"
|
|
|
+ :width="columnWidths.group"
|
|
|
+ :cell-class-name="groupCellClassName"
|
|
|
+ >
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="full-content-cell">
|
|
|
+ {{ row.group }}
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column :label="t('mainPlan.MaintItems')" align="center" prop="name"
|
|
|
+ :show-overflow-tooltip="false" :width="columnWidths.name" fixed="left">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="full-content-cell">
|
|
|
+ <!-- 保养项 只显示'->'后的内容 -->
|
|
|
+ {{ formatMaintItemName(row.name) }}
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <!-- 消耗物料规则列 -->
|
|
|
+ <el-table-column :label="t('mainPlan.consumeMaterials')" align="center" width="80">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-switch
|
|
|
+ v-model="scope.row.rule"
|
|
|
+ :active-value="0"
|
|
|
+ :inactive-value="1"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column :label="t('main.mileage')" key="mileageRule" align="center"
|
|
|
+ :width="columnWidths.mileageRule" v-if="false">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-switch
|
|
|
+ v-model="scope.row.mileageRule"
|
|
|
+ :active-value="0"
|
|
|
+ :inactive-value="1"
|
|
|
+ :disabled="true"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column :label="t('main.runTime')" key="runningTimeRule" align="center"
|
|
|
+ :width="columnWidths.runningTimeRule" v-if="false">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-switch
|
|
|
+ v-model="scope.row.runningTimeRule"
|
|
|
+ :active-value="0"
|
|
|
+ :inactive-value="1"
|
|
|
+ :disabled="true"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column :label="t('main.date')" key="naturalDateRule" align="center"
|
|
|
+ :width="columnWidths.naturalDateRule" v-if="false">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-switch
|
|
|
+ v-model="scope.row.naturalDateRule"
|
|
|
+ :active-value="0"
|
|
|
+ :inactive-value="1"
|
|
|
+ :disabled="true"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column :label="t('operationFillForm.sumTime')" align="center" prop="totalRunTime" v-if="hasTimeRuleInCurrentPage"
|
|
|
+ :formatter="erpPriceTableColumnFormatter" :width="columnWidths.totalRunTime">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.totalRunTime ?? row.tempTotalRunTime }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column :label="t('operationFillForm.sumKil')" align="center" prop="totalMileage" v-if="hasMileageRuleInCurrentPage"
|
|
|
+ :formatter="erpPriceTableColumnFormatter" :width="columnWidths.totalMileage">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.totalMileage ?? row.tempTotalMileage }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column :label="t('mainPlan.lastMaintenanceDate')" prop="lastMaintenanceDate" :width="columnWidths.lastMaintenanceDate">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="full-content-cell">
|
|
|
+ {{ row.lastMaintenanceDate }}
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <!-- 保养里程 分组 -->
|
|
|
+ <el-table-column v-if="hasMileageRuleInCurrentPage" label="保养里程" align="center">
|
|
|
+ <el-table-column :label="t('mainPlan.lastMaintenanceMileage')" align="center" prop="lastRunningKilometers"
|
|
|
+ :formatter="erpPriceTableColumnFormatter" :width="columnWidths.lastRunningKilometers">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.lastRunningKilometers }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column :label="t('mainPlan.nextMaintenanceKm')"
|
|
|
+ align="center" prop="nextMaintenanceKm" :width="columnWidths.nextMaintenanceKm">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.nextMaintenanceKm ?? '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column :label="t('mainPlan.remainKm')"
|
|
|
+ align="center" prop="remainKm" :width="columnWidths.remainKm">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.remainKm != null ? row.remainKm.toFixed(2) : '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <!-- 保养时长 分组 -->
|
|
|
+ <el-table-column v-if="hasTimeRuleInCurrentPage" label="保养时长" align="center">
|
|
|
+ <el-table-column :label="t('mainPlan.lastMaintenanceOperationTime')" align="center" prop="lastRunningTime"
|
|
|
+ :formatter="erpPriceTableColumnFormatter" :width="columnWidths.lastRunningTime">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.lastRunningTime }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column :label="t('mainPlan.nextMaintenanceH')"
|
|
|
+ align="center" prop="nextMaintenanceH" :width="columnWidths.nextMaintenanceH">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.nextMaintenanceH ?? '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column :label="t('mainPlan.remainH')"
|
|
|
+ align="center" prop="remainH" :width="columnWidths.remainH">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.remainH != null ? row.remainH.toFixed(2) : '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column
|
|
|
+ v-if="shouldShowRunningTimeColumn"
|
|
|
+ :label="t('mainPlan.RunTimeCycle')"
|
|
|
+ align="center"
|
|
|
+ prop="nextRunningTime"
|
|
|
+ :width="columnWidths.nextRunningTime"
|
|
|
+ >
|
|
|
+ <template #default="scope">
|
|
|
+ <el-input-number
|
|
|
+ v-if="scope.row.runningTimeRule === 0"
|
|
|
+ v-model="scope.row.nextRunningTime"
|
|
|
+ :precision="1"
|
|
|
+ :min="1"
|
|
|
+ :controls="false"
|
|
|
+ style="width: 100%"
|
|
|
+ @change="validateRunningTime(scope.row)"
|
|
|
+ />
|
|
|
+ <span v-else>-</span>
|
|
|
+ <!-- 错误提示 -->
|
|
|
+ <div v-if="scope.row.timeError" class="error-text">
|
|
|
+ {{ scope.row.timeError }}
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <!-- 保养日期 分组 -->
|
|
|
+ <el-table-column v-if="hasDateRuleInCurrentPage" label="保养日期" align="center">
|
|
|
+ <el-table-column :label="t('mainPlan.lastMaintenanceNaturalDate')" align="center" prop="tempLastNaturalDate"
|
|
|
+ :formatter="erpPriceTableColumnFormatter" :width="columnWidths.tempLastNaturalDate">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.tempLastNaturalDate }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column :label="t('mainPlan.nextMaintDate')"
|
|
|
+ align="center" prop="nextMaintenanceDate" :width="columnWidths.nextMaintenanceDate">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.nextMaintenanceDate ?? '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column :label="t('mainPlan.remainDay')"
|
|
|
+ align="center" prop="remainDay" :width="columnWidths.remainDay">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.remainDay ?? '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column :label="t('common.status')" align="center" width="100">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ getStatusText(scope.row) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <!--
|
|
|
+ <el-table-column :label="t('iotMaintain.numberOfMaterials')" align="center" width="90">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ getMaterialCount(scope.row.bomNodeId) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column> -->
|
|
|
+
|
|
|
+ <el-table-column :label="t('iotMaintain.operation')" align="center" prop="operation" :width="columnWidths.operation" fixed="right">
|
|
|
+ <template #default="scope">
|
|
|
+ <div class="horizontal-actions">
|
|
|
+ <!-- 查看当前保养项已经绑定的物料列表
|
|
|
+ <el-tooltip
|
|
|
+ v-if="scope.row.deviceBomMaterials && scope.row.deviceBomMaterials.length > 0"
|
|
|
+ effect="dark"
|
|
|
+ :content="t('mainPlan.deviceBomMaterials')"
|
|
|
+ placement="top"
|
|
|
+ :show-after="300"
|
|
|
+ >
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ link
|
|
|
+ :icon="View"
|
|
|
+ @click="handleDropdownCommand({ action: 'deviceBomMaterials', row: scope.row })"
|
|
|
+ class="action-button"
|
|
|
+ />
|
|
|
+ </el-tooltip> -->
|
|
|
+
|
|
|
+ <!-- 延迟保养按钮 -->
|
|
|
+ <el-tooltip
|
|
|
+ v-if="scope.row.status === 0"
|
|
|
+ effect="dark"
|
|
|
+ :content="t('stock.DelayMaintenance')"
|
|
|
+ placement="top"
|
|
|
+ :show-after="300"
|
|
|
+ >
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ link
|
|
|
+ :icon="Clock"
|
|
|
+ @click="handleDropdownCommand({ action: 'delay', row: scope.row })"
|
|
|
+ class="action-button"
|
|
|
+ />
|
|
|
+ </el-tooltip>
|
|
|
+
|
|
|
+ <!-- 选择物料按钮
|
|
|
+ <el-tooltip
|
|
|
+ v-if="scope.row.status === 0"
|
|
|
+ effect="dark"
|
|
|
+ :content="t('stock.selectMaterial')"
|
|
|
+ placement="top"
|
|
|
+ :show-after="300"
|
|
|
+ >
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ link
|
|
|
+ :icon="Box"
|
|
|
+ @click="handleDropdownCommand({ action: 'material', row: scope.row })"
|
|
|
+ class="action-button"
|
|
|
+ />
|
|
|
+ </el-tooltip> -->
|
|
|
+
|
|
|
+ <!-- 物料详情按钮
|
|
|
+ <el-tooltip
|
|
|
+ effect="dark"
|
|
|
+ :content="t('bomList.materialDetail')"
|
|
|
+ placement="top"
|
|
|
+ :show-after="300"
|
|
|
+ >
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ link
|
|
|
+ :icon="Document"
|
|
|
+ @click="handleDropdownCommand({ action: 'detail', row: scope.row })"
|
|
|
+ class="action-button"
|
|
|
+ />
|
|
|
+ </el-tooltip> -->
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <div style="margin-top: 20px; display: flex; justify-content: flex-end;">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="currentPage"
|
|
|
+ v-model:page-size="pageSize"
|
|
|
+ :page-sizes="[10]"
|
|
|
+ :background="true"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ :total="list.length"
|
|
|
+ @current-change="handleCurrentChange"
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </ContentWrap>
|
|
|
+
|
|
|
+ <!-- 选择的物料列表 -->
|
|
|
+ <ContentWrap>
|
|
|
+
|
|
|
+ <div style="margin-bottom: 15px; display: flex; justify-content: flex-start; align-items: center; gap: 12px;">
|
|
|
+ <!-- 2. 新增“选择物料”按钮,与现有按钮平齐 -->
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ :icon="Box"
|
|
|
+ @click="handleSelectMaterial"
|
|
|
+ :disabled="!currentBomItem"
|
|
|
+ >
|
|
|
+ {{ t('stock.selectMaterial') }}
|
|
|
+ </el-button>
|
|
|
+ <!-- 原有“显示所有物料/当前保养项物料”按钮 -->
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ @click="toggleShowAllMaterials"
|
|
|
+ :icon="showAllMaterials ? Star : StarFilled"
|
|
|
+ >
|
|
|
+ {{ showAllMaterials ? '显示当前保养项物料' : '显示所有物料' }}
|
|
|
+ </el-button>
|
|
|
+ <!-- 新增物料按钮 -->
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ :icon="Plus"
|
|
|
+ @click="handleAddMaterial"
|
|
|
+ :disabled="!currentBomItem"
|
|
|
+ >
|
|
|
+ 新增物料
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-table v-loading="false" :data="displayedMaterialList" :stripe="true" :show-overflow-tooltip="true"
|
|
|
+ v-if="showMaterialTable" :row-class-name="getMaterialRowClassName">
|
|
|
+ <el-table-column :label="t('iotDevice.serial')" align="center" width="50px">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ scope.$index + 1 }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="" align="center" prop="deviceId" v-if="false"/>
|
|
|
+ <el-table-column :label="t('bomList.bomNode')" align="center" prop="bomNodeId" v-if="false"/>
|
|
|
+ <el-table-column label="工厂id" align="center" prop="factoryId" v-if="false"/>
|
|
|
+ <el-table-column label="工厂名称" align="center" prop="factory"/>
|
|
|
+ <el-table-column label="成本中心id" align="center" prop="costCenterId" v-if="false"/>
|
|
|
+ <el-table-column label="成本中心名称" align="center" prop="costCenter"/>
|
|
|
+ <el-table-column label="库存地点id" align="center" prop="storageLocationId" v-if="false"/>
|
|
|
+ <el-table-column label="库存地点名称" align="center" prop="projectDepartment"/>
|
|
|
+ <el-table-column label="物料编码" align="center" prop="materialCode" >
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input
|
|
|
+ v-if="row.isNew"
|
|
|
+ v-model="row.materialCode"
|
|
|
+ placeholder="请输入物料编码"
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ <span v-else>{{ row.materialCode }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="物料名称" align="center" prop="materialName" >
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input
|
|
|
+ v-if="row.isNew"
|
|
|
+ v-model="row.materialName"
|
|
|
+ placeholder="请输入物料名称"
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ <span v-else>{{ row.materialName }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="单位" align="center" prop="unit" width="60px">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input
|
|
|
+ v-if="row.isNew"
|
|
|
+ v-model="row.unit"
|
|
|
+ placeholder="单位"
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ <span v-else>{{ row.unit }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="单价(CNY/元)" align="center" prop="unitPrice" :formatter="erpPriceTableColumnFormatter" width="120px">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="unit-price-container">
|
|
|
+ <el-input-number
|
|
|
+ v-if="row.isNew || !row.unitPrice || row.unitPrice === 0"
|
|
|
+ v-model="row.unitPrice"
|
|
|
+ :precision="2"
|
|
|
+ :min="0"
|
|
|
+ :controls="false"
|
|
|
+ style="width: 100%"
|
|
|
+ class="unit-price-input"
|
|
|
+ :disabled="isMaterialDisabled(row)"
|
|
|
+ />
|
|
|
+ <span v-else class="unit-price-text">{{ row.unitPrice }}</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="消耗数量" align="center" prop="quantity" width="120px">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input-number
|
|
|
+ v-model="row.quantity"
|
|
|
+ :precision="4"
|
|
|
+ :min="0"
|
|
|
+ :controls="false"
|
|
|
+ style="width: 100%"
|
|
|
+ :disabled="isMaterialDisabled(row)"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="库存数量" align="center" prop="totalInventoryQuantity" width="80px"/>
|
|
|
+ <el-table-column label="来源" align="center" prop="materialSource" width="100px"/>
|
|
|
+ <el-table-column label="操作" align="center" width="80" fixed="right">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-tooltip
|
|
|
+ effect="dark"
|
|
|
+ content="删除物料"
|
|
|
+ placement="top"
|
|
|
+ :show-after="300"
|
|
|
+ >
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ link
|
|
|
+ :icon="Delete"
|
|
|
+ @click="handleDeleteMaterialInTable(scope.row)"
|
|
|
+ :disabled="isMaterialDisabled(scope.row)"
|
|
|
+ class="action-button"
|
|
|
+ />
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </ContentWrap>
|
|
|
+
|
|
|
+ </ContentWrap>
|
|
|
+
|
|
|
+ <ContentWrap>
|
|
|
+ <el-form>
|
|
|
+ <el-form-item style="float: right">
|
|
|
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">{{t('common.save')}}</el-button>
|
|
|
+ <el-button @click="close">{{t('common.cancel')}}</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </ContentWrap>
|
|
|
+
|
|
|
+ <!-- 新增配置对话框 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="configDialog.visible"
|
|
|
+ :title="`设备 ${configDialog.current?.deviceCode+'-'+configDialog.current?.name} 保养配置`"
|
|
|
+ width="600px"
|
|
|
+ >
|
|
|
+ <!-- 使用header插槽自定义标题 -->
|
|
|
+ <template #header>
|
|
|
+ <span>设备 <strong>{{ configDialog.current?.deviceCode }}-{{ configDialog.current?.name }}</strong> 保养项配置</span>
|
|
|
+ </template>
|
|
|
+ <el-form :model="configDialog.form" label-width="200px" :rules="configFormRules" ref="configFormRef">
|
|
|
+ <div class="form-group">
|
|
|
+ <div class="group-title">{{ t('mainPlan.basicMaintenanceRecords') }}</div>
|
|
|
+ <!-- 里程配置 -->
|
|
|
+ <el-form-item
|
|
|
+ v-if="configDialog.current?.mileageRule === 0"
|
|
|
+ :label="t('mainPlan.lastMaintenanceMileage')"
|
|
|
+ prop="lastRunningKilometers"
|
|
|
+ >
|
|
|
+ <el-input-number
|
|
|
+ v-model="configDialog.form.lastRunningKilometers"
|
|
|
+ :precision="2"
|
|
|
+ :min="0"
|
|
|
+ controls-position="right"
|
|
|
+ :controls="false"
|
|
|
+ style="width: 60%"
|
|
|
+ :disabled="true"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <!-- 推迟公里数 -->
|
|
|
+ <el-form-item
|
|
|
+ v-if="configDialog.current?.mileageRule === 0"
|
|
|
+ :label="t('mainPlan.DelayKil')"
|
|
|
+ prop="delayKilometers"
|
|
|
+ >
|
|
|
+ <el-input-number
|
|
|
+ v-model="configDialog.form.delayKilometers"
|
|
|
+ :precision="2"
|
|
|
+ :min="0"
|
|
|
+ controls-position="right"
|
|
|
+ :controls="false"
|
|
|
+ style="width: 60%"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <!-- 运行时间配置 -->
|
|
|
+ <el-form-item
|
|
|
+ v-if="configDialog.current?.runningTimeRule === 0"
|
|
|
+ :label="t('mainPlan.lastMaintenanceOperationTime')"
|
|
|
+ prop="lastRunningTime"
|
|
|
+ >
|
|
|
+ <el-input-number
|
|
|
+ v-model="configDialog.form.lastRunningTime"
|
|
|
+ :precision="1"
|
|
|
+ :min="0"
|
|
|
+ controls-position="right"
|
|
|
+ :controls="false"
|
|
|
+ style="width: 60%"
|
|
|
+ :disabled="true"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <!-- 推迟时长 -->
|
|
|
+ <el-form-item
|
|
|
+ v-if="configDialog.current?.runningTimeRule === 0"
|
|
|
+ :label="t('mainPlan.DelayDuration')"
|
|
|
+ prop="delayDuration"
|
|
|
+ >
|
|
|
+ <el-input-number
|
|
|
+ v-model="configDialog.form.delayDuration"
|
|
|
+ :precision="2"
|
|
|
+ :min="0"
|
|
|
+ controls-position="right"
|
|
|
+ :controls="false"
|
|
|
+ style="width: 60%"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <!-- 自然日期配置 -->
|
|
|
+ <el-form-item
|
|
|
+ v-if="configDialog.current?.naturalDateRule === 0"
|
|
|
+ :label="t('mainPlan.lastMaintenanceNaturalDate')"
|
|
|
+ prop="lastNaturalDate"
|
|
|
+ >
|
|
|
+ <el-date-picker
|
|
|
+ v-model="configDialog.form.lastNaturalDate"
|
|
|
+ type="date"
|
|
|
+ placeholder="选择日期"
|
|
|
+ format="YYYY-MM-DD"
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
+ style="width: 60%"
|
|
|
+ :disabled="true"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <!-- 推迟自然日期 -->
|
|
|
+ <el-form-item
|
|
|
+ v-if="configDialog.current?.naturalDateRule === 0"
|
|
|
+ :label="t('mainPlan.DelayDate')"
|
|
|
+ prop="delayNaturalDate"
|
|
|
+ >
|
|
|
+ <el-input-number
|
|
|
+ v-model="configDialog.form.delayNaturalDate"
|
|
|
+ :precision="2"
|
|
|
+ :min="0"
|
|
|
+ controls-position="right"
|
|
|
+ :controls="false"
|
|
|
+ style="width: 60%"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item
|
|
|
+ :label="t('stock.DelayReason')"
|
|
|
+ prop="delayReason"
|
|
|
+ v-if="configDialog.current?.mileageRule === 0 ||
|
|
|
+ configDialog.current?.runningTimeRule === 0 ||
|
|
|
+ configDialog.current?.naturalDateRule === 0"
|
|
|
+ >
|
|
|
+ <el-input
|
|
|
+ v-model="configDialog.form.delayReason"
|
|
|
+ type="textarea"
|
|
|
+ :rows="2"
|
|
|
+ :placeholder="t('stock.DelayReason')"
|
|
|
+ style="width: 60%"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group" v-if="configDialog.current?.mileageRule === 0">
|
|
|
+ <div class="group-title">{{ t('mainPlan.operatingMileageRuleConfiguration') }}</div>
|
|
|
+ <!-- 保养规则周期值 + 提前量 -->
|
|
|
+ <el-form-item
|
|
|
+ v-if="configDialog.current?.mileageRule === 0"
|
|
|
+ :label="t('mainPlan.operatingMileageCycle')"
|
|
|
+ prop="nextRunningKilometers"
|
|
|
+ >
|
|
|
+ <el-input-number
|
|
|
+ v-model="configDialog.form.nextRunningKilometers"
|
|
|
+ :precision="2"
|
|
|
+ :min="0"
|
|
|
+ controls-position="right"
|
|
|
+ :controls="false"
|
|
|
+ style="width: 60%"
|
|
|
+ :disabled="true"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item
|
|
|
+ v-if="configDialog.current?.mileageRule === 0"
|
|
|
+ :label="t('mainPlan.OperatingMileageCycle_lead')"
|
|
|
+ prop="kiloCycleLead"
|
|
|
+ >
|
|
|
+ <el-input-number
|
|
|
+ v-model="configDialog.form.kiloCycleLead"
|
|
|
+ :precision="2"
|
|
|
+ :min="0"
|
|
|
+ controls-position="right"
|
|
|
+ :controls="false"
|
|
|
+ style="width: 60%"
|
|
|
+ :disabled="true"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group" v-if="configDialog.current?.runningTimeRule === 0">
|
|
|
+ <div class="group-title">{{ t('mainPlan.RunTimeRuleConfiguration') }}</div>
|
|
|
+ <el-form-item
|
|
|
+ v-if="configDialog.current?.runningTimeRule === 0"
|
|
|
+ :label="t('mainPlan.RunTimeCycle')"
|
|
|
+ prop="nextRunningTime"
|
|
|
+ >
|
|
|
+ <el-input-number
|
|
|
+ v-model="configDialog.form.nextRunningTime"
|
|
|
+ :precision="1"
|
|
|
+ :min="0"
|
|
|
+ controls-position="right"
|
|
|
+ :controls="false"
|
|
|
+ style="width: 60%"
|
|
|
+ :disabled="true"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item
|
|
|
+ v-if="configDialog.current?.runningTimeRule === 0"
|
|
|
+ :label="t('mainPlan.RunTimeCycle_Lead')"
|
|
|
+ prop="timePeriodLead"
|
|
|
+ >
|
|
|
+ <el-input-number
|
|
|
+ v-model="configDialog.form.timePeriodLead"
|
|
|
+ :precision="1"
|
|
|
+ :min="0"
|
|
|
+ controls-position="right"
|
|
|
+ :controls="false"
|
|
|
+ style="width: 60%"
|
|
|
+ :disabled="true"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group" v-if="configDialog.current?.naturalDateRule === 0">
|
|
|
+ <div class="group-title">{{ t('mainPlan.NaturalDayRuleConfig') }}</div>
|
|
|
+ <el-form-item
|
|
|
+ v-if="configDialog.current?.naturalDateRule === 0"
|
|
|
+ :label="t('mainPlan.NaturalDailyCycle') "
|
|
|
+ prop="nextNaturalDate"
|
|
|
+ >
|
|
|
+ <el-input-number
|
|
|
+ v-model="configDialog.form.nextNaturalDate"
|
|
|
+ :min="0"
|
|
|
+ controls-position="right"
|
|
|
+ :controls="false"
|
|
|
+ style="width: 60%"
|
|
|
+ :disabled="true"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item
|
|
|
+ v-if="configDialog.current?.naturalDateRule === 0"
|
|
|
+ :label="t('mainPlan.NaturalDailyCycle_Lead') "
|
|
|
+ prop="naturalDatePeriodLead"
|
|
|
+ >
|
|
|
+ <el-input-number
|
|
|
+ v-model="configDialog.form.naturalDatePeriodLead"
|
|
|
+ :min="0"
|
|
|
+ controls-position="right"
|
|
|
+ :controls="false"
|
|
|
+ style="width: 60%"
|
|
|
+ :disabled="true"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="configDialog.visible = false">{{ t('common.cancel')}}</el-button>
|
|
|
+ <el-button type="primary" @click="saveConfig">{{ t('common.ok')}}</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ <!-- 表单弹窗:添加/修改 -->
|
|
|
+ <WorkOrderMaterial ref="materialFormRef" @choose="selectChoose" />
|
|
|
+ <!-- 设备BOM节点绑定的物料列表 -->
|
|
|
+ <DeviceBomMaterials ref="deviceBomMaterialsRef" />
|
|
|
+ <!-- 抽屉组件 展示已经选择的物料 并编辑物料消耗 -->
|
|
|
+ <MaterialListDrawer
|
|
|
+ :model-value="drawerVisible"
|
|
|
+ @update:model-value="val => drawerVisible = val"
|
|
|
+ :node-id="currentBomNodeId"
|
|
|
+ :materials="materialList.filter(item => item.bomNodeId === currentBomNodeId)"
|
|
|
+ @delete="handleDeleteMaterial"
|
|
|
+ :hide-extra-columns="hideExtraColumnsInDrawer"
|
|
|
+ />
|
|
|
+</template>
|
|
|
+<script setup lang="ts">
|
|
|
+import * as UserApi from '@/api/system/user'
|
|
|
+import { useUserStore } from '@/store/modules/user'
|
|
|
+import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
|
|
+import { useRouter, useRoute } from 'vue-router'
|
|
|
+import { IotMaintenanceBomApi, IotMaintenanceBomVO } from '@/api/pms/iotmaintenancebom'
|
|
|
+import { IotMainWorkOrderBomApi, IotMainWorkOrderBomVO } from '@/api/pms/iotmainworkorderbom'
|
|
|
+import { IotMainWorkOrderBomMaterialApi, IotMainWorkOrderBomMaterialVO } from '@/api/pms/iotmainworkorderbommaterial'
|
|
|
+import { IotMainWorkOrderApi, IotMainWorkOrderVO } from '@/api/pms/iotmainworkorder'
|
|
|
+import { useTagsViewStore } from '@/store/modules/tagsView'
|
|
|
+import * as DeptApi from "@/api/system/dept";
|
|
|
+import {erpPriceTableColumnFormatter} from "@/utils";
|
|
|
+import dayjs from 'dayjs'
|
|
|
+import MaterialListDrawer from "@/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue";
|
|
|
+import WorkOrderMaterial from "@/views/pms/iotmainworkorder/WorkOrderMaterial.vue";
|
|
|
+import DeviceBomMaterials from "@/views/pms/iotmainworkorder/DeviceBomMaterials.vue";
|
|
|
+import {DICT_TYPE, getIntDictOptions} from "@/utils/dict";
|
|
|
+// 引入图标
|
|
|
+import { View, Clock, Box, Document, Star, StarFilled, Delete, Plus } from '@element-plus/icons-vue';
|
|
|
+
|
|
|
+/** 保养工单 表单 */
|
|
|
+defineOptions({ name: 'IotMainWorkOrderOptimize' })
|
|
|
+
|
|
|
+const { t } = useI18n() // 国际化
|
|
|
+const message = useMessage() // 消息弹窗
|
|
|
+const { delView } = useTagsViewStore() // 视图操作
|
|
|
+const { currentRoute, push } = useRouter()
|
|
|
+const deptUsers = ref<UserApi.UserVO[]>([]) // 用户列表
|
|
|
+const dept = ref() // 当前登录人所属部门对象
|
|
|
+const configFormRef = ref() // 配置弹出框对象
|
|
|
+const bomNodeId = ref() // 最新的bomNodeId
|
|
|
+const dialogTitle = ref('') // 弹窗的标题
|
|
|
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
|
|
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
|
|
+const deviceLabel = ref('') // 表单的类型:create - 新增;update - 修改
|
|
|
+const drawerVisible = ref<boolean>(false)
|
|
|
+const currentBomNodeId = ref() // 当前选中的bom节点
|
|
|
+const showDrawer = ref()
|
|
|
+const list = ref<IotMainWorkOrderBomVO[]>([]) // 保养工单bom关联列表的数据
|
|
|
+const materialList = ref<IotMainWorkOrderBomMaterialVO[]>([]) // 保养工单bom关联物料列表
|
|
|
+const deviceIds = ref<number[]>([]) // 已经选择的设备id数组
|
|
|
+const { params, name } = useRoute() // 查询参数
|
|
|
+const id = params.id
|
|
|
+// 控制抽屉额外列的显示
|
|
|
+const hideExtraColumnsInDrawer = ref(false)
|
|
|
+
|
|
|
+// 分页相关变量
|
|
|
+const currentPage = ref(1)
|
|
|
+const pageSize = ref(10)
|
|
|
+
|
|
|
+const tableRef = ref();
|
|
|
+
|
|
|
+// 新增响应式变量
|
|
|
+const maintItemsWidth = ref('auto')
|
|
|
+
|
|
|
+const lastNaturalDateWatchers = ref(new Map())
|
|
|
+
|
|
|
+const columnWidths = ref<Record<string, string>>({});
|
|
|
+
|
|
|
+// 物料列表相关变量
|
|
|
+const showMaterialTable = ref(true) // 控制物料列表显示
|
|
|
+const currentBomItem = ref(null) // 当前选中的保养项
|
|
|
+const bomMaterialsMap = ref({}) // 存储所有保养项物料列表的映射
|
|
|
+const showAllMaterials = ref(false) // 控制是否显示所有物料
|
|
|
+
|
|
|
+// 计算当前页是否有分组数据
|
|
|
+const hasGroupInCurrentPage = computed(() => {
|
|
|
+ return paginatedList.value.some(item => item.group && item.group !== '');
|
|
|
+});
|
|
|
+
|
|
|
+// 计算属性:根据showAllMaterials决定显示的物料列表
|
|
|
+const displayedMaterialList = computed(() => {
|
|
|
+ if (showAllMaterials.value) {
|
|
|
+ // 显示所有物料
|
|
|
+ const allMaterials = [];
|
|
|
+ for (const key in bomMaterialsMap.value) {
|
|
|
+ allMaterials.push(...bomMaterialsMap.value[key]);
|
|
|
+ }
|
|
|
+ return allMaterials;
|
|
|
+ } else {
|
|
|
+ // 显示当前选中保养项的物料
|
|
|
+ return materialList.value;
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// 判断物料是否被禁用
|
|
|
+const isMaterialDisabled = (material) => {
|
|
|
+ // 根据物料的bomNodeId找到对应的保养项
|
|
|
+ const bomItem = list.value.find(item => item.bomNodeId === material.bomNodeId);
|
|
|
+ // 如果保养项存在且rule=1(不消耗物料),则禁用
|
|
|
+ return bomItem && bomItem.rule === 1;
|
|
|
+};
|
|
|
+
|
|
|
+// 物料表格行类名 - 用于设置灰色背景
|
|
|
+const getMaterialRowClassName = ({ row }) => {
|
|
|
+ if (row.isNew) {
|
|
|
+ return 'new-material-row';
|
|
|
+ }
|
|
|
+ if (isMaterialDisabled(row)) {
|
|
|
+ return 'disabled-material-row';
|
|
|
+ }
|
|
|
+ return '';
|
|
|
+};
|
|
|
+
|
|
|
+// 切换显示所有物料/当前保养项物料
|
|
|
+const toggleShowAllMaterials = () => {
|
|
|
+ showAllMaterials.value = !showAllMaterials.value;
|
|
|
+
|
|
|
+ // 切换显示模式时,需要同步更新 materialList
|
|
|
+ if (!showAllMaterials.value && currentBomItem.value) {
|
|
|
+ // 切换到显示当前保养项物料
|
|
|
+ const uniqueKey = `${currentBomItem.value.deviceId}${currentBomItem.value.bomNodeId}`;
|
|
|
+ materialList.value = bomMaterialsMap.value[uniqueKey] || [];
|
|
|
+ } else if (showAllMaterials.value) {
|
|
|
+ // 切换到显示所有物料
|
|
|
+ const allMaterials = [];
|
|
|
+ for (const key in bomMaterialsMap.value) {
|
|
|
+ allMaterials.push(...bomMaterialsMap.value[key]);
|
|
|
+ }
|
|
|
+ materialList.value = allMaterials;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 为表格行添加类名,实现高亮效果
|
|
|
+const tableRowClassName = ({ row }) => {
|
|
|
+ return row.isSelected ? 'highlight-row' : '';
|
|
|
+};
|
|
|
+
|
|
|
+// 分组合并计算逻辑
|
|
|
+const groupSpans = ref<Record<string, { span: number, index: number }>>({})
|
|
|
+
|
|
|
+const formData = ref({
|
|
|
+ id: undefined,
|
|
|
+ deptId: undefined,
|
|
|
+ name: '',
|
|
|
+ orderNumber: undefined,
|
|
|
+ responsiblePerson: undefined,
|
|
|
+ actualStartTime: undefined,
|
|
|
+ actualEndTime: undefined,
|
|
|
+ cost: undefined,
|
|
|
+ otherCost: undefined,
|
|
|
+ outsourcingFlag: 0,
|
|
|
+ remark: undefined,
|
|
|
+ status: undefined,
|
|
|
+ devicePersons: '',
|
|
|
+ deviceCode: '',
|
|
|
+ deviceName: ''
|
|
|
+})
|
|
|
+
|
|
|
+const formRules = reactive({
|
|
|
+ name: [{ required: true, message: '工单名称不能为空', trigger: 'blur' }],
|
|
|
+ actualStartTime: [{
|
|
|
+ required: true,
|
|
|
+ message: t('fault.start') + '不能为空',
|
|
|
+ trigger: 'change'
|
|
|
+ }, {
|
|
|
+ validator: (rule, value, callback) => {
|
|
|
+ const now = dayjs();
|
|
|
+ const start = value ? dayjs(Number(value)) : null;
|
|
|
+
|
|
|
+ // 验证开始时间 <= 当前日期
|
|
|
+ if (start && start.isAfter(now)) {
|
|
|
+ callback(new Error(t('fault.start') + '不能超过当前日期'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const end = formData.value.actualEndTime
|
|
|
+ ? dayjs(Number(formData.value.actualEndTime))
|
|
|
+ : null;
|
|
|
+
|
|
|
+ // 只有当结束时间有效时,才比较开始和结束时间
|
|
|
+ if (end && end.isValid() && !end.isAfter(now)) {
|
|
|
+ if (start && start.isAfter(end)) {
|
|
|
+ callback(new Error(t('fault.start') + '不能超过' + t('fault.end')));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 触发结束时间的重新校验
|
|
|
+ if (formRef.value) {
|
|
|
+ formRef.value.validateField('actualEndTime', () => {});
|
|
|
+ }
|
|
|
+
|
|
|
+ callback();
|
|
|
+ },
|
|
|
+ trigger: 'change'
|
|
|
+ }],
|
|
|
+ actualEndTime: [{
|
|
|
+ required: true,
|
|
|
+ message: t('fault.end') + '不能为空',
|
|
|
+ trigger: 'change'
|
|
|
+ }, {
|
|
|
+ validator: (rule, value, callback) => {
|
|
|
+ const now = dayjs();
|
|
|
+ const end = value ? dayjs(Number(value)) : null;
|
|
|
+
|
|
|
+ // 验证结束时间 <= 当前日期
|
|
|
+ if (end && end.isAfter(now)) {
|
|
|
+ callback(new Error(t('fault.end') + '不能超过当前日期'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const start = formData.value.actualStartTime
|
|
|
+ ? dayjs(Number(formData.value.actualStartTime))
|
|
|
+ : null;
|
|
|
+
|
|
|
+ // 验证结束时间 >= 开始时间(仅当开始时间存在时)
|
|
|
+ if (start && end && end.isBefore(start)) {
|
|
|
+ callback(new Error(t('fault.end') + '必须大于等于' + t('fault.start')));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ callback();
|
|
|
+ },
|
|
|
+ trigger: 'change'
|
|
|
+ }]
|
|
|
+})
|
|
|
+
|
|
|
+const formRef = ref() // 表单 Ref
|
|
|
+
|
|
|
+interface MaterialFormExpose {
|
|
|
+ open: (deptId: number, bomNodeId: number, row: any, type: string) => void
|
|
|
+}
|
|
|
+
|
|
|
+const materialFormRef = ref<MaterialFormExpose>();
|
|
|
+
|
|
|
+const deviceBomMaterialsRef = ref<MaterialFormExpose>();
|
|
|
+
|
|
|
+// 新增配置相关状态
|
|
|
+const configDialog = reactive({
|
|
|
+ visible: false,
|
|
|
+ current: null as IotMaintenanceBomVO | null,
|
|
|
+ form: {
|
|
|
+ lastRunningKilometers: 0,
|
|
|
+ delayKilometers: 0,
|
|
|
+ lastRunningTime: 0,
|
|
|
+ delayDuration: 0,
|
|
|
+ lastNaturalDate: '',
|
|
|
+ delayNaturalDate: 0,
|
|
|
+ // 保养规则 周期
|
|
|
+ nextRunningKilometers: 0,
|
|
|
+ nextRunningTime: 0,
|
|
|
+ nextNaturalDate: 0,
|
|
|
+ // 提前量
|
|
|
+ kiloCycleLead: 0,
|
|
|
+ timePeriodLead: 0,
|
|
|
+ naturalDatePeriodLead: 0,
|
|
|
+ // 推迟原因
|
|
|
+ delayReason: ''
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 打开配置对话框
|
|
|
+const openConfigDialog = (row: IotMainWorkOrderBomVO) => {
|
|
|
+ configDialog.current = row
|
|
|
+
|
|
|
+ // 处理日期初始化(核心修改)
|
|
|
+ let initialDate = ''
|
|
|
+ if (row.lastNaturalDate) {
|
|
|
+ // 如果已有值:时间戳 -> 日期字符串
|
|
|
+ initialDate = dayjs(row.lastNaturalDate).format('YYYY-MM-DD')
|
|
|
+ } else {
|
|
|
+ // 如果无值:设置默认值避免1970问题
|
|
|
+ initialDate = ''
|
|
|
+ }
|
|
|
+
|
|
|
+ configDialog.form = {
|
|
|
+ lastRunningKilometers: row.lastRunningKilometers || 0,
|
|
|
+ delayKilometers: row.delayKilometers || 0,
|
|
|
+ lastRunningTime: row.lastRunningTime || 0,
|
|
|
+ delayDuration: row.delayDuration || 0,
|
|
|
+ lastNaturalDate: initialDate,
|
|
|
+ delayNaturalDate: row.delayNaturalDate || 0,
|
|
|
+ // 保养规则 周期值
|
|
|
+ nextRunningKilometers: row.nextRunningKilometers || 0,
|
|
|
+ nextRunningTime: row.nextRunningTime || 0,
|
|
|
+ nextNaturalDate: row.nextNaturalDate || 0,
|
|
|
+ // 提前量
|
|
|
+ kiloCycleLead: row.kiloCycleLead || 0,
|
|
|
+ timePeriodLead: row.timePeriodLead || 0,
|
|
|
+ naturalDatePeriodLead: row.naturalDatePeriodLead || 0,
|
|
|
+ // 推迟原因
|
|
|
+ delayReason: row.delayReason || ''
|
|
|
+ }
|
|
|
+ configDialog.visible = true
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化 保养项名称 方法 只显示 -> 后面的内容
|
|
|
+const formatMaintItemName = (name: string) => {
|
|
|
+ if (!name) return '';
|
|
|
+
|
|
|
+ // 包含'->'时只取后半部分
|
|
|
+ if (name.includes('->')) {
|
|
|
+ return name.split('->').pop()?.trim() || name;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 不含'->'时显示完整内容
|
|
|
+ return name;
|
|
|
+};
|
|
|
+
|
|
|
+// 行合并方法 (优化后符合图片效果)
|
|
|
+const handleSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
|
|
|
+ // 只处理保养项组列
|
|
|
+ if (column.property === 'group' && row.group) {
|
|
|
+ const groupKey = `${currentPage.value}-${row.group}`;
|
|
|
+ const spanInfo = groupSpans.value[groupKey];
|
|
|
+
|
|
|
+ if (spanInfo && rowIndex === spanInfo.index) {
|
|
|
+ return {
|
|
|
+ rowspan: spanInfo.span,
|
|
|
+ colspan: 1
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ return {
|
|
|
+ rowspan: 0,
|
|
|
+ colspan: 0
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 其他列不合并
|
|
|
+ return {
|
|
|
+ rowspan: 1,
|
|
|
+ colspan: 1
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+// 计算分组合并信息 (优化后符合图片效果)
|
|
|
+const calculateGroupSpans = () => {
|
|
|
+ // 重置分组信息
|
|
|
+ groupSpans.value = {};
|
|
|
+
|
|
|
+ const pageKey = currentPage.value;
|
|
|
+ let currentGroup = '';
|
|
|
+ let groupStartIndex = 0;
|
|
|
+ let groupCount = 0;
|
|
|
+
|
|
|
+ // 先清除所有行的分组标记
|
|
|
+ paginatedList.value.forEach(item => {
|
|
|
+ delete item.isGroupFirstRow;
|
|
|
+ });
|
|
|
+
|
|
|
+ paginatedList.value.forEach((item, index) => {
|
|
|
+ // 获取当前行的分组(从name中提取)
|
|
|
+ const group = item.name && item.name.includes('->')
|
|
|
+ ? item.name.split('->')[0].trim()
|
|
|
+ : '';
|
|
|
+
|
|
|
+ // 如果分组变化
|
|
|
+ if (group !== currentGroup) {
|
|
|
+ // 保存上一个分组的信息
|
|
|
+ if (currentGroup && groupCount > 0) {
|
|
|
+ const groupKey = `${pageKey}-${currentGroup}`;
|
|
|
+ groupSpans.value[groupKey] = {
|
|
|
+ span: groupCount,
|
|
|
+ index: groupStartIndex
|
|
|
+ };
|
|
|
+
|
|
|
+ // 标记上一个分组的起始行(添加这行)
|
|
|
+ paginatedList.value[groupStartIndex].isGroupFirstRow = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 开始新分组
|
|
|
+ currentGroup = group;
|
|
|
+ groupStartIndex = index;
|
|
|
+ groupCount = 1;
|
|
|
+ } else {
|
|
|
+ // 相同分组,计数增加
|
|
|
+ groupCount++;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 保存最后一个分组的信息
|
|
|
+ if (currentGroup && groupCount > 0) {
|
|
|
+ const groupKey = `${pageKey}-${currentGroup}`;
|
|
|
+ groupSpans.value[groupKey] = {
|
|
|
+ span: groupCount,
|
|
|
+ index: groupStartIndex
|
|
|
+ };
|
|
|
+
|
|
|
+ // 标记最后一个分组的起始行(添加这行)
|
|
|
+ paginatedList.value[groupStartIndex].isGroupFirstRow = true;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 运行时间周期 单行校验方法
|
|
|
+const validateRunningTime = (row: IotMainWorkOrderBomVO) => {
|
|
|
+ if (row.runningTimeRule === 0 && (!row.nextRunningTime || row.nextRunningTime <= 0)) {
|
|
|
+ row.timeError = t('mainPlan.runningTimeCycleError');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ row.timeError = '';
|
|
|
+ return true;
|
|
|
+};
|
|
|
+
|
|
|
+// 计算属性:获取当前页的数据
|
|
|
+const paginatedList = computed(() => {
|
|
|
+ const start = (currentPage.value - 1) * pageSize.value
|
|
|
+ const end = start + pageSize.value
|
|
|
+ return list.value.slice(start, end)
|
|
|
+})
|
|
|
+
|
|
|
+// 页码改变处理
|
|
|
+const handleCurrentChange = (newPage: number) => {
|
|
|
+ currentPage.value = newPage
|
|
|
+}
|
|
|
+
|
|
|
+// 每页条数改变处理(保持10条)
|
|
|
+const handleSizeChange = (newSize: number) => {
|
|
|
+ pageSize.value = newSize
|
|
|
+ currentPage.value = 1 // 重置到第一页
|
|
|
+}
|
|
|
+
|
|
|
+// 运行时间周期 全局校验方法(在submitForm中调用)
|
|
|
+const validateAllRunningTimes = (): boolean => {
|
|
|
+ let isValid = true;
|
|
|
+ list.value.forEach(row => {
|
|
|
+ if (row.runningTimeRule === 0 && (!row.nextRunningTime || row.nextRunningTime <= 0)) {
|
|
|
+ isValid = false;
|
|
|
+ // 高亮标记错误行
|
|
|
+ row.timeError = t('mainPlan.runningTimeCycleError');
|
|
|
+ message.error(`${t('mainPlan.runningTimeCycleError')}: ${row.deviceCode}-${row.name}`);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return isValid;
|
|
|
+};
|
|
|
+
|
|
|
+/** 新增物料 */
|
|
|
+const handleAddMaterial = () => {
|
|
|
+ // 检查是否选中保养项
|
|
|
+ if (!currentBomItem.value) {
|
|
|
+ message.warning('请先在上方表格中选择一个保养项');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建空记录
|
|
|
+ const newMaterial = {
|
|
|
+ // 关联字段
|
|
|
+ deviceId: currentBomItem.value.deviceId,
|
|
|
+ bomNodeId: currentBomItem.value.bomNodeId,
|
|
|
+
|
|
|
+ // 不可编辑字段 - 设置为空
|
|
|
+ factory: '',
|
|
|
+ costCenter: '',
|
|
|
+ projectDepartment: '',
|
|
|
+ factoryId: undefined,
|
|
|
+ costCenterId: undefined,
|
|
|
+ storageLocationId: undefined,
|
|
|
+ totalInventoryQuantity: '',
|
|
|
+
|
|
|
+ // 可编辑字段 - 设置为初始值
|
|
|
+ materialCode: '',
|
|
|
+ materialName: '',
|
|
|
+ unit: '',
|
|
|
+ unitPrice: 0,
|
|
|
+ quantity: 0,
|
|
|
+
|
|
|
+ // 固定字段
|
|
|
+ materialSource: '手动添加',
|
|
|
+
|
|
|
+ // 临时标识(用于区分新记录)
|
|
|
+ isNew: true,
|
|
|
+ tempId: Date.now() // 临时ID用于唯一标识
|
|
|
+ };
|
|
|
+
|
|
|
+ // 生成唯一键
|
|
|
+ const uniqueKey = `${currentBomItem.value.deviceId}${currentBomItem.value.bomNodeId}`;
|
|
|
+
|
|
|
+ // 更新 bomMaterialsMap
|
|
|
+ if (!bomMaterialsMap.value[uniqueKey]) {
|
|
|
+ bomMaterialsMap.value[uniqueKey] = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加到物料映射表的最前面
|
|
|
+ bomMaterialsMap.value[uniqueKey].unshift(newMaterial);
|
|
|
+
|
|
|
+ // 关键修复:同步更新materialList引用(确保视图实时刷新)
|
|
|
+ if (!showAllMaterials.value) {
|
|
|
+ // 显示当前保养项物料时,强制让materialList指向最新的bomMaterialsMap数据
|
|
|
+ materialList.value = bomMaterialsMap.value[uniqueKey];
|
|
|
+ } else {
|
|
|
+ // 显示所有物料时,重新赋值触发响应式(因displayedMaterialList依赖bomMaterialsMap)
|
|
|
+ materialList.value = [...materialList.value];
|
|
|
+ }
|
|
|
+
|
|
|
+ message.success('已新增空白物料记录,请填写相关信息');
|
|
|
+};
|
|
|
+
|
|
|
+const openMaterialForm = (row: any) => {
|
|
|
+ bomNodeId.value = row.bomNodeId;
|
|
|
+ console.log('这是一个对象:', row.bomNodeId)
|
|
|
+ const type = 'maintenance'
|
|
|
+ materialFormRef.value.open(formData.value.deptId, bomNodeId.value, row, type)
|
|
|
+}
|
|
|
+
|
|
|
+// 查看当前保养项已经绑定的物料列表
|
|
|
+const openDeviceBomMaterials = (row: any) => {
|
|
|
+ bomNodeId.value = row.bomNodeId;
|
|
|
+ const type = 'maintenance'
|
|
|
+ deviceBomMaterialsRef.value.open(formData.value.deptId, bomNodeId.value, row, type)
|
|
|
+}
|
|
|
+
|
|
|
+const selectChoose = (selectedMaterial) => {
|
|
|
+ selectedMaterial.bomNodeId = bomNodeId.value
|
|
|
+ // 关联 bomNodeId
|
|
|
+ const processedMaterials = selectedMaterial.map(material => ({
|
|
|
+ ...material,
|
|
|
+ bomNodeId: bomNodeId.value // 统一关联当前行的 bomNodeId
|
|
|
+ }));
|
|
|
+
|
|
|
+ // 避免重复添加
|
|
|
+ processedMaterials.forEach(newMaterial => {
|
|
|
+ // 检查是否已存在相同 工厂+成本中心+库存地点+bomNodeId + materialCode 的条目
|
|
|
+ const isExist = materialList.value.some(item =>
|
|
|
+ item.bomNodeId === bomNodeId.value &&
|
|
|
+ item.materialCode === newMaterial.materialCode &&
|
|
|
+ item.factoryId === newMaterial.factoryId &&
|
|
|
+ item.costCenterId === newMaterial.costCenterId &&
|
|
|
+ item.storageLocationId === newMaterial.storageLocationId
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!isExist) {
|
|
|
+ materialList.value.push(newMaterial);
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// 计算属性:判断是否需要显示运行时间周期列
|
|
|
+const shouldShowRunningTimeColumn = computed(() => {
|
|
|
+ return list.value.some(item => item.runningTimeRule === 0);
|
|
|
+});
|
|
|
+
|
|
|
+/** 查看已经选择的物料 并编辑 */
|
|
|
+const handleView = (row: IotMainWorkOrderBomVO) => {
|
|
|
+ currentBomNodeId.value = row.bomNodeId
|
|
|
+ drawerVisible.value = true
|
|
|
+ // 根据状态值设置是否隐藏额外列
|
|
|
+ hideExtraColumnsInDrawer.value = row.status === 1
|
|
|
+ console.log('当前bom节点:', currentBomNodeId.value)
|
|
|
+}
|
|
|
+
|
|
|
+// 计算保养金额
|
|
|
+const calculateTotalCost = () => {
|
|
|
+ // 物料总金额 = ∑(单价 * 消耗数量)
|
|
|
+ const materialTotal = materialList.value.reduce((sum, item) => {
|
|
|
+ const price = Number(item.unitPrice) || 0
|
|
|
+ const quantity = Number(item.quantity) || 0
|
|
|
+ return sum + (price * quantity)
|
|
|
+ }, 0)
|
|
|
+
|
|
|
+ // 保养 = 物料总金额
|
|
|
+ formData.value.cost = (materialTotal).toFixed(2)
|
|
|
+}
|
|
|
+
|
|
|
+// 监听物料列表变化
|
|
|
+watch(
|
|
|
+ () => materialList.value,
|
|
|
+ () => {
|
|
|
+ calculateTotalCost()
|
|
|
+ },
|
|
|
+ { deep: true }
|
|
|
+)
|
|
|
+
|
|
|
+const handleInput = (value, obj) => {
|
|
|
+ // 1. 过滤非法字符(只允许数字和小数点)
|
|
|
+ let filtered = value.replace(/[^\d.]/g, '')
|
|
|
+
|
|
|
+ // 2. 处理多个小数点的情况
|
|
|
+ filtered = filtered.replace(/\.{2,}/g, '.')
|
|
|
+
|
|
|
+ // 3. 限制小数点后最多两位
|
|
|
+ let decimalParts = filtered.split('.')
|
|
|
+ if (decimalParts.length > 1) {
|
|
|
+ decimalParts = decimalParts.slice(0, 2)
|
|
|
+ filtered = decimalParts.join('.')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 处理以小数点开头的情况(自动补0)
|
|
|
+ if (filtered.startsWith('.')) {
|
|
|
+ filtered = '0' + filtered
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 更新绑定值(同时处理连续输入多个0的情况)
|
|
|
+ formData.value[obj] = filtered.replace(/^0+(?=\d)/, '')
|
|
|
+}
|
|
|
+
|
|
|
+// 保存配置
|
|
|
+const saveConfig = () => {
|
|
|
+ (configFormRef.value as any).validate((valid: boolean) => {
|
|
|
+ if (!valid) return
|
|
|
+ if (!configDialog.current) return
|
|
|
+
|
|
|
+ // 动态校验逻辑
|
|
|
+ const requiredFields = []
|
|
|
+ if (configDialog.current.mileageRule === 0) {
|
|
|
+ requiredFields.push('nextRunningKilometers', 'kiloCycleLead')
|
|
|
+ }
|
|
|
+ if (configDialog.current.runningTimeRule === 0) {
|
|
|
+ requiredFields.push('nextRunningTime', 'timePeriodLead')
|
|
|
+ }
|
|
|
+ if (configDialog.current.naturalDateRule === 0) {
|
|
|
+ requiredFields.push('nextNaturalDate', 'naturalDatePeriodLead')
|
|
|
+ }
|
|
|
+
|
|
|
+ const missingFields = requiredFields.filter(field =>
|
|
|
+ !configDialog.form[field as keyof typeof configDialog.form]
|
|
|
+ )
|
|
|
+
|
|
|
+ if (missingFields.length > 0) {
|
|
|
+ message.error('请填写所有必填项')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 强制校验逻辑
|
|
|
+ if (configDialog.current.naturalDateRule === 0) {
|
|
|
+ if (!configDialog.form.lastNaturalDate) {
|
|
|
+ message.error('必须选择自然日期')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证日期有效性
|
|
|
+ const dateValue = dayjs(configDialog.form.lastNaturalDate)
|
|
|
+ if (!dateValue.isValid()) {
|
|
|
+ message.error('日期格式不正确')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 转换逻辑(关键修改)
|
|
|
+ const finalDate = configDialog.form.lastNaturalDate
|
|
|
+ ? dayjs(configDialog.form.lastNaturalDate).valueOf()
|
|
|
+ : null // 改为null而不是0
|
|
|
+
|
|
|
+ // 更新当前行的数据
|
|
|
+ Object.assign(configDialog.current, {
|
|
|
+ ...configDialog.form,
|
|
|
+ lastNaturalDate: finalDate
|
|
|
+ })
|
|
|
+ configDialog.visible = false
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const queryParams = reactive({
|
|
|
+ workOrderId: id
|
|
|
+})
|
|
|
+
|
|
|
+// 获取指定bomNodeId的物料数量
|
|
|
+const getMaterialCount = (bomNodeId: number) => {
|
|
|
+ console.log('当前BOM节点:' + bomNodeId)
|
|
|
+ return materialList.value.filter(item => item.bomNodeId === bomNodeId).length
|
|
|
+}
|
|
|
+
|
|
|
+// 删除物料(在表格中)
|
|
|
+const handleDeleteMaterialInTable = (material) => {
|
|
|
+ // 确认删除对话框
|
|
|
+ message.confirm('确认删除该物料吗?', { title: '提示' }).then(() => {
|
|
|
+ let targetBomNodeId = material.bomNodeId;
|
|
|
+
|
|
|
+ // 如果没有选中保养项(显示所有物料),需要根据物料信息找到对应的bomNodeId
|
|
|
+ if (showAllMaterials.value && !currentBomItem.value) {
|
|
|
+ // 从物料中提取bomNodeId
|
|
|
+ targetBomNodeId = material.bomNodeId;
|
|
|
+ } else if (currentBomItem.value) {
|
|
|
+ // 使用当前选中保养项的bomNodeId
|
|
|
+ targetBomNodeId = currentBomItem.value.bomNodeId;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从materialList中删除
|
|
|
+ const index = materialList.value.findIndex(item =>
|
|
|
+ // 对于新记录,使用tempId进行比较;对于已有记录,使用原有逻辑
|
|
|
+ (item.isNew && item.tempId === material.tempId) ||
|
|
|
+ (!item.isNew &&
|
|
|
+ item.bomNodeId === targetBomNodeId &&
|
|
|
+ item.factoryId === material.factoryId &&
|
|
|
+ item.costCenterId === material.costCenterId &&
|
|
|
+ item.storageLocationId === material.storageLocationId &&
|
|
|
+ item.materialCode === material.materialCode)
|
|
|
+ );
|
|
|
+
|
|
|
+ if (index !== -1) {
|
|
|
+ materialList.value.splice(index, 1);
|
|
|
+
|
|
|
+ // 同时从bomMaterialsMap中删除
|
|
|
+ const uniqueKey = `${material.deviceId}${targetBomNodeId}`;
|
|
|
+ if (bomMaterialsMap.value[uniqueKey]) {
|
|
|
+ const mapIndex = bomMaterialsMap.value[uniqueKey].findIndex(item =>
|
|
|
+ (item.isNew && item.tempId === material.tempId) ||
|
|
|
+ (!item.isNew &&
|
|
|
+ item.factoryId === material.factoryId &&
|
|
|
+ item.costCenterId === material.costCenterId &&
|
|
|
+ item.storageLocationId === material.storageLocationId &&
|
|
|
+ item.materialCode === material.materialCode)
|
|
|
+ );
|
|
|
+ if (mapIndex !== -1) {
|
|
|
+ bomMaterialsMap.value[uniqueKey].splice(mapIndex, 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ message.success('物料删除成功');
|
|
|
+ } else {
|
|
|
+ message.error('未找到要删除的物料');
|
|
|
+ }
|
|
|
+ }).catch(() => {
|
|
|
+ // 用户取消删除
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const handleDeleteMaterial = (material) => {
|
|
|
+ // 根据唯一标识查找要删除的物料索引
|
|
|
+ const index = materialList.value.findIndex(item =>
|
|
|
+ item.bomNodeId === material.bomNodeId &&
|
|
|
+ item.factoryId === material.factoryId &&
|
|
|
+ item.costCenterId === material.costCenterId &&
|
|
|
+ item.storageLocationId === material.storageLocationId &&
|
|
|
+ item.materialCode === material.materialCode
|
|
|
+ );
|
|
|
+
|
|
|
+ if (index !== -1) {
|
|
|
+ materialList.value.splice(index, 1);
|
|
|
+ message.success('物料删除成功');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const close = () => {
|
|
|
+ delView(unref(currentRoute))
|
|
|
+ push({ name: 'IotMainWorkOrder', params:{}})
|
|
|
+}
|
|
|
+
|
|
|
+// 分组单元格类名方法
|
|
|
+const groupCellClassName = ({ row, column }) => {
|
|
|
+ if (column.property === 'group' && row.isGroupFirstRow) {
|
|
|
+ return 'group-first-row';
|
|
|
+ }
|
|
|
+ return '';
|
|
|
+};
|
|
|
+
|
|
|
+/** 提交表单 */
|
|
|
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
|
|
+const submitForm = async () => {
|
|
|
+ // 校验表单
|
|
|
+ await formRef.value.validate()
|
|
|
+
|
|
|
+ // 运行时间周期全局校验
|
|
|
+ if (!validateAllRunningTimes()) {
|
|
|
+ return; // 校验失败则终止提交
|
|
|
+ }
|
|
|
+
|
|
|
+ // 校验表格数据
|
|
|
+ const isValid = validateTableData()
|
|
|
+ if (!isValid) return
|
|
|
+ // 提交请求
|
|
|
+ formLoading.value = true
|
|
|
+ try {
|
|
|
+ const convertedList = list.value.map(item => ({
|
|
|
+ ...item,
|
|
|
+ lastNaturalDate: typeof item.lastNaturalDate === 'number'
|
|
|
+ ? item.lastNaturalDate
|
|
|
+ : (item.lastNaturalDate ? dayjs(item.lastNaturalDate).valueOf() : null)
|
|
|
+ }));
|
|
|
+ const data = {
|
|
|
+ mainWorkOrder: formData.value,
|
|
|
+ mainWorkOrderBom: convertedList,
|
|
|
+ mainWorkOrderMaterials: materialList.value
|
|
|
+ }
|
|
|
+ await IotMainWorkOrderApi.fillWorkOrder(data)
|
|
|
+ message.success(t('common.createSuccess'))
|
|
|
+ close()
|
|
|
+ // 发送操作成功的事件
|
|
|
+ emit('success')
|
|
|
+ } finally {
|
|
|
+ formLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const validateDelayReason = (rule: any, value: any, callback: any) => {
|
|
|
+ const form = configDialog.form
|
|
|
+ const hasDelay =
|
|
|
+ (form.delayKilometers > 0) ||
|
|
|
+ (form.delayDuration > 0) ||
|
|
|
+ (form.delayNaturalDate > 0)
|
|
|
+
|
|
|
+ if (hasDelay && (!value || value.trim() === '')) {
|
|
|
+ callback(new Error('请填写推迟原因'))
|
|
|
+ } else {
|
|
|
+ callback()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 新增表单校验规则
|
|
|
+const configFormRules = reactive({
|
|
|
+ nextRunningKilometers: [{
|
|
|
+ required: true,
|
|
|
+ message: '里程周期必须填写',
|
|
|
+ trigger: 'blur'
|
|
|
+ }],
|
|
|
+ kiloCycleLead: [{
|
|
|
+ required: true,
|
|
|
+ message: '提前量必须填写',
|
|
|
+ trigger: 'blur'
|
|
|
+ }],
|
|
|
+ nextRunningTime: [{
|
|
|
+ required: true,
|
|
|
+ message: '时间周期必须填写',
|
|
|
+ trigger: 'blur'
|
|
|
+ }],
|
|
|
+ timePeriodLead: [{
|
|
|
+ required: true,
|
|
|
+ message: '提前量必须填写',
|
|
|
+ trigger: 'blur'
|
|
|
+ }],
|
|
|
+ nextNaturalDate: [{
|
|
|
+ required: true,
|
|
|
+ message: '自然日周期必须填写',
|
|
|
+ trigger: 'blur'
|
|
|
+ }],
|
|
|
+ naturalDatePeriodLead: [{
|
|
|
+ required: true,
|
|
|
+ message: '提前量必须填写',
|
|
|
+ trigger: 'blur'
|
|
|
+ }],
|
|
|
+ // 新增推迟原因验证规则
|
|
|
+ delayReason: [
|
|
|
+ { validator: validateDelayReason, trigger: ['blur', 'change'] }
|
|
|
+ ]
|
|
|
+})
|
|
|
+
|
|
|
+// 计算文本宽度的辅助函数
|
|
|
+const getTextWidth = (text: string, fontSize = 14): number => {
|
|
|
+ if (!text) return 0;
|
|
|
+
|
|
|
+ const span = document.createElement('span')
|
|
|
+ span.style.visibility = 'hidden'
|
|
|
+ span.style.position = 'absolute'
|
|
|
+ span.style.whiteSpace = 'nowrap'
|
|
|
+ span.style.fontSize = `${fontSize}px` // 与表格实际字体一致
|
|
|
+ span.style.fontFamily = 'inherit'
|
|
|
+ span.style.fontWeight = 'normal';
|
|
|
+ span.innerText = text
|
|
|
+ document.body.appendChild(span)
|
|
|
+ const width = span.offsetWidth
|
|
|
+ document.body.removeChild(span)
|
|
|
+ return width
|
|
|
+}
|
|
|
+
|
|
|
+// 计算列宽的主函数
|
|
|
+const calculateMaintItemsWidth = () => {
|
|
|
+ if (list.value.length === 0) {
|
|
|
+ maintItemsWidth.value = 'auto'
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 计算表头文本宽度
|
|
|
+ const headerText = t('mainPlan.MaintItems')
|
|
|
+ const headerWidth = getTextWidth(headerText)
|
|
|
+
|
|
|
+ // 2. 计算内容最大宽度
|
|
|
+ let contentMaxWidth = 0
|
|
|
+ list.value.forEach(item => {
|
|
|
+ if (item.name) {
|
|
|
+ const width = getTextWidth(item.name.toString())
|
|
|
+ if (width > contentMaxWidth) {
|
|
|
+ contentMaxWidth = width
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 3. 取最大值 + 内边距(20px)
|
|
|
+ const maxWidth = Math.max(headerWidth, contentMaxWidth) + 20
|
|
|
+ maintItemsWidth.value = `${maxWidth}px`
|
|
|
+}
|
|
|
+
|
|
|
+// 计算下次保养公里数(通用函数)
|
|
|
+const calculateNextMaintenanceKm = (row: IotMaintenanceBomVO) => {
|
|
|
+ // 验证条件:规则开启 + 两个值都存在且 > 0
|
|
|
+ const isValid = row.mileageRule === 0 &&
|
|
|
+ row.lastRunningKilometers > 0 &&
|
|
|
+ row.nextRunningKilometers > 0;
|
|
|
+
|
|
|
+ return isValid
|
|
|
+ ? (row.lastRunningKilometers + row.nextRunningKilometers)
|
|
|
+ : null; // 不满足条件返回null
|
|
|
+};
|
|
|
+
|
|
|
+// 计算剩余保养公里数(通用函数)
|
|
|
+const calculateRemainKm = (row: IotMaintenanceBomVO) => {
|
|
|
+ // 确定使用的里程值(优先totalMileage)
|
|
|
+ const mileageValue = row.totalMileage ?? row.tempTotalMileage;
|
|
|
+ // 验证条件:规则开启 + 3个值都存在且 > 0
|
|
|
+ const isValid = row.mileageRule === 0 &&
|
|
|
+ row.lastRunningKilometers > 0 &&
|
|
|
+ mileageValue > 0 &&
|
|
|
+ row.nextRunningKilometers > 0;
|
|
|
+
|
|
|
+ if (!isValid) return 0;
|
|
|
+ const result = row.nextRunningKilometers - (mileageValue - row.lastRunningKilometers);
|
|
|
+ return parseFloat(result.toFixed(2));
|
|
|
+};
|
|
|
+
|
|
|
+// 计算下次保养运行时长(通用函数)
|
|
|
+const calculateNextMaintenanceH = (row: IotMaintenanceBomVO) => {
|
|
|
+ // 验证条件:规则开启 + 两个值都存在且 > 0
|
|
|
+ const isValid = row.runningTimeRule === 0 &&
|
|
|
+ row.lastRunningTime > 0 &&
|
|
|
+ row.nextRunningTime > 0;
|
|
|
+
|
|
|
+ return isValid
|
|
|
+ ? (row.lastRunningTime + row.nextRunningTime)
|
|
|
+ : null; // 不满足条件返回null
|
|
|
+};
|
|
|
+
|
|
|
+// 计算剩余运行时间(通用函数)
|
|
|
+const calculateRemainH = (row: IotMaintenanceBomVO) => {
|
|
|
+ // 确定使用的 运行时长 值(优先 totalRunTime)
|
|
|
+ const runTimeValue = row.totalRunTime ?? row.tempTotalRunTime;
|
|
|
+ // 验证条件:规则开启 + 3个值都存在且 > 0
|
|
|
+ const isValid = row.runningTimeRule === 0 &&
|
|
|
+ row.lastRunningTime > 0 &&
|
|
|
+ runTimeValue > 0 &&
|
|
|
+ row.nextRunningTime > 0;
|
|
|
+ if (!isValid) return 0;
|
|
|
+ const result = row.nextRunningTime - (runTimeValue - row.lastRunningTime);
|
|
|
+ return parseFloat(result.toFixed(2));
|
|
|
+};
|
|
|
+
|
|
|
+// 计算下次保养日期(通用函数)
|
|
|
+const calculateNextMaintenanceDate = (row: IotMaintenanceBomVO) => {
|
|
|
+ // 验证条件:规则开启 + 两个值都存在且 > 0
|
|
|
+ const isValid = row.naturalDateRule === 0 &&
|
|
|
+ row.lastNaturalDate &&
|
|
|
+ row.nextNaturalDate;
|
|
|
+
|
|
|
+ return isValid
|
|
|
+ ? dayjs(row.lastNaturalDate).add(row.nextNaturalDate, 'day').format('YYYY-MM-DD')
|
|
|
+ : null; // 不满足条件返回null
|
|
|
+};
|
|
|
+
|
|
|
+// 计算 自然日期保养 剩余天数(通用函数)
|
|
|
+const calculateRemainDay = (row: IotMaintenanceBomVO) => {
|
|
|
+ // 验证条件:规则开启 + 两个值都存在且有效
|
|
|
+ const isValid = row.naturalDateRule === 0 &&
|
|
|
+ row.lastNaturalDate !== null &&
|
|
|
+ row.nextNaturalDate !== null &&
|
|
|
+ row.nextNaturalDate > 0;
|
|
|
+
|
|
|
+ if (!isValid) {
|
|
|
+ return '-';
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 上次保养日期:将时间戳转换为 Day.js 对象
|
|
|
+ const lastNaturalDate = dayjs(row.lastNaturalDate);
|
|
|
+
|
|
|
+ // 计算下次保养日期
|
|
|
+ const nextMaintenanceDate = lastNaturalDate.add(row.nextNaturalDate, 'day');
|
|
|
+
|
|
|
+ // 计算剩余天数(当前日期到下次保养日期的天数差)
|
|
|
+ return nextMaintenanceDate.diff(dayjs(), 'day');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('计算保养剩余天数错误:', error);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+};
|
|
|
+
|
|
|
+const getStatusText = (row: any) => {
|
|
|
+ // 状态为1直接返回"完成"
|
|
|
+ if (row.status === 1) return t('mainPlan.completed');
|
|
|
+
|
|
|
+ // 状态为0时判断延迟字段
|
|
|
+ const delayDuration = Number(row.delayDuration) || 0;
|
|
|
+ const delayKilometers = Number(row.delayKilometers) || 0;
|
|
|
+ const delayNaturalDate = Number(row.delayNaturalDate) || 0;
|
|
|
+
|
|
|
+ // 任意延迟字段大于0 -> 延时
|
|
|
+ if (delayDuration > 0 || delayKilometers > 0 || delayNaturalDate > 0) {
|
|
|
+ return t('mainPlan.delayed');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 否则显示保养中
|
|
|
+ return t('mainPlan.maintaining');
|
|
|
+};
|
|
|
+
|
|
|
+// 计算属性 - 检查当前页是否有开启的里程规则
|
|
|
+const hasMileageRuleInCurrentPage = computed(() => {
|
|
|
+ return paginatedList.value.some(row => row.mileageRule === 0);
|
|
|
+});
|
|
|
+
|
|
|
+// 计算属性 - 检查当前页是否有开启的 运行时间 规则
|
|
|
+const hasTimeRuleInCurrentPage = computed(() => {
|
|
|
+ return paginatedList.value.some(row => row.runningTimeRule === 0);
|
|
|
+});
|
|
|
+
|
|
|
+// 计算属性 - 检查当前页是否有开启的 自然日期 规则
|
|
|
+const hasDateRuleInCurrentPage = computed(() => {
|
|
|
+ return paginatedList.value.some(row => row.naturalDateRule === 0);
|
|
|
+});
|
|
|
+
|
|
|
+// 统一计算所有列宽
|
|
|
+const calculateAllColumnsWidth = () => {
|
|
|
+ const MIN_WIDTH = 70; // 最小列宽
|
|
|
+ const PADDING = 10; // 列内边距
|
|
|
+ const FIXED_COLUMN_PADDING = 10; // 固定列额外内边距
|
|
|
+ const GROUP_COLUMN_EXTRA = 10; // 分组列额外宽度
|
|
|
+
|
|
|
+ // 需要自适应的列配置
|
|
|
+ const autoColumns = [
|
|
|
+ { prop: 'serial', label: t('iotDevice.serial') },
|
|
|
+ // { prop: 'deviceCode', label: t('iotMaintain.deviceCode') },
|
|
|
+ // { prop: 'deviceName', label: t('iotMaintain.deviceName') },
|
|
|
+ { prop: 'group', label: t('mainPlan.MaintItemsGroup') },
|
|
|
+ {
|
|
|
+ prop: 'totalRunTime',
|
|
|
+ label: t('operationFillForm.sumTime'),
|
|
|
+ getValue: (row) => row.totalRunTime ?? row.tempTotalRunTime
|
|
|
+ },
|
|
|
+ {
|
|
|
+ prop: 'totalMileage',
|
|
|
+ label: t('operationFillForm.sumKil'),
|
|
|
+ getValue: (row) => row.totalMileage ?? row.tempTotalMileage
|
|
|
+ },
|
|
|
+ { prop: 'name', label: t('bomList.bomNode') },
|
|
|
+ { prop: 'lastMaintenanceDate', label: t('mainPlan.lastMaintenanceDate') },
|
|
|
+ { prop: 'mileageRule', label: t('main.mileage') },
|
|
|
+ { prop: 'runningTimeRule', label: t('main.runTime') },
|
|
|
+ { prop: 'naturalDateRule', label: t('main.date') },
|
|
|
+ { prop: 'lastRunningKilometers', label: t('mainPlan.lastMaintenanceMileage') },
|
|
|
+ { prop: 'nextMaintenanceKm', label: t('mainPlan.nextMaintenanceKm') },
|
|
|
+ { prop: 'remainKm', label: t('mainPlan.remainKm') },
|
|
|
+ { prop: 'lastRunningTime', label: t('mainPlan.lastMaintenanceOperationTime') },
|
|
|
+ { prop: 'nextMaintenanceH', label: t('mainPlan.nextMaintenanceH') },
|
|
|
+ { prop: 'remainH', label: t('mainPlan.remainH') },
|
|
|
+ { prop: 'nextRunningTime', label: t('mainPlan.RunTimeCycle') },
|
|
|
+ { prop: 'tempLastNaturalDate', label: t('mainPlan.lastMaintenanceNaturalDate') },
|
|
|
+ { prop: 'nextMaintenanceDate', label: t('mainPlan.nextMaintDate') },
|
|
|
+ { prop: 'remainDay', label: t('mainPlan.remainDay') },
|
|
|
+ { prop: 'rule', label: t('mainPlan.consumeMaterials') }, // 消耗物料规则列
|
|
|
+ { prop: 'operation', label: t('operationFill.operation') }
|
|
|
+ ];
|
|
|
+
|
|
|
+ const newWidths: Record<string, number> = {};
|
|
|
+
|
|
|
+ autoColumns.forEach(col => {
|
|
|
+ const headerText = col.label;
|
|
|
+ // 计算表头宽度
|
|
|
+ const headerWidth = getTextWidth(headerText) * 1.2;
|
|
|
+
|
|
|
+ // 计算内容最大宽度
|
|
|
+ let contentMaxWidth = 0;
|
|
|
+ if (col.prop === 'operation') {
|
|
|
+ // 操作列固定宽度(根据按钮数量)
|
|
|
+ contentMaxWidth = 50;
|
|
|
+ } else if (['mileageRule', 'runningTimeRule', 'naturalDateRule', 'rule'].includes(col.prop)) {
|
|
|
+ // 开关列固定宽度
|
|
|
+ contentMaxWidth = 80;
|
|
|
+ } else {
|
|
|
+ list.value.forEach(row => {
|
|
|
+ const text = col.getValue ? String(col.getValue(row)) : String(row[col.prop] || '');
|
|
|
+ const textWidth = getTextWidth(text);
|
|
|
+ if (textWidth > contentMaxWidth) contentMaxWidth = textWidth;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // 取最大值并添加内边距
|
|
|
+ let finalWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING;
|
|
|
+ // 为分组列增加额外宽度
|
|
|
+ if ([
|
|
|
+ 'group',
|
|
|
+ 'lastRunningKilometers',
|
|
|
+ 'nextMaintenanceKm',
|
|
|
+ 'remainKm',
|
|
|
+ 'lastRunningTime',
|
|
|
+ 'nextMaintenanceH',
|
|
|
+ 'remainH',
|
|
|
+ 'tempLastNaturalDate',
|
|
|
+ 'nextMaintenanceDate',
|
|
|
+ 'remainDay'
|
|
|
+ ].includes(col.prop)) {
|
|
|
+ finalWidth += GROUP_COLUMN_EXTRA;
|
|
|
+ }
|
|
|
+
|
|
|
+ newWidths[col.prop] = finalWidth;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 固定列特殊处理 - 增加额外空间
|
|
|
+ ['serial', 'name'].forEach(prop => {
|
|
|
+ if (newWidths[prop]) {
|
|
|
+ newWidths[prop] += FIXED_COLUMN_PADDING;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 转换为CSS宽度值
|
|
|
+ Object.keys(newWidths).forEach(prop => {
|
|
|
+ columnWidths.value[prop] = `${newWidths[prop]}px`;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 为每一行建立lastNaturalDate到tempLastNaturalDate的同步
|
|
|
+const setupNaturalDateSync = (row: IotMaintenanceBomVO) => {
|
|
|
+ // 如果该行已有watcher则跳过
|
|
|
+ if (lastNaturalDateWatchers.value.has(row.id)) return
|
|
|
+
|
|
|
+ // 为该行创建单独的watcher
|
|
|
+ const unwatch = watch(
|
|
|
+ () => row.lastNaturalDate,
|
|
|
+ (newVal) => {
|
|
|
+ // 转换日期格式 (时间戳 -> YYYY-MM-DD)
|
|
|
+ row.tempLastNaturalDate = newVal
|
|
|
+ ? dayjs(newVal).format('YYYY-MM-DD')
|
|
|
+ : null;
|
|
|
+ },
|
|
|
+ { immediate: true, deep: true }
|
|
|
+ )
|
|
|
+
|
|
|
+ // 保存watcher用于后续清理
|
|
|
+ lastNaturalDateWatchers.value.set(row.id, unwatch)
|
|
|
+}
|
|
|
+
|
|
|
+const tableHeaderStyle = ({ row, rowIndex }) => {
|
|
|
+ return {
|
|
|
+ border: '1px solid #333',
|
|
|
+ backgroundColor: rowIndex === 0 ? '#f0f9eb' : '#f5f7fa' // 分组行特殊背景
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 原有代码保持不变,新增“选择物料”按钮的点击处理方法
|
|
|
+const handleSelectMaterial = () => {
|
|
|
+ // 校验是否已选中保养项(避免无选中项时点击按钮报错)
|
|
|
+ if (!currentBomItem.value) {
|
|
|
+ message.warning('请先在上方表格中选择一个保养项');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 调用现有方法打开物料选择窗口,传入当前选中的保养项
|
|
|
+ openMaterialForm(currentBomItem.value);
|
|
|
+};
|
|
|
+
|
|
|
+// 监听分页数据和规则变化 - 重新布局表格
|
|
|
+watch([paginatedList, hasMileageRuleInCurrentPage, hasTimeRuleInCurrentPage, hasDateRuleInCurrentPage], () => {
|
|
|
+ nextTick(() => {
|
|
|
+ tableRef.value?.doLayout();
|
|
|
+ calculateAllColumnsWidth(); // 重新计算列宽
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+// 监听分页变化时重新计算分组信息
|
|
|
+watch([paginatedList, currentPage], () => {
|
|
|
+ calculateGroupSpans();
|
|
|
+ calculateAllColumnsWidth();
|
|
|
+ tableRef.value?.doLayout(); // 确保表格重新布局
|
|
|
+});
|
|
|
+
|
|
|
+// 监听分页数据变化,自动选中第一条
|
|
|
+watch(() => paginatedList.value, (newList) => {
|
|
|
+ if (newList && newList.length > 0) {
|
|
|
+ // 取消之前选中的保养项(如果存在)
|
|
|
+ if (currentBomItem.value) {
|
|
|
+ currentBomItem.value.isSelected = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 选中新分页的第一条
|
|
|
+ handleBomItemClick(newList[0]);
|
|
|
+ } else {
|
|
|
+ // 如果没有数据,清空选中状态
|
|
|
+ currentBomItem.value = null;
|
|
|
+ materialList.value = [];
|
|
|
+ }
|
|
|
+}, { immediate: true });
|
|
|
+
|
|
|
+// 下拉菜单命令处理
|
|
|
+const handleDropdownCommand = (command: { action: string; row: IotMainWorkOrderBomVO }) => {
|
|
|
+ switch (command.action) {
|
|
|
+ case 'delay':
|
|
|
+ openConfigDialog(command.row);
|
|
|
+ break;
|
|
|
+ case 'material':
|
|
|
+ openMaterialForm(command.row);
|
|
|
+ break;
|
|
|
+ case 'deviceBomMaterials':
|
|
|
+ openDeviceBomMaterials(command.row);
|
|
|
+ break;
|
|
|
+ case 'detail':
|
|
|
+ handleView(command.row);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 校验表格数据 */
|
|
|
+const validateTableData = (): boolean => {
|
|
|
+ let isValid = true;
|
|
|
+ const errorMessages: string[] = []; // 通用错误集合
|
|
|
+ const materialRequiredErrors: string[] = []; // 物料缺失专用错误集合
|
|
|
+
|
|
|
+ // 1. 基础校验:工单明细是否存在
|
|
|
+ if (list.value.length === 0) {
|
|
|
+ message.error('工单明细不存在');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 校验设备状态
|
|
|
+ list.value.forEach((row, index) => {
|
|
|
+ const rowNumber = index + 1;
|
|
|
+ if (row.deviceCode === null || row.deviceName === null) {
|
|
|
+ errorMessages.push(`第${rowNumber}行设备状态错误`);
|
|
|
+ isValid = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 3. 校验物料必填(仅在非委外模式下)
|
|
|
+ if (formData.value.outsourcingFlag !== 1) {
|
|
|
+ list.value.forEach((row, index) => {
|
|
|
+ const rowNumber = index + 1;
|
|
|
+ const deviceIdentifier = `${row.deviceCode}-${row.name}`;
|
|
|
+
|
|
|
+ // 检查消耗物料规则,如果为1(不消耗物料)则跳过校验
|
|
|
+ if (row.rule === 1) {
|
|
|
+ return; // 跳过当前保养项的物料校验
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否设置了推迟保养
|
|
|
+ const hasDelay =
|
|
|
+ (row.delayKilometers || 0) > 0 ||
|
|
|
+ (row.delayNaturalDate || 0) > 0 ||
|
|
|
+ (row.delayDuration || 0) > 0;
|
|
|
+
|
|
|
+ // 未设置推迟保养且未选择物料
|
|
|
+ if (!hasDelay && getMaterialCount(row.bomNodeId) === 0) {
|
|
|
+ materialRequiredErrors.push(`第${rowNumber}行【${deviceIdentifier}】未添加物料`);
|
|
|
+ isValid = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 智能错误提示
|
|
|
+ if (!isValid) {
|
|
|
+ // 构建错误消息HTML
|
|
|
+ let errorHtml = '';
|
|
|
+
|
|
|
+ // 添加通用错误
|
|
|
+ if (errorMessages.length > 0) {
|
|
|
+ errorHtml += errorMessages.join('<br>');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加物料错误(带智能截断)
|
|
|
+ if (materialRequiredErrors.length > 0) {
|
|
|
+ if (errorHtml) errorHtml += '<br>'; // 添加换行分隔
|
|
|
+
|
|
|
+ if (materialRequiredErrors.length > 3) {
|
|
|
+ errorHtml += materialRequiredErrors.slice(0, 3).join('<br>');
|
|
|
+ errorHtml += `<br>...等共 ${materialRequiredErrors.length} 个保养项未添加物料`;
|
|
|
+ } else {
|
|
|
+ errorHtml += materialRequiredErrors.join('<br>');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加标题
|
|
|
+ const title = "<span style='font-weight:bold;color:#f56c6c'></span>";
|
|
|
+ errorHtml = title + errorHtml;
|
|
|
+
|
|
|
+ // 显示带格式的错误消息
|
|
|
+ message.error({
|
|
|
+ message: errorHtml,
|
|
|
+ dangerouslyUseHTMLString: true,
|
|
|
+ duration: 8000 // 延长显示时间
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return isValid;
|
|
|
+};
|
|
|
+
|
|
|
+/** 重置表单 */
|
|
|
+const resetForm = () => {
|
|
|
+ formData.value = {
|
|
|
+ id: undefined,
|
|
|
+ deviceId: undefined,
|
|
|
+ status: undefined,
|
|
|
+ description: undefined,
|
|
|
+ pic: undefined,
|
|
|
+ remark: undefined,
|
|
|
+ deviceName: undefined,
|
|
|
+ processInstanceId: undefined,
|
|
|
+ auditStatus: undefined,
|
|
|
+ deptId: undefined
|
|
|
+ }
|
|
|
+ formRef.value?.resetFields()
|
|
|
+}
|
|
|
+
|
|
|
+// 防抖函数实现
|
|
|
+function debounce(func: Function, wait: number) {
|
|
|
+ let timeout: ReturnType<typeof setTimeout> | null
|
|
|
+ return function executedFunction(...args: any[]) {
|
|
|
+ const later = () => {
|
|
|
+ clearTimeout(timeout!)
|
|
|
+ func(...args)
|
|
|
+ }
|
|
|
+ clearTimeout(timeout!)
|
|
|
+ timeout = setTimeout(later, wait)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 响应窗口大小变化
|
|
|
+const handleResize = debounce(calculateMaintItemsWidth, 300)
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ materialList.value = []
|
|
|
+ const deptId = useUserStore().getUser.deptId
|
|
|
+ // 查询当前登录人所属部门名称
|
|
|
+ dept.value = await DeptApi.getDept(deptId)
|
|
|
+ deptUsers.value = await UserApi.getDeptUsersByDeptId(deptId)
|
|
|
+ formData.value.deptId = deptId
|
|
|
+ try{
|
|
|
+ formType.value = 'update'
|
|
|
+ // 查询保养工单 主表数据
|
|
|
+ const workOrder = await IotMainWorkOrderApi.getIotMainWorkOrder(id);
|
|
|
+ formData.value = workOrder
|
|
|
+ // 查询保养工单 明细数据
|
|
|
+ const data = await IotMainWorkOrderBomApi.getWorkOrderBOMs(queryParams);
|
|
|
+ list.value = []
|
|
|
+ if (Array.isArray(data)) {
|
|
|
+ list.value = data.map(item => {
|
|
|
+ // 提取分组名称
|
|
|
+ const group = item.name && item.name.includes('->')
|
|
|
+ ? item.name.split('->')[0].trim()
|
|
|
+ : '';
|
|
|
+
|
|
|
+ // 处理物料数据映射
|
|
|
+ if (item.deviceBomMaterials && item.deviceBomMaterials.length > 0) {
|
|
|
+ // 生成唯一键
|
|
|
+ const uniqueKey = `${item.deviceId}${item.bomNodeId}`;
|
|
|
+
|
|
|
+ // 转换物料字段映射
|
|
|
+ const mappedMaterials = item.deviceBomMaterials.map(material => ({
|
|
|
+ ...material,
|
|
|
+ materialName: material.name, // name -> materialName
|
|
|
+ materialCode: material.code, // code -> materialCode
|
|
|
+ projectDepartment: material.storageLocation, // storageLocation -> projectDepartment
|
|
|
+ totalInventoryQuantity: material.stockQuantity // stockQuantity -> totalInventoryQuantity
|
|
|
+ }));
|
|
|
+
|
|
|
+ // 存储到映射表中
|
|
|
+ bomMaterialsMap.value[uniqueKey] = mappedMaterials;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (item.mileageRule === 0) {
|
|
|
+ item.nextMaintenanceKm = calculateNextMaintenanceKm(item);
|
|
|
+ item.remainKm = calculateRemainKm(item);
|
|
|
+ }
|
|
|
+ if (item.runningTimeRule === 0) {
|
|
|
+ item.nextMaintenanceH = calculateNextMaintenanceH(item);
|
|
|
+ item.remainH = calculateRemainH(item);
|
|
|
+ }
|
|
|
+ if (item.naturalDateRule === 0) {
|
|
|
+ item.nextMaintenanceDate = calculateNextMaintenanceDate(item);
|
|
|
+ item.remainDay = calculateRemainDay(item);
|
|
|
+ }
|
|
|
+ setupNaturalDateSync(item);
|
|
|
+ return {
|
|
|
+ ...item,
|
|
|
+ group,
|
|
|
+ lastNaturalDate: item.lastNaturalDate,
|
|
|
+ lastMaintenanceDate: item.lastMaintenanceDate
|
|
|
+ ? dayjs(item.lastMaintenanceDate).format("YYYY-MM-DD") // 时间戳 → 日期字符串
|
|
|
+ : null, // 处理空值
|
|
|
+ // 设置consumableRule默认值,如果后端没有返回则默认为0
|
|
|
+ rule: item.rule !== undefined ? Number(item.rule) : 0
|
|
|
+ }
|
|
|
+ })
|
|
|
+ // 设备信息提取逻辑
|
|
|
+ if (list.value.length > 0) {
|
|
|
+ // 从第一个保养项提取设备信息(所有保养项共享相同设备)
|
|
|
+ formData.value.deviceCode = list.value[0].deviceCode;
|
|
|
+ formData.value.deviceName = list.value[0].deviceName;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 查询当前保养工单已经关联的所有物料
|
|
|
+ const materials = await IotMainWorkOrderBomMaterialApi.getWorkOrderBomMaterials(queryParams);
|
|
|
+ materialList.value = []
|
|
|
+ if (Array.isArray(materials)) {
|
|
|
+ materialList.value = materials
|
|
|
+ .map(item => ({
|
|
|
+ ...item,
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ if (list.value.length > 0) {
|
|
|
+ handleBomItemClick(list.value[0]);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('数据加载失败:', error)
|
|
|
+ message.error('数据加载失败,请重试')
|
|
|
+ }
|
|
|
+ nextTick(() => {
|
|
|
+ calculateAllColumnsWidth()
|
|
|
+ window.addEventListener('resize', calculateAllColumnsWidth);
|
|
|
+ })
|
|
|
+
|
|
|
+ // 初始化 保养项 分组信息
|
|
|
+ calculateGroupSpans();
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(async () => {
|
|
|
+ window.removeEventListener('resize', calculateAllColumnsWidth);
|
|
|
+})
|
|
|
+
|
|
|
+// 处理保养项点击事件
|
|
|
+const handleBomItemClick = (item) => {
|
|
|
+ // 移除之前的高亮
|
|
|
+ if (currentBomItem.value) {
|
|
|
+ currentBomItem.value.isSelected = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置当前选中项
|
|
|
+ item.isSelected = true;
|
|
|
+ currentBomItem.value = item;
|
|
|
+ // 切换到当前保养项物料视图
|
|
|
+ showAllMaterials.value = false;
|
|
|
+ // 生成唯一键
|
|
|
+ const uniqueKey = `${item.deviceId}${item.bomNodeId}`;
|
|
|
+
|
|
|
+ // 更新物料列表
|
|
|
+ materialList.value = bomMaterialsMap.value[uniqueKey] || [];
|
|
|
+}
|
|
|
+
|
|
|
+// 为表格行添加点击事件处理
|
|
|
+// 在el-table上添加row-click事件
|
|
|
+// <el-table ... @row-click="handleRowClick">
|
|
|
+const handleRowClick = (row) => {
|
|
|
+ handleBomItemClick(row);
|
|
|
+}
|
|
|
+
|
|
|
+</script>
|
|
|
+<style scoped>
|
|
|
+.base-expandable-content {
|
|
|
+ overflow: hidden; /* 隐藏溢出的内容 */
|
|
|
+ transition: max-height 0.3s ease; /* 平滑过渡效果 */
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-input-number .el-input__inner) {
|
|
|
+ text-align: left !important;
|
|
|
+ padding-left: 10px; /* 保持左侧间距 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 分组容器样式 */
|
|
|
+.form-group {
|
|
|
+ position: relative;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 20px 15px 10px;
|
|
|
+ margin-bottom: 18px;
|
|
|
+ transition: border-color 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+/* 分组标题样式 */
|
|
|
+.group-title {
|
|
|
+ position: absolute;
|
|
|
+ top: -10px;
|
|
|
+ left: 20px;
|
|
|
+ background: white;
|
|
|
+ padding: 0 8px;
|
|
|
+ color: #606266;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.error-text {
|
|
|
+ color: #f56c6c;
|
|
|
+ font-size: 12px;
|
|
|
+ margin-top: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table__body) {
|
|
|
+ .el-table__cell {
|
|
|
+ .cell {
|
|
|
+ word-break: break-word;
|
|
|
+ max-width: 600px; /* 最大宽度限制 */
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.full-content-cell {
|
|
|
+ white-space: nowrap; /* 禁止换行 */
|
|
|
+ overflow: visible; /* 允许内容溢出单元格 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 分组表头样式 */
|
|
|
+:deep(.el-table__header) {
|
|
|
+ border: 1px solid #dcdfe6 !important;
|
|
|
+}
|
|
|
+:deep(.el-table__header th) {
|
|
|
+ border-right: 1px solid #dcdfe6 !important;
|
|
|
+ border-bottom: 1px solid #dcdfe6 !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table__header .is-group th) {
|
|
|
+ background-color: #f5f7fa !important;
|
|
|
+ border-bottom: 1px solid #dcdfe6 !important;
|
|
|
+ font-weight: 600;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+:deep(.el-table__header .is-group th::after) {
|
|
|
+ display: none !important;
|
|
|
+}
|
|
|
+/* 分组标题下的子表头单元格 */
|
|
|
+:deep(.el-table__header .el-table__cell:not(.is-group)) {
|
|
|
+ border-top: 1px solid #dcdfe6 !important; /* 添加顶部边框连接分组标题 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 分组行样式 - 符合图片效果 */
|
|
|
+:deep(.el-table .group-row) {
|
|
|
+ background-color: #f8f8f9; /* 轻微的背景色区分 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 分组单元格样式 */
|
|
|
+:deep(.el-table .group-cell) {
|
|
|
+ font-weight: 600; /* 加粗字体 */
|
|
|
+ vertical-align: middle; /* 垂直居中 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 分组第一行单元格样式 - 添加底部边框 */
|
|
|
+:deep(.el-table .group-first-row) {
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+/* 使用伪元素创建更明显的底部边框 */
|
|
|
+:deep(.el-table .group-first-row::after) {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ height: 2px;
|
|
|
+ background-color: #606266; /* 深灰色边框 */
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+/* 调整分组行的高度,使边框更明显 */
|
|
|
+:deep(.el-table .el-table__row.group-first-row) {
|
|
|
+ border-bottom: none; /* 移除默认边框 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 添加选中行高亮样式 - 增强版 */
|
|
|
+:deep(.el-table .highlight-row) {
|
|
|
+ background-color: #d1eaff !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table .highlight-row:hover>td) {
|
|
|
+ background-color: #b8dfff !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table .highlight-row.current-row>td) {
|
|
|
+ background-color: #99ccff !important;
|
|
|
+ border-bottom: 2px solid #409eff !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 增强高亮行样式的特异性,确保覆盖Element UI的默认样式 */
|
|
|
+:deep(.el-table__body tr.highlight-row td) {
|
|
|
+ background-color: #d1eaff !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table__body tr.highlight-row:hover td) {
|
|
|
+ background-color: #d9ecff !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table__body tr.highlight-row.current-row td) {
|
|
|
+ background-color: #c6e2ff !important;
|
|
|
+ border-bottom: 2px solid #409eff !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 针对斑马纹表格的特殊处理 */
|
|
|
+:deep(.el-table--striped .el-table__body tr.highlight-row.el-table__row--striped td) {
|
|
|
+ background-color: #d1eaff !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table--striped .el-table__body tr.highlight-row.el-table__row--striped:hover td) {
|
|
|
+ background-color: #b8dfff !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table--striped .el-table__body tr.highlight-row.el-table__row--striped.current-row td) {
|
|
|
+ background-color: #99ccff !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 物料列表操作区域样式 */
|
|
|
+.material-list-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.material-list-title {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+/* 单价列统一容器样式:强制居左,固定宽度,对齐基准一致 */
|
|
|
+.unit-price-container {
|
|
|
+ width: 100%;
|
|
|
+ text-align: left; /* 强制内容居左 */
|
|
|
+ padding: 0 4px; /* 可选:添加轻微内边距,避免内容贴边 */
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+/* 单价输入框样式:消除默认内边距差异,与静态文本对齐 */
|
|
|
+:deep(.unit-price-input .el-input__inner) {
|
|
|
+ text-align: left !important; /* 覆盖el-input默认居中对齐 */
|
|
|
+ padding-left: 8px !important; /* 统一输入框内边距,与文本缩进匹配 */
|
|
|
+ padding-right: 8px !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 单价静态文本样式:与输入框保持一致的内边距和对齐 */
|
|
|
+.unit-price-text {
|
|
|
+ display: inline-block; /* 转为行内块,支持padding */
|
|
|
+ width: 100%;
|
|
|
+ padding: 4px 8px; /* 与输入框内边距匹配(输入框默认height约32px,padding上下4px) */
|
|
|
+ box-sizing: border-box;
|
|
|
+ vertical-align: middle; /* 确保与输入框垂直对齐 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 禁用物料行的样式 */
|
|
|
+:deep(.disabled-material-row) {
|
|
|
+ background-color: #f5f7fa !important;
|
|
|
+ color: #c0c4cc !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.disabled-material-row:hover>td) {
|
|
|
+ background-color: #f5f7fa !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.disabled-material-row .el-input-number.is-disabled) {
|
|
|
+ opacity: 0.6;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.disabled-material-row .el-button.is-disabled) {
|
|
|
+ opacity: 0.6;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+/* 新增物料行的样式 */
|
|
|
+:deep(.el-table__body tr.new-material-row) {
|
|
|
+ background-color: #f0f9ff !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table__body tr.new-material-row:hover>td) {
|
|
|
+ background-color: #e6f7ff !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 新增物料输入框样式 */
|
|
|
+:deep(.new-material-row .el-input .el-input__inner) {
|
|
|
+ background-color: white !important;
|
|
|
+ border: 1px solid #dcdfe6 !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.new-material-row .el-input-number .el-input__inner) {
|
|
|
+ background-color: white !important;
|
|
|
+ border: 1px solid #dcdfe6 !important;
|
|
|
+}
|
|
|
+
|
|
|
+</style>
|