|
@@ -0,0 +1,3118 @@
|
|
|
|
|
+<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" disabled/>
|
|
|
|
|
+ </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 disabled>
|
|
|
|
|
+ <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')"
|
|
|
|
|
+ disabled
|
|
|
|
|
+ />
|
|
|
|
|
+ </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')"
|
|
|
|
|
+ disabled
|
|
|
|
|
+ />
|
|
|
|
|
+ </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="其他费用"
|
|
|
|
|
+ disabled
|
|
|
|
|
+ />
|
|
|
|
|
+ </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 ref="mainTableRef"
|
|
|
|
|
+ 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"
|
|
|
|
|
+ highlight-current-row
|
|
|
|
|
+ @current-change="handleCurrentChangeTable">
|
|
|
|
|
+ <!-- 序号列 -->
|
|
|
|
|
+ <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"
|
|
|
|
|
+ disabled
|
|
|
|
|
+ />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+
|
|
|
|
|
+ <el-table-column :label="t('mainPlan.mainStatus')" align="center" width="140">
|
|
|
|
|
+ <template #default="scope">
|
|
|
|
|
+ <div class="status-container">
|
|
|
|
|
+ <el-switch
|
|
|
|
|
+ v-model="scope.row.status"
|
|
|
|
|
+ :active-value="1"
|
|
|
|
|
+ :inactive-value="0"
|
|
|
|
|
+ :disabled="scope.row.initialStatus === 1"
|
|
|
|
|
+ @change="handleStatusChange(scope.row)"
|
|
|
|
|
+ class="status-switch"
|
|
|
|
|
+ />
|
|
|
|
|
+ <span class="status-text">
|
|
|
|
|
+ {{ getStatusText(scope.row) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </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('operationFillForm.mainSumTime')" align="center" prop="mainRuntime" v-if="hasTimeRuleInCurrentPage"
|
|
|
|
|
+ :formatter="erpPriceTableColumnFormatter" :width="columnWidths.mainRuntime">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-tooltip
|
|
|
|
|
+ :disabled="!row.mainRuntimeError"
|
|
|
|
|
+ :content="row.mainRuntimeError"
|
|
|
|
|
+ placement="top"
|
|
|
|
|
+ effect="light"
|
|
|
|
|
+ popper-class="main-runtime-tooltip"
|
|
|
|
|
+ :show-after="0"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="main-runtime-input-wrapper">
|
|
|
|
|
+ <el-input-number
|
|
|
|
|
+ v-model="row.mainRuntime"
|
|
|
|
|
+ :precision="2"
|
|
|
|
|
+ :min="0"
|
|
|
|
|
+ :controls="false"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ :disabled="row.status === 1"
|
|
|
|
|
+ @change="validateMainRuntime(row)"
|
|
|
|
|
+ @blur="validateMainRuntime(row)"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'is-required-input': row.mainRuntimeError,
|
|
|
|
|
+ 'error-input': row.mainRuntimeError
|
|
|
|
|
+ }"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-tooltip>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column :label="t('operationFillForm.mainSumKil')" align="center" prop="mainMileage" v-if="hasMileageRuleInCurrentPage"
|
|
|
|
|
+ :formatter="erpPriceTableColumnFormatter" :width="columnWidths.mainMileage">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-tooltip
|
|
|
|
|
+ :disabled="!row.mainMileageError"
|
|
|
|
|
+ :content="row.mainMileageError"
|
|
|
|
|
+ placement="top"
|
|
|
|
|
+ effect="light"
|
|
|
|
|
+ popper-class="main-runtime-tooltip"
|
|
|
|
|
+ :show-after="0"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="main-runtime-input-wrapper">
|
|
|
|
|
+ <el-input-number
|
|
|
|
|
+ v-model="row.mainMileage"
|
|
|
|
|
+ :precision="2"
|
|
|
|
|
+ :min="0"
|
|
|
|
|
+ :controls="false"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ :disabled="row.status === 1"
|
|
|
|
|
+ @change="validateMainMileage(row)"
|
|
|
|
|
+ @blur="validateMainMileage(row)"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'is-required-input': row.mainMileageError,
|
|
|
|
|
+ 'error-input': row.mainMileageError
|
|
|
|
|
+ }"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-tooltip>
|
|
|
|
|
+ </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 }">
|
|
|
|
|
+ <span :class="{ 'negative-value': row.remainKm != null && row.remainKm < 0 }">
|
|
|
|
|
+ {{ row.remainKm != null ? row.remainKm.toFixed(2) : '-' }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </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 }">
|
|
|
|
|
+ <span :class="{ 'negative-value': row.remainH != null && row.remainH < 0 }">
|
|
|
|
|
+ {{ row.remainH != null ? row.remainH.toFixed(2) : '-' }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </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)"
|
|
|
|
|
+ disabled
|
|
|
|
|
+ />
|
|
|
|
|
+ <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 }">
|
|
|
|
|
+ <span :class="{ 'negative-value': row.remainDay != null && row.remainDay < 0 }">
|
|
|
|
|
+ {{ row.remainDay ?? '-' }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </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.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>
|
|
|
|
|
+
|
|
|
|
|
+ </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 || isButtonsDisabled"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ 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 || isButtonsDisabled"
|
|
|
|
|
+ >
|
|
|
|
|
+ 新增物料
|
|
|
|
|
+ </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%"
|
|
|
|
|
+ :disabled="isMaterialDisabled(row)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <span v-else>{{ row.materialCode }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="物料名称" align="center" prop="materialName" >
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <!-- 新增物料行:显示tooltip和必填红色边框 -->
|
|
|
|
|
+ <el-tooltip
|
|
|
|
|
+ v-if="row.isNew"
|
|
|
|
|
+ effect=""
|
|
|
|
|
+ content=""
|
|
|
|
|
+ placement="top"
|
|
|
|
|
+ :disabled="row.materialName?.trim()"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="row.materialName"
|
|
|
|
|
+ placeholder="请输入物料名称(必填)"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ :class="{ 'is-required-input': row.isNew && !row.materialName?.trim() }"
|
|
|
|
|
+ :disabled="isMaterialDisabled(row)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-tooltip>
|
|
|
|
|
+ <!-- 非新增行:正常显示 -->
|
|
|
|
|
+ <span v-else>{{ row.materialName }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="单位" align="center" prop="unit" width="60px">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-tooltip
|
|
|
|
|
+ v-if="row.isNew"
|
|
|
|
|
+ effect=""
|
|
|
|
|
+ content=""
|
|
|
|
|
+ placement=""
|
|
|
|
|
+ :disabled="row.unit?.trim()"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="row.unit"
|
|
|
|
|
+ placeholder="单位(必填)"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'is-required-input': row.isNew && !row.unit?.trim()
|
|
|
|
|
+ }"
|
|
|
|
|
+ :disabled="isMaterialDisabled(row)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-tooltip>
|
|
|
|
|
+ <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-model="row.unitPrice"
|
|
|
|
|
+ :precision="2"
|
|
|
|
|
+ :min="0"
|
|
|
|
|
+ :controls="false"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ class="unit-price-input"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'is-required-input': row.unitPrice <= 0
|
|
|
|
|
+ }"
|
|
|
|
|
+ :disabled="isMaterialDisabled(row)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </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%"
|
|
|
|
|
+ :class="{ 'is-required-input': row.quantity <= 0 }"
|
|
|
|
|
+ :disabled="isMaterialDisabled(row)"
|
|
|
|
|
+ placeholder="请输入(必填)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <!-- </el-tooltip> -->
|
|
|
|
|
+ </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: 'IotMainWorkOrderModify' })
|
|
|
|
|
+
|
|
|
|
|
+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 mainTableRef = ref(); // 添加表格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);
|
|
|
|
|
+ // 如果保养项不存在,默认不禁用
|
|
|
|
|
+ if (!bomItem) return false;
|
|
|
|
|
+ // 如果保养项状态为已完成(status=1),则禁用该物料
|
|
|
|
|
+ if (bomItem.status === 1) return true;
|
|
|
|
|
+ // 如果保养项rule=1(不消耗物料),则禁用
|
|
|
|
|
+ if (bomItem.rule === 1) return true;
|
|
|
|
|
+ return false;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 物料表格行类名 - 用于设置灰色背景
|
|
|
|
|
+const getMaterialRowClassName = ({ row }) => {
|
|
|
|
|
+ if (row.isNew) {
|
|
|
|
|
+ return 'new-material-row';
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isMaterialDisabled(row)) {
|
|
|
|
|
+ return 'disabled-material-row';
|
|
|
|
|
+ }
|
|
|
|
|
+ return '';
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 计算属性:判断按钮是否应该禁用
|
|
|
|
|
+const isButtonsDisabled = computed(() => {
|
|
|
|
|
+ if (!currentBomItem.value) return true;
|
|
|
|
|
+
|
|
|
|
|
+ // 条件1:保养状态为已完成(status=1)
|
|
|
|
|
+ if (currentBomItem.value.status === 1) return true;
|
|
|
|
|
+
|
|
|
|
|
+ // 条件2:消耗物料字段为不消耗物料(rule=1)
|
|
|
|
|
+ if (currentBomItem.value.rule === 1) return true;
|
|
|
|
|
+
|
|
|
|
|
+ return false;
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 切换显示所有物料/当前保养项物料
|
|
|
|
|
+const toggleShowAllMaterials = () => {
|
|
|
|
|
+ showAllMaterials.value = !showAllMaterials.value;
|
|
|
|
|
+
|
|
|
|
|
+ // 切换显示模式时,需要同步更新 materialList
|
|
|
|
|
+ if (!showAllMaterials.value && currentBomItem.value) {
|
|
|
|
|
+ // 切换到显示当前保养项物料时,使用新的刷新方法
|
|
|
|
|
+ refreshCurrentBomItemMaterials();
|
|
|
|
|
+ } else if (showAllMaterials.value) {
|
|
|
|
|
+ // 切换到显示所有物料
|
|
|
|
|
+ const allMaterials = [];
|
|
|
|
|
+ for (const key in bomMaterialsMap.value) {
|
|
|
|
|
+ allMaterials.push(...bomMaterialsMap.value[key]);
|
|
|
|
|
+ }
|
|
|
|
|
+ materialList.value = allMaterials;
|
|
|
|
|
+ }
|
|
|
|
|
+ console.log('切换显示模式:', {
|
|
|
|
|
+ showAllMaterials: showAllMaterials.value,
|
|
|
|
|
+ materialCount: materialList.value.length
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 分组合并计算逻辑
|
|
|
|
|
+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 // 重置到第一页
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 处理状态变化
|
|
|
|
|
+const handleStatusChange = (row: IotMainWorkOrderBomVO) => {
|
|
|
|
|
+ // 如果是从未完成(0)切换到完成(1)
|
|
|
|
|
+ if (row.status === 1 && row.initialStatus === 0) {
|
|
|
|
|
+ // 校验 mainRuntime
|
|
|
|
|
+ if (!validateMainRuntime(row)) {
|
|
|
|
|
+ message.error(`${row.deviceCode}-${formatMaintItemName(row.name)} ${t('mainPlan.mainRuntimeError')}`);
|
|
|
|
|
+ row.status = 0; // 重置状态
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 校验 mainMileage
|
|
|
|
|
+ if (!validateMainMileage(row)) {
|
|
|
|
|
+ message.error(`${row.deviceCode}-${formatMaintItemName(row.name)} ${t('mainPlan.mainMileageError')}`);
|
|
|
|
|
+ row.status = 0; // 重置状态
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查消耗物料规则:如果设置为不消耗物料(rule=1),则跳过物料校验
|
|
|
|
|
+ if (row.rule === 1) {
|
|
|
|
|
+ console.log(`保养项 ${row.name} 设置为不消耗物料,跳过物料校验`);
|
|
|
|
|
+ message.success(`${row.deviceCode}-${formatMaintItemName(row.name)} 已标记为完成`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 校验物料数据
|
|
|
|
|
+ const isValid = validateBomItemMaterials(row.bomNodeId);
|
|
|
|
|
+
|
|
|
|
|
+ if (!isValid) {
|
|
|
|
|
+ // 获取无效物料信息用于提示
|
|
|
|
|
+ const invalidMaterials = getInvalidMaterialsInfo(row.bomNodeId);
|
|
|
|
|
+ const errorMessage = `请填写物料必填项\n无效物料:\n${invalidMaterials.join('\n')}`;
|
|
|
|
|
+
|
|
|
|
|
+ message.error(errorMessage);
|
|
|
|
|
+
|
|
|
|
|
+ // 重置状态为未完成
|
|
|
|
|
+ row.status = 0;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`保养项 ${row.name} 状态改为: ${row.status === 1 ? '完成' : '未完成'}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 这里可以添加状态变化后的业务逻辑
|
|
|
|
|
+ // 例如:如果状态变为完成,可以自动填充完成时间等
|
|
|
|
|
+ if (row.status === 1) {
|
|
|
|
|
+ // 保养完成时的逻辑
|
|
|
|
|
+ message.success(`${row.deviceCode}-${formatMaintItemName(row.name)} 已标记为完成`);
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 运行时间周期 全局校验方法(在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;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 校验 mainRuntime 的方法
|
|
|
|
|
+const validateMainRuntime = (row: IotMainWorkOrderBomVO) => {
|
|
|
|
|
+ // 清除之前的错误
|
|
|
|
|
+ row.mainRuntimeError = '';
|
|
|
|
|
+
|
|
|
|
|
+ const mainRuntime = Number(row.mainRuntime) || 0;
|
|
|
|
|
+ const totalRunTime = Number(row.totalRunTime ?? row.tempTotalRunTime) || 0;
|
|
|
|
|
+ const lastRunningTime = Number(row.lastRunningTime) || 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果 mainRuntime 为 0 或空,不显示错误(允许为空)如果设置了累计运行时长保养规则 mainRuntime 必填
|
|
|
|
|
+ /* if (!mainRuntime || mainRuntime === 0) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } */
|
|
|
|
|
+
|
|
|
|
|
+ // 校验规则:lastRunningTime ≤ mainRuntime ≤ totalRunTime
|
|
|
|
|
+ if (mainRuntime < lastRunningTime) {
|
|
|
|
|
+ row.mainRuntimeError = `保养时累计运行时间不能小于上次保养时长(${lastRunningTime})`;
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (mainRuntime > totalRunTime) {
|
|
|
|
|
+ row.mainRuntimeError = `保养时累计运行时间不能大于累计运行时间(${totalRunTime})`;
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 校验通过时确保错误信息被清除
|
|
|
|
|
+ row.mainRuntimeError = '';
|
|
|
|
|
+ return true;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 校验 mainMileage 的方法
|
|
|
|
|
+const validateMainMileage = (row: IotMainWorkOrderBomVO) => {
|
|
|
|
|
+ // 清除之前的错误
|
|
|
|
|
+ row.mainMileageError = '';
|
|
|
|
|
+
|
|
|
|
|
+ const mainMileage = Number(row.mainMileage) || 0;
|
|
|
|
|
+ const totalMileage = Number(row.totalMileage ?? row.tempTotalMileage) || 0;
|
|
|
|
|
+ const lastRunningKilometers = Number(row.lastRunningKilometers) || 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果 mainMileage 为 0 或空,不显示错误(允许为空)如果设置了累计运行时长保养规则 mainMileage 必填
|
|
|
|
|
+ /* if (!mainMileage || mainMileage === 0) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } */
|
|
|
|
|
+
|
|
|
|
|
+ // 校验规则:lastRunningKilometers ≤ mainMileage ≤ totalMileage
|
|
|
|
|
+ if (mainMileage < lastRunningKilometers) {
|
|
|
|
|
+ row.mainMileageError = `保养时累计运行公里数不能小于上次保养里程数(${lastRunningKilometers})`;
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (mainMileage > totalMileage) {
|
|
|
|
|
+ row.mainMileageError = `保养时累计运行公里数不能大于累计运行公里数(${totalMileage})`;
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 校验通过时确保错误信息被清除
|
|
|
|
|
+ row.mainMileageError = '';
|
|
|
|
|
+ return true;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 全局校验所有 mainRuntime
|
|
|
|
|
+const validateAllMainRuntimes = (): boolean => {
|
|
|
|
|
+ let isValid = true;
|
|
|
|
|
+ list.value.forEach(row => {
|
|
|
|
|
+ if (!validateMainRuntime(row)) {
|
|
|
|
|
+ isValid = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ return isValid;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 全局校验所有 mainMileage
|
|
|
|
|
+const validateAllMainMileages = (): boolean => {
|
|
|
|
|
+ let isValid = true;
|
|
|
|
|
+ list.value.forEach(row => {
|
|
|
|
|
+ if (!validateMainMileage(row)) {
|
|
|
|
|
+ isValid = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ 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() + Math.random() // 临时ID用于唯一标识
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 生成唯一键
|
|
|
|
|
+ const uniqueKey = `${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('已新增空白物料记录,请填写相关信息');
|
|
|
|
|
+
|
|
|
|
|
+ // 新增物料后重新计算费用
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ calculateTotalCost();
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+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 refreshCurrentBomItemMaterials = () => {
|
|
|
|
|
+ if (!currentBomItem.value) {
|
|
|
|
|
+ materialList.value = [];
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const uniqueKey = `${currentBomItem.value.bomNodeId}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 确保从映射中获取最新数据
|
|
|
|
|
+ const currentMaterials = bomMaterialsMap.value[uniqueKey] || [];
|
|
|
|
|
+
|
|
|
|
|
+ // 强制更新 materialList 的引用,确保响应式更新
|
|
|
|
|
+ materialList.value = [...currentMaterials];
|
|
|
|
|
+
|
|
|
|
|
+ console.log('刷新物料列表:', {
|
|
|
|
|
+ currentBomItem: currentBomItem.value.name,
|
|
|
|
|
+ materialCount: materialList.value.length,
|
|
|
|
|
+ uniqueKey: uniqueKey,
|
|
|
|
|
+ showAllMaterials: showAllMaterials.value
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const selectChoose = (selectedMaterial) => {
|
|
|
|
|
+ // 检查当前是否有选中的保养项
|
|
|
|
|
+ if (!currentBomItem.value) {
|
|
|
|
|
+ message.warning('请先选择一个保养项');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const targetBomNodeId = currentBomItem.value.bomNodeId;
|
|
|
|
|
+ const targetDeviceId = currentBomItem.value.deviceId;
|
|
|
|
|
+
|
|
|
|
|
+ selectedMaterial.bomNodeId = bomNodeId.value
|
|
|
|
|
+ // 关联 bomNodeId
|
|
|
|
|
+ const processedMaterials = selectedMaterial.map(material => ({
|
|
|
|
|
+ ...material,
|
|
|
|
|
+ bomNodeId: targetBomNodeId, // 统一关联当前行的 bomNodeId
|
|
|
|
|
+ deviceId: targetDeviceId // 确保 deviceId 也正确关联
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 生成唯一键
|
|
|
|
|
+ const uniqueKey = `${targetBomNodeId}`;
|
|
|
|
|
+ // 初始化物料映射(如果不存在)
|
|
|
|
|
+ if (!bomMaterialsMap.value[uniqueKey]) {
|
|
|
|
|
+ bomMaterialsMap.value[uniqueKey] = [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 避免重复添加
|
|
|
|
|
+ processedMaterials.forEach(newMaterial => {
|
|
|
|
|
+ // 检查是否已存在相同 工厂+成本中心+库存地点+bomNodeId + materialCode 的条目
|
|
|
|
|
+ const isExist = bomMaterialsMap.value[uniqueKey].some(item =>
|
|
|
|
|
+ item.bomNodeId === bomNodeId.value &&
|
|
|
|
|
+ item.materialCode === newMaterial.materialCode &&
|
|
|
|
|
+ item.factoryId === newMaterial.factoryId &&
|
|
|
|
|
+ item.costCenterId === newMaterial.costCenterId &&
|
|
|
|
|
+ item.storageLocationId === newMaterial.storageLocationId
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (!isExist) {
|
|
|
|
|
+ bomMaterialsMap.value[uniqueKey].push(newMaterial);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 立即更新当前显示的物料列表
|
|
|
|
|
+ refreshCurrentBomItemMaterials();
|
|
|
|
|
+
|
|
|
|
|
+ // 选择物料后立即计算费用
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ calculateTotalCost();
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 计算属性:判断是否需要显示运行时间周期列
|
|
|
|
|
+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 = () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 创建保养项ID到消耗规则的映射
|
|
|
|
|
+ const ruleMap = new Map<number, number>();
|
|
|
|
|
+ list.value.forEach(item => {
|
|
|
|
|
+ ruleMap.set(item.bomNodeId, item.rule);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 从 bomMaterialsMap 中获取所有保养项的所有物料
|
|
|
|
|
+ let allMaterials = [];
|
|
|
|
|
+ for (const key in bomMaterialsMap.value) {
|
|
|
|
|
+ allMaterials = allMaterials.concat(bomMaterialsMap.value[key]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 物料总金额 = ∑(单价 * 消耗数量)
|
|
|
|
|
+ const materialTotal = allMaterials.reduce((sum, item) => {
|
|
|
|
|
+ // 获取物料所属保养项的消耗规则
|
|
|
|
|
+ const rule = ruleMap.get(item.bomNodeId);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果保养项设置为不消耗物料(rule=1),跳过计算
|
|
|
|
|
+ if (rule === 1) return sum;
|
|
|
|
|
+
|
|
|
|
|
+ const price = Number(item.unitPrice) || 0
|
|
|
|
|
+ const quantity = Number(item.quantity) || 0
|
|
|
|
|
+ return sum + (price * quantity)
|
|
|
|
|
+ }, 0)
|
|
|
|
|
+
|
|
|
|
|
+ // 保养费用 = 物料总金额
|
|
|
|
|
+ formData.value.cost = (materialTotal).toFixed(2)
|
|
|
|
|
+
|
|
|
|
|
+ console.log('计算保养费用:', {
|
|
|
|
|
+ 物料总数: allMaterials.length,
|
|
|
|
|
+ 参与计算的物料数: allMaterials.filter(item => {
|
|
|
|
|
+ const rule = ruleMap.get(item.bomNodeId);
|
|
|
|
|
+ return rule !== 1; // 过滤掉不消耗物料的项
|
|
|
|
|
+ }).length,
|
|
|
|
|
+ 总金额: materialTotal
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('计算保养费用错误:', error);
|
|
|
|
|
+ formData.value.cost = '0.00';
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 同时监听物料列表和保养项列表变化
|
|
|
|
|
+watch(
|
|
|
|
|
+ [() => materialList.value, () => list.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) => {
|
|
|
|
|
+ const uniqueKey = `${bomNodeId}`;
|
|
|
|
|
+ const materials = bomMaterialsMap.value[uniqueKey] || [];
|
|
|
|
|
+
|
|
|
|
|
+ // 只计算有效的物料数量
|
|
|
|
|
+ return materials.filter(material =>
|
|
|
|
|
+ !isMaterialDisabled(material) &&
|
|
|
|
|
+ material.materialName &&
|
|
|
|
|
+ material.unit &&
|
|
|
|
|
+ (material.unitPrice || 0) > 0 &&
|
|
|
|
|
+ (material.quantity || 0) > 0
|
|
|
|
|
+ ).length;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 校验保养项下物料数据的方法
|
|
|
|
|
+const validateBomItemMaterials = (bomNodeId: number): boolean => {
|
|
|
|
|
+ const uniqueKey = `${bomNodeId}`;
|
|
|
|
|
+ const materials = bomMaterialsMap.value[uniqueKey] || [];
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有物料数据,直接返回true(允许完成)
|
|
|
|
|
+ if (materials.length === 0) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查所有物料的单价和消耗数量是否都大于0
|
|
|
|
|
+ const hasInvalidMaterial = materials.some(material => {
|
|
|
|
|
+ const unitPrice = Number(material.unitPrice) || 0;
|
|
|
|
|
+ const quantity = Number(material.quantity) || 0;
|
|
|
|
|
+
|
|
|
|
|
+ return unitPrice <= 0 || quantity <= 0;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return !hasInvalidMaterial;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 获取校验失败的物料信息(用于错误提示)
|
|
|
|
|
+const getInvalidMaterialsInfo = (bomNodeId: number): string[] => {
|
|
|
|
|
+ const uniqueKey = `${bomNodeId}`;
|
|
|
|
|
+ const materials = bomMaterialsMap.value[uniqueKey] || [];
|
|
|
|
|
+ const invalidMaterials = [];
|
|
|
|
|
+
|
|
|
|
|
+ materials.forEach(material => {
|
|
|
|
|
+ const unitPrice = Number(material.unitPrice) || 0;
|
|
|
|
|
+ const quantity = Number(material.quantity) || 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (unitPrice <= 0 || quantity <= 0) {
|
|
|
|
|
+ invalidMaterials.push(`${material.materialName || material.materialCode} (单价: ${unitPrice}, 数量: ${quantity})`);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return invalidMaterials;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleDeleteMaterialInTable = (material) => {
|
|
|
|
|
+ // 确认删除对话框
|
|
|
|
|
+ message.confirm('确认删除该物料吗?', { title: '提示' }).then(() => {
|
|
|
|
|
+ const targetBomNodeId = material.bomNodeId;
|
|
|
|
|
+
|
|
|
|
|
+ // 修复:统一使用 bomNodeId 作为键(与新增物料保持一致)
|
|
|
|
|
+ const uniqueKey = `${targetBomNodeId}`;
|
|
|
|
|
+
|
|
|
|
|
+ console.log('删除物料调试信息:', {
|
|
|
|
|
+ material,
|
|
|
|
|
+ uniqueKey,
|
|
|
|
|
+ bomMaterialsMapKeys: Object.keys(bomMaterialsMap.value),
|
|
|
|
|
+ currentMaterials: bomMaterialsMap.value[uniqueKey] || []
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 从 bomMaterialsMap 中删除
|
|
|
|
|
+ if (bomMaterialsMap.value[uniqueKey]) {
|
|
|
|
|
+ let mapIndex = -1;
|
|
|
|
|
+
|
|
|
|
|
+ // 简化查找逻辑
|
|
|
|
|
+ if (material.isNew && material.tempId) {
|
|
|
|
|
+ // 新增物料:通过 tempId 查找
|
|
|
|
|
+ mapIndex = bomMaterialsMap.value[uniqueKey].findIndex(item =>
|
|
|
|
|
+ item.isNew && item.tempId === material.tempId
|
|
|
|
|
+ );
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 已有物料:通过关键字段查找
|
|
|
|
|
+ mapIndex = bomMaterialsMap.value[uniqueKey].findIndex(item =>
|
|
|
|
|
+ !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('物料删除成功');
|
|
|
|
|
+
|
|
|
|
|
+ console.log('删除成功后的物料列表:', bomMaterialsMap.value[uniqueKey]);
|
|
|
|
|
+
|
|
|
|
|
+ // 关键修复:立即更新当前显示的物料列表
|
|
|
|
|
+ refreshCurrentBomItemMaterials();
|
|
|
|
|
+
|
|
|
|
|
+ // 删除物料后重新计算费用
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ calculateTotalCost();
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ message.warning('未找到要删除的物料记录');
|
|
|
|
|
+ console.error('未找到匹配的物料记录:', material);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ message.warning('未找到对应的物料列表');
|
|
|
|
|
+ }
|
|
|
|
|
+ }).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; // 校验失败则终止提交
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 保养时 累计运行时长 mainRuntime 全局校验
|
|
|
|
|
+ if (!validateAllMainRuntimes()) {
|
|
|
|
|
+ message.error(t('mainPlan.mainRuntimeError'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 保养时 累计运行公里数 mainMileage 全局校验
|
|
|
|
|
+ if (!validateAllMainMileages()) {
|
|
|
|
|
+ message.error(t('mainPlan.mainMileageError'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 校验所有设置为完成状态的保养项的物料数据
|
|
|
|
|
+ const invalidBomItems = [];
|
|
|
|
|
+
|
|
|
|
|
+ list.value.forEach(row => {
|
|
|
|
|
+ // 只校验状态为完成(1)且消耗物料规则为消耗(0)的保养项
|
|
|
|
|
+ if (row.status === 1 && row.rule === '0') {
|
|
|
|
|
+ const isValid = validateBomItemMaterials(row.bomNodeId);
|
|
|
|
|
+ if (!isValid) {
|
|
|
|
|
+ invalidBomItems.push({
|
|
|
|
|
+ name: `${row.deviceCode}-${formatMaintItemName(row.name)}`,
|
|
|
|
|
+ invalidMaterials: getInvalidMaterialsInfo(row.bomNodeId)
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 如果有校验失败的保养项,显示错误信息并终止提交
|
|
|
|
|
+ if (invalidBomItems.length > 0) {
|
|
|
|
|
+ let errorMessage = '请填写物料必填项\n\n';
|
|
|
|
|
+
|
|
|
|
|
+ invalidBomItems.forEach((item, index) => {
|
|
|
|
|
+ errorMessage += `${index + 1}. ${item.name}:\n`;
|
|
|
|
|
+ item.invalidMaterials.forEach(material => {
|
|
|
|
|
+ errorMessage += ` - ${material}\n`;
|
|
|
|
|
+ });
|
|
|
|
|
+ errorMessage += '\n';
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ message.error(errorMessage);
|
|
|
|
|
+ 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)
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 从 bomMaterialsMap 获取所有物料数据,而不是 materialList
|
|
|
|
|
+ const allMaterials = [];
|
|
|
|
|
+ for (const key in bomMaterialsMap.value) {
|
|
|
|
|
+ allMaterials.push(...bomMaterialsMap.value[key]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data = {
|
|
|
|
|
+ mainWorkOrder: formData.value,
|
|
|
|
|
+ mainWorkOrderBom: convertedList,
|
|
|
|
|
+ mainWorkOrderMaterials: allMaterials
|
|
|
|
|
+ }
|
|
|
|
|
+ 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: 'mainRuntime',
|
|
|
|
|
+ label: t('operationFillForm.mainSumTime'),
|
|
|
|
|
+ getValue: (row) => row.mainRuntime
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ prop: 'mainMileage',
|
|
|
|
|
+ label: t('operationFillForm.mainSumKil'),
|
|
|
|
|
+ getValue: (row) => row.mainMileage
|
|
|
|
|
+ },
|
|
|
|
|
+ { 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 updateListWithCumulativeData = (cumulativeData: any[]) => {
|
|
|
|
|
+ cumulativeData.forEach(item => {
|
|
|
|
|
+ // 根据 bomNodeId 找到对应的行
|
|
|
|
|
+ const targetRow = list.value.find(row => row.bomNodeId === item.bomNodeId)
|
|
|
|
|
+ if (targetRow) {
|
|
|
|
|
+ // 按照优先级设置 mainRuntime 字段的值 保养时长
|
|
|
|
|
+ // 优先级:mainRuntime > tempMainRunTime
|
|
|
|
|
+ if (item.mainRuntime && Number(item.mainRuntime) > 0) {
|
|
|
|
|
+ // 如果 mainRuntime 有值且大于0,使用 mainRuntime
|
|
|
|
|
+ targetRow.mainRuntime = Number(item.mainRuntime)
|
|
|
|
|
+ } else if (item.tempMainRunTime && Number(item.tempMainRunTime) > 0) {
|
|
|
|
|
+ // 如果 mainRuntime 没有有效值,但 tempMainRunTime 有值且大于0,使用 tempMainRunTime
|
|
|
|
|
+ targetRow.mainRuntime = Number(item.tempMainRunTime)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 两个字段都没有有效值,保持原值或设置为0
|
|
|
|
|
+ targetRow.mainRuntime = 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 按照优先级设置 mainMileage 字段的值 保养里程数
|
|
|
|
|
+ // 优先级:mainMileage > tempMainMileage
|
|
|
|
|
+ if (item.mainMileage && Number(item.mainMileage) > 0) {
|
|
|
|
|
+ // 如果 mainMileage 有值且大于0,使用 mainMileage
|
|
|
|
|
+ targetRow.mainMileage = Number(item.mainMileage)
|
|
|
|
|
+ } else if (item.tempMainMileage && Number(item.tempMainMileage) > 0) {
|
|
|
|
|
+ // 如果 mainMileage 没有有效值,但 tempMainMileage 有值且大于0,使用 tempMainMileage
|
|
|
|
|
+ targetRow.mainMileage = Number(item.tempMainMileage)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 两个字段都没有有效值,保持原值或设置为0
|
|
|
|
|
+ targetRow.mainMileage = 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新数据后立即重新校验,清除可能的错误状态
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ validateMainRuntime(targetRow)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const fetchCumulativeData = async () => {
|
|
|
|
|
+ if (!formData.value.actualStartTime) {
|
|
|
|
|
+ console.warn('actualStartTime is empty, skip fetching cumulative data');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 确保时间戳格式正确
|
|
|
|
|
+ const startTime = Number(formData.value.actualStartTime);
|
|
|
|
|
+ if (isNaN(startTime)) {
|
|
|
|
|
+ console.error('Invalid actualStartTime:', formData.value.actualStartTime);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const queryParams = {
|
|
|
|
|
+ workOrderId: id,
|
|
|
|
|
+ actualStartTime: dayjs(startTime).format('YYYY-MM-DD')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('Fetching cumulative data with params:', queryParams);
|
|
|
|
|
+
|
|
|
|
|
+ const response = await IotMainWorkOrderBomApi.maintenanceCumulativeValue(queryParams)
|
|
|
|
|
+
|
|
|
|
|
+ if (response && Array.isArray(response)) {
|
|
|
|
|
+ updateListWithCumulativeData(response);
|
|
|
|
|
+ // 在所有数据更新完成后,重新校验所有行的 mainRuntime
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ list.value.forEach(row => {
|
|
|
|
|
+ // 校验 保养时累计运行时长
|
|
|
|
|
+ if (row.mainRuntime) {
|
|
|
|
|
+ validateMainRuntime(row);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 校验 保养时累计运行公里数
|
|
|
|
|
+ if (row.mainMileage) {
|
|
|
|
|
+ validateMainMileage(row);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ message.success('累计运行数据更新成功');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取累计运行数据失败:', error);
|
|
|
|
|
+ message.error('获取累计运行数据失败');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 监听保养开始时间变化
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => formData.value.actualStartTime,
|
|
|
|
|
+ async (newVal, oldVal) => {
|
|
|
|
|
+ console.log('actualStartTime changed:', { newVal, oldVal });
|
|
|
|
|
+
|
|
|
|
|
+ if (newVal && newVal !== oldVal) {
|
|
|
|
|
+ // await fetchCumulativeData();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ { immediate: false, deep: true }
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+// 原有代码保持不变,新增“选择物料”按钮的点击处理方法
|
|
|
|
|
+const handleSelectMaterial = () => {
|
|
|
|
|
+ // 校验是否已选中保养项(避免无选中项时点击按钮报错)
|
|
|
|
|
+ if (!currentBomItem.value) {
|
|
|
|
|
+ message.warning('请先在上方表格中选择一个保养项');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 调用现有方法打开物料选择窗口,传入当前选中的保养项
|
|
|
|
|
+ openMaterialForm(currentBomItem.value);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// Element UI表格当前行变化事件
|
|
|
|
|
+const handleCurrentChangeTable = (currentRow) => {
|
|
|
|
|
+ if (currentRow) {
|
|
|
|
|
+ handleBomItemClick(currentRow);
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 监听分页数据和规则变化 - 重新布局表格
|
|
|
|
|
+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) {
|
|
|
|
|
+ // 使用Element UI的方法设置当前行
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ if (mainTableRef.value) {
|
|
|
|
|
+ mainTableRef.value.setCurrentRow(newList[0]);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 选中新分页的第一条
|
|
|
|
|
+ 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}-${formatMaintItemName(row.name)}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 检查消耗物料规则,如果为1(不消耗物料)则跳过校验
|
|
|
|
|
+ if (row.rule === 1) {
|
|
|
|
|
+ return; // 跳过当前保养项的物料校验
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否设置了推迟保养
|
|
|
|
|
+ const hasDelay =
|
|
|
|
|
+ (row.delayKilometers || 0) > 0 ||
|
|
|
|
|
+ (row.delayNaturalDate || 0) > 0 ||
|
|
|
|
|
+ (row.delayDuration || 0) > 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 未设置推迟保养时,需要校验物料
|
|
|
|
|
+ if (!hasDelay) {
|
|
|
|
|
+ // 使用 bomMaterialsMap 获取物料,更可靠
|
|
|
|
|
+ const uniqueKey = `${row.bomNodeId}`;
|
|
|
|
|
+ const materials = bomMaterialsMap.value[uniqueKey] || [];
|
|
|
|
|
+
|
|
|
|
|
+ // 过滤有效的物料(非禁用、非无效状态)
|
|
|
|
|
+ const validMaterials = materials.filter(material =>
|
|
|
|
|
+ material.materialName && // 物料名称不为空
|
|
|
|
|
+ material.unit && // 单位不为空
|
|
|
|
|
+ (material.unitPrice || 0) > 0 && // 单价大于0
|
|
|
|
|
+ (material.quantity || 0) > 0 // 数量大于0
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (validMaterials.length === 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 initialStatus = item.status;
|
|
|
|
|
+
|
|
|
|
|
+ // 提取分组名称
|
|
|
|
|
+ const group = item.name && item.name.includes('->')
|
|
|
|
|
+ ? item.name.split('->')[0].trim()
|
|
|
|
|
+ : '';
|
|
|
|
|
+
|
|
|
|
|
+ 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,
|
|
|
|
|
+ initialStatus, // 保存初始状态
|
|
|
|
|
+ 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);
|
|
|
|
|
+ // 重新初始化物料映射,确保接口数据完全覆盖
|
|
|
|
|
+ const tempBomMaterialsMap = {};
|
|
|
|
|
+ // 首先,用 getWorkOrderBomMaterials 返回的数据初始化映射(主数据源)
|
|
|
|
|
+ if (Array.isArray(materials) && materials.length > 0) {
|
|
|
|
|
+ materials.forEach(material => {
|
|
|
|
|
+ const uniqueKey = `${material.bomNodeId}`;
|
|
|
|
|
+ if (!tempBomMaterialsMap[uniqueKey]) {
|
|
|
|
|
+ tempBomMaterialsMap[uniqueKey] = [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 通过 bomNodeId 在 list 中查找对应的保养项,获取 deviceId
|
|
|
|
|
+ const correspondingItem = list.value.find(item => item.bomNodeId === material.bomNodeId);
|
|
|
|
|
+ const deviceId = correspondingItem ? correspondingItem.deviceId : null;
|
|
|
|
|
+
|
|
|
|
|
+ const materialWithDeviceId = {
|
|
|
|
|
+ ...material,
|
|
|
|
|
+ deviceId: deviceId
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 避免重复添加
|
|
|
|
|
+ const existingIndex = tempBomMaterialsMap[uniqueKey].findIndex(item =>
|
|
|
|
|
+ item.materialCode === material.materialCode &&
|
|
|
|
|
+ item.factoryId === material.factoryId &&
|
|
|
|
|
+ item.costCenterId === material.costCenterId &&
|
|
|
|
|
+ item.storageLocationId === material.storageLocationId
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (existingIndex === -1) {
|
|
|
|
|
+ tempBomMaterialsMap[uniqueKey].push(materialWithDeviceId);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tempBomMaterialsMap[uniqueKey][existingIndex] = materialWithDeviceId;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ // 其次,对于 getWorkOrderBOMs 接口返回的 deviceBomMaterials,
|
|
|
|
|
+ // 只有当 getWorkOrderBomMaterials 中没有对应保养项的物料时,才使用 deviceBomMaterials 作为备用
|
|
|
|
|
+ list.value.forEach(item => {
|
|
|
|
|
+ const uniqueKey = `${item.bomNodeId}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果 getWorkOrderBomMaterials 中没有这个保养项的物料,且 deviceBomMaterials 有值,则使用备用数据
|
|
|
|
|
+ if ((!tempBomMaterialsMap[uniqueKey] || tempBomMaterialsMap[uniqueKey].length === 0) &&
|
|
|
|
|
+ item.deviceBomMaterials && item.deviceBomMaterials.length > 0) {
|
|
|
|
|
+
|
|
|
|
|
+ tempBomMaterialsMap[uniqueKey] = item.deviceBomMaterials.map(material => ({
|
|
|
|
|
+ ...material,
|
|
|
|
|
+ materialName: material.name,
|
|
|
|
|
+ materialCode: material.code,
|
|
|
|
|
+ projectDepartment: material.storageLocation,
|
|
|
|
|
+ totalInventoryQuantity: material.stockQuantity,
|
|
|
|
|
+ deviceId: item.deviceId
|
|
|
|
|
+ }));
|
|
|
|
|
+ } else if (!tempBomMaterialsMap[uniqueKey]) {
|
|
|
|
|
+ // 确保每个保养项在映射中都有对应的数组(即使是空的)
|
|
|
|
|
+ tempBomMaterialsMap[uniqueKey] = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 更新响应式映射
|
|
|
|
|
+ bomMaterialsMap.value = tempBomMaterialsMap;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果有数据,选中第一个保养项并显示其物料
|
|
|
|
|
+ if (list.value.length > 0) {
|
|
|
|
|
+ // 确保第一个保养项的物料正确显示
|
|
|
|
|
+ const firstItem = list.value[0];
|
|
|
|
|
+ const uniqueKey = `${firstItem.bomNodeId}`;
|
|
|
|
|
+ materialList.value = bomMaterialsMap.value[uniqueKey] || [];
|
|
|
|
|
+
|
|
|
|
|
+ // 使用Element UI的方法设置当前行
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ if (mainTableRef.value) {
|
|
|
|
|
+ mainTableRef.value.setCurrentRow(firstItem);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ handleBomItemClick(firstItem);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 页面初始化完成后立即计算费用
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ calculateTotalCost();
|
|
|
|
|
+ });
|
|
|
|
|
+ } 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;
|
|
|
|
|
+
|
|
|
|
|
+ // 使用新的刷新方法
|
|
|
|
|
+ refreshCurrentBomItemMaterials();
|
|
|
|
|
+
|
|
|
|
|
+ // 切换保养项时重新计算费用
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ calculateTotalCost();
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 为表格行添加点击事件处理
|
|
|
|
|
+// 在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; /* 移除默认边框 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 增强高亮行样式的特异性,确保覆盖Element UI的默认样式 */
|
|
|
|
|
+:deep(.el-table__body tr.current-row>td) {
|
|
|
|
|
+ background-color: #d1eaff !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.el-table__body tr.current-row:hover>td) {
|
|
|
|
|
+ background-color: #b8dfff !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;
|
|
|
|
|
+ cursor: not-allowed !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.disabled-material-row:hover>td) {
|
|
|
|
|
+ background-color: #f5f7fa !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.disabled-material-row .el-input.is-disabled .el-input__inner) {
|
|
|
|
|
+ background-color: #f5f7fa !important;
|
|
|
|
|
+ color: #c0c4cc !important;
|
|
|
|
|
+ cursor: not-allowed !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.disabled-material-row .el-input-number.is-disabled) {
|
|
|
|
|
+ opacity: 0.6;
|
|
|
|
|
+ cursor: not-allowed !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.disabled-material-row .el-button.is-disabled) {
|
|
|
|
|
+ opacity: 0.6;
|
|
|
|
|
+ cursor: not-allowed !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 新增物料行的样式 */
|
|
|
|
|
+: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;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 必填输入框红色边框样式(含hover状态) */
|
|
|
|
|
+:deep(.is-required-input .el-input__inner) {
|
|
|
|
|
+ border-color: #f56c6c !important; /* Element错误色 */
|
|
|
|
|
+ box-shadow: 0 0 0 1px rgba(245, 108, 108, 0.4) !important; /* 错误阴影 */
|
|
|
|
|
+}
|
|
|
|
|
+:deep(.is-required-input .el-input-number__input),
|
|
|
|
|
+:deep(.error-input .el-input-number__input) {
|
|
|
|
|
+ border-color: #f56c6c !important;
|
|
|
|
|
+ background-color: #fef0f0 !important;
|
|
|
|
|
+ box-shadow: 0 0 0 1px rgba(245, 108, 108, 0.4) !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 鼠标悬停时保持红色边框(覆盖Element默认hover样式) */
|
|
|
|
|
+:deep(.is-required-input .el-input__inner:hover) {
|
|
|
|
|
+ border-color: #f56c6c !important;
|
|
|
|
|
+}
|
|
|
|
|
+:deep(.is-required-input .el-input-number__input:hover),
|
|
|
|
|
+:deep(.error-input .el-input-number__input:hover) {
|
|
|
|
|
+ border-color: #f56c6c !important;
|
|
|
|
|
+ background-color: #fef0f0 !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.is-required-input .el-input-number__input:focus),
|
|
|
|
|
+:deep(.error-input .el-input-number__input:focus) {
|
|
|
|
|
+ border-color: #f56c6c !important;
|
|
|
|
|
+ background-color: #fef0f0 !important;
|
|
|
|
|
+ box-shadow: 0 0 0 1px rgba(245, 108, 108, 0.2) !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 状态列容器样式 - 水平排列 */
|
|
|
|
|
+.status-container {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 8px; /* 设置开关和文本之间的间距 */
|
|
|
|
|
+ white-space: nowrap; /* 防止换行 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 状态开关样式 */
|
|
|
|
|
+.status-switch {
|
|
|
|
|
+ flex-shrink: 0; /* 防止开关被压缩 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 状态列样式优化 */
|
|
|
|
|
+.status-text {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ min-width: 40px; /* 为文本设置最小宽度,确保对齐 */
|
|
|
|
|
+ text-align: left;
|
|
|
|
|
+ flex-shrink: 0; /* 防止文本被压缩 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 禁用状态的switch样式 */
|
|
|
|
|
+:deep(.el-switch.is-disabled) {
|
|
|
|
|
+ opacity: 0.6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 已完成状态的特殊样式 */
|
|
|
|
|
+:deep(.status-completed .el-switch__core) {
|
|
|
|
|
+ background-color: #67c23a;
|
|
|
|
|
+ border-color: #67c23a;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 保养中状态的样式 */
|
|
|
|
|
+:deep(.status-maintaining .el-switch__core) {
|
|
|
|
|
+ background-color: #409eff;
|
|
|
|
|
+ border-color: #409eff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 延迟状态的样式 */
|
|
|
|
|
+:deep(.status-delayed .el-switch__core) {
|
|
|
|
|
+ background-color: #e6a23c;
|
|
|
|
|
+ border-color: #e6a23c;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 自定义淡红色背景的 tooltip */
|
|
|
|
|
+:deep(.main-runtime-tooltip) {
|
|
|
|
|
+ background: #fef0f0 !important;
|
|
|
|
|
+ border: 1px solid #fbc4c4 !important;
|
|
|
|
|
+ color: #f56c6c !important;
|
|
|
|
|
+ max-width: 300px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 隐藏 Tooltip 菱形箭头(核心需求) */
|
|
|
|
|
+:deep(.main-runtime-tooltip .el-tooltip__arrow),
|
|
|
|
|
+:deep(.main-runtime-tooltip .el-tooltip__arrow::before) {
|
|
|
|
|
+ display: none !important; /* 完全隐藏箭头及伪元素 */
|
|
|
|
|
+ width: 0 !important;
|
|
|
|
|
+ height: 0 !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 新增包装层样式,确保tooltip正确触发 */
|
|
|
|
|
+.main-runtime-input-wrapper {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 负数值红色样式 */
|
|
|
|
|
+:deep(.negative-value) {
|
|
|
|
|
+ color: #f56c6c !important;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 确保在表格单元格中正确显示 */
|
|
|
|
|
+:deep(.el-table .cell .negative-value) {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|