| | |
| | | placeholder="请输入" |
| | | @change="handleQuery" |
| | | clearable |
| | | prefix-icon="Search" /> |
| | | prefix-icon="Search"/> |
| | | </div> |
| | | <div class="search-item"> |
| | | <el-button type="primary" |
| | | @click="handleQuery">搜索</el-button> |
| | | @click="handleQuery">搜索 |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | :page="page" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination"> |
| | | <template #completionStatus="{ row }"> |
| | | <el-progress :percentage="toProgressPercentage(row?.completionStatus)" :color="progressColor(toProgressPercentage(row?.completionStatus))" :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" /> |
| | | </template> |
| | | </PIMTable> |
| | | <template #completionStatus="{ row }"> |
| | | <el-progress :percentage="toProgressPercentage(row?.completionStatus)" |
| | | :color="progressColor(toProgressPercentage(row?.completionStatus))" |
| | | :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''"/> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | <el-dialog v-model="editDialogVisible" |
| | | title="编辑时间" |
| | |
| | | type="date" |
| | | placeholder="请选择" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 300px" /> |
| | | style="width: 300px"/> |
| | | </el-form-item> |
| | | <el-form-item label="计划结束时间"> |
| | | <el-date-picker v-model="editrow.planEndTime" |
| | | type="date" |
| | | placeholder="请选择" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 300px" /> |
| | | style="width: 300px"/> |
| | | </el-form-item> |
| | | <el-form-item label="实际开始时间"> |
| | | <el-date-picker v-model="editrow.actualStartTime" |
| | | type="date" |
| | | placeholder="请选择" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 300px" /> |
| | | style="width: 300px"/> |
| | | </el-form-item> |
| | | <el-form-item label="实际结束时间"> |
| | | <el-date-picker v-model="editrow.actualEndTime" |
| | | type="date" |
| | | placeholder="请选择" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 300px" /> |
| | | style="width: 300px"/> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | |
| | | </div> |
| | | <!-- <div class="info-item"> |
| | | <span class="info-label">工单状态</span> |
| | | <span class="info-value">{{ |
| | | transferCardRowData.status === 1 ? '待确认' : |
| | | transferCardRowData.status === 2 ? '待生产' : |
| | | transferCardRowData.status === 3 ? '生产中' : |
| | | transferCardRowData.status === 4 ? '已生产' : |
| | | transferCardRowData.status |
| | | <span class="info-value">{{ |
| | | transferCardRowData.status === 1 ? '待确认' : |
| | | transferCardRowData.status === 2 ? '待生产' : |
| | | transferCardRowData.status === 3 ? '生产中' : |
| | | transferCardRowData.status === 4 ? '已生产' : |
| | | transferCardRowData.status |
| | | }}</span> |
| | | </div> --> |
| | | |
| | | <div class="info-item"> |
| | | <span class="info-label">计划开始时间</span> |
| | | <span class="info-value">{{ transferCardRowData.planStartTime }}</span> |
| | |
| | | <div class="qr-container"> |
| | | <img :src="transferCardQrUrl" |
| | | alt="流转卡二维码" |
| | | style="width: 200px; height: 200px;" /> |
| | | style="width: 200px; height: 200px;"/> |
| | | <!-- <div class="qr-tip" |
| | | style="margin-top: 10px; text-align: center;">流转卡二维码</div> --> |
| | | </div> |
| | |
| | | margin-bottom: 40px;"> |
| | | <el-button type="primary" |
| | | style="margin-top: 20px;" |
| | | @click="printTransferCard">打印流转卡</el-button> |
| | | @click="printTransferCard">打印流转卡 |
| | | </el-button> |
| | | </div> |
| | | </el-dialog> |
| | | <el-dialog v-model="reportDialogVisible" |
| | | title="报工" |
| | | width="500px"> |
| | | <el-form :model="reportForm" |
| | | label-width="120px"> |
| | | <el-form-item label="待生产数量"> |
| | | <el-input v-model="reportForm.planQuantity" |
| | | readonly |
| | | style="width: 300px" /> |
| | | </el-form-item> |
| | | <el-form-item label="本次生产数量"> |
| | | <el-input v-model.number="reportForm.quantity" |
| | | type="number" |
| | | min="1" |
| | | style="width: 300px" |
| | | placeholder="请输入本次生产数量" /> |
| | | </el-form-item> |
| | | <el-form-item label="班组信息"> |
| | | <el-select v-model="reportForm.userId" |
| | | style="width: 300px" |
| | | placeholder="请选择班组信息" |
| | | clearable |
| | | filterable |
| | | @change="handleUserChange"> |
| | | <el-option v-for="user in userOptions" |
| | | :key="user.userId" |
| | | :label="user.userName" |
| | | :value="user.userId" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | :title="`报工(工单编号:${currentReportRowData?.workOrderNo || '-'})`" |
| | | width="1000px"> |
| | | <el-form |
| | | ref="reportFormRef" |
| | | :model="reportForm" |
| | | :rules="reportFormRules" |
| | | label-width="120px" |
| | | > |
| | | <el-row :gutter="20"> |
| | | |
| | | <el-col :span="12"> |
| | | <el-form-item label="待生产数量"> |
| | | <el-input v-model="reportForm.planQuantity" readonly disabled/> |
| | | </el-form-item> |
| | | </el-col> |
| | | |
| | | <el-col :span="12"> |
| | | <el-form-item label="本次生产数量" prop="quantity"> |
| | | <el-input |
| | | v-model.number="reportForm.quantity" |
| | | type="number" |
| | | min="1" |
| | | step="1" |
| | | placeholder="请输入本次生产数量" |
| | | style="width: 100%" |
| | | @input="handleQuantityInput" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | |
| | | <el-col :span="12"> |
| | | <el-form-item label="补产数量" prop="replenishQty"> |
| | | <el-input |
| | | v-model.number="reportForm.replenishQty" |
| | | type="number" |
| | | min="0" |
| | | step="1" |
| | | placeholder="请输入补产数量" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | |
| | | <el-col :span="12"> |
| | | <el-form-item label="报废数量" prop="scrapQty"> |
| | | <el-input |
| | | v-model.number="reportForm.scrapQty" |
| | | type="number" |
| | | min="0" |
| | | step="1" |
| | | placeholder="请输入报废数量" |
| | | @input="handleScrapQtyInput" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | |
| | | <el-col :span="12"> |
| | | <el-form-item label="班组信息" prop="teamList"> |
| | | <el-select |
| | | v-model="reportForm.teamList" |
| | | multiple |
| | | filterable |
| | | clearable |
| | | collapse-tags |
| | | value-key="userId" |
| | | placeholder="请选择班组成员" |
| | | > |
| | | <el-option |
| | | v-for="user in userTeamOptions" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="{ userId: user.userId, userName: user.nickName }" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | |
| | | <el-col :span="12"> |
| | | <el-form-item label="机台" prop="deviceId"> |
| | | <el-select |
| | | v-model="reportForm.deviceId" |
| | | placeholder="请选择机台" |
| | | filterable |
| | | clearable |
| | | @change="(val) => handleDeviceChange(val)" |
| | | :disabled="isDetail" |
| | | > |
| | | <el-option |
| | | v-for="item in deviceOptions" |
| | | :key="item.id" |
| | | :label="item.deviceName" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | |
| | | <el-col :span="12"> |
| | | <el-form-item label="审核人" prop="auditUserId"> |
| | | <el-select |
| | | v-model="reportForm.auditUserId" |
| | | placeholder="请选择审核人" |
| | | clearable |
| | | filterable |
| | | @change="handleReviewerIdChange" |
| | | > |
| | | <el-option |
| | | v-for="user in userOptions" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | |
| | | </el-row> |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <el-dialog |
| | | v-model="auditDialogVisible" |
| | | title="审核" |
| | | width="1000px" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <el-table :data="auditTableData" border style="width: 100%" v-loading="auditLoading"> |
| | | <el-table-column label="产品名称" prop="productName" min-width="140" show-overflow-tooltip/> |
| | | <el-table-column label="规格" prop="model" min-width="120" show-overflow-tooltip/> |
| | | <el-table-column label="单位" prop="unit" width="80"/> |
| | | <el-table-column label="工序名称" prop="processName" min-width="120" show-overflow-tooltip/> |
| | | <el-table-column label="需求数量" prop="planQuantity" width="110"/> |
| | | <el-table-column label="完成数量" prop="completeQuantity" width="110"/> |
| | | <el-table-column label="完成进度" prop="completionStatus" width="140"> |
| | | <template #default="{ row }"> |
| | | <el-progress |
| | | :percentage="toProgressPercentage(row?.completionStatus)" |
| | | :color="progressColor(toProgressPercentage(row?.completionStatus))" |
| | | :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="计划开始时间" prop="planStartTime" width="140"/> |
| | | <el-table-column label="计划结束时间" prop="planEndTime" width="140"/> |
| | | </el-table> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button type="primary" :loading="auditLoading" @click="submitAudit(1)">通过</el-button> |
| | | <el-button type="danger" :loading="auditLoading" @click="submitAudit(2)">不通过</el-button> |
| | | <el-button :disabled="auditLoading" @click="auditDialogVisible = false">取消</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <el-dialog v-model="scheduleDialogVisible" |
| | | :title="`生产排产(工单编号:${currentReportRowData?.workOrderNo || '-'})`" |
| | | width="1000px" |
| | | :close-on-click-modal="false"> |
| | | <div class="schedule-panel"> |
| | | <el-row style="margin-bottom: 12px;"> |
| | | <el-col> |
| | | <el-button type="primary" plain :disabled="scheduleLoading || scheduleSaving" @click="addScheduleRow"> |
| | | 新增一行 |
| | | </el-button> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-table :data="scheduleRows" border style="width: 100%" v-loading="scheduleLoading"> |
| | | <el-table-column type="index" label="序号" width="70" align="center" /> |
| | | |
| | | <el-table-column label="本次上机机台" min-width="220"> |
| | | <template #default="{ row }"> |
| | | <el-select |
| | | v-model="row.deviceId" |
| | | placeholder="请选择机台" |
| | | filterable |
| | | clearable |
| | | style="width: 100%" |
| | | :disabled="scheduleSaving" |
| | | @change="val => handleScheduleDeviceChange(val, row)" |
| | | > |
| | | <el-option |
| | | v-for="item in deviceOptions" |
| | | :key="item.id" |
| | | :label="item.deviceName" |
| | | :value="String(item.id)" |
| | | /> |
| | | </el-select> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="本次上机人" min-width="220"> |
| | | <template #default="{ row }"> |
| | | <el-select |
| | | v-model="row.userIds" |
| | | placeholder="请选择上机人" |
| | | filterable |
| | | multiple |
| | | clearable |
| | | collapse-tags |
| | | style="width: 100%" |
| | | :disabled="scheduleSaving" |
| | | @change="val => handleScheduleUserChange(val, row)" |
| | | > |
| | | <el-option |
| | | v-for="user in row.userOptions" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="String(user.userId)" |
| | | /> |
| | | </el-select> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="本次上机时间" min-width="240"> |
| | | <template #default="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.startTime" |
| | | type="datetime" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | placeholder="请选择上机时间" |
| | | style="width: 100%" |
| | | :disabled="scheduleSaving" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="操作" width="110" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | link |
| | | type="danger" |
| | | :loading="row.deleting" |
| | | :disabled="scheduleSaving" |
| | | @click="removeScheduleRow(row)" |
| | | > |
| | | 删除 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <Pagination |
| | | v-show="schedulePage.total > 0" |
| | | style="margin-top: 12px" |
| | | :total="schedulePage.total" |
| | | :page="schedulePage.current" |
| | | :limit="schedulePage.size" |
| | | @pagination="handleSchedulePagination" |
| | | /> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button type="primary" :loading="scheduleSaving" @click="handleSaveSchedule">保存排产</el-button> |
| | | <el-button :disabled="scheduleSaving" @click="scheduleDialogVisible = false">取消</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <FilesDia ref="workOrderFilesRef"/> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, ref } from "vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import dayjs from "dayjs"; |
| | | import { |
| | | productWorkOrderPage, |
| | | updateProductWorkOrder, |
| | | addProductMain, |
| | | } from "@/api/productionManagement/workOrder.js"; |
| | | import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | import QRCode from "qrcode"; |
| | | import { getCurrentInstance, reactive, toRefs } from "vue"; |
| | | const { proxy } = getCurrentInstance(); |
| | | import {onMounted, ref, nextTick, computed} from "vue"; |
| | | import {deepClone} from "@/utils/index.js" |
| | | import {ElMessageBox, ElMessage} from "element-plus"; |
| | | import Pagination from "@/components/PIMTable/Pagination.vue"; |
| | | import dayjs from "dayjs"; |
| | | import { |
| | | productWorkOrderPage, |
| | | updateProductWorkOrder, |
| | | addProductMain, |
| | | downProductWorkOrder, |
| | | addProductionMachineRecord, |
| | | productionMachineRecordListPage, |
| | | deleteProductionMachineRecord |
| | | } from "@/api/productionManagement/workOrder.js"; |
| | | import {getUserProfile, userListNoPageByTenantId} from "@/api/system/user.js"; |
| | | import QRCode from "qrcode"; |
| | | import {getCurrentInstance, reactive, toRefs} from "vue"; |
| | | import FilesDia from "./components/filesDia.vue"; |
| | | import {getDeviceLedger} from "@/api/equipmentManagement/ledger.js"; |
| | | |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "工单编号", |
| | | prop: "workOrderNo", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "生产订单号", |
| | | prop: "productOrderNpsNo", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "产品名称", |
| | | prop: "productName", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "规格", |
| | | prop: "model", |
| | | }, |
| | | { |
| | | label: "单位", |
| | | prop: "unit", |
| | | }, |
| | | { |
| | | label: "工序名称", |
| | | prop: "processName", |
| | | }, |
| | | { |
| | | label: "需求数量", |
| | | prop: "planQuantity", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "完成数量", |
| | | prop: "completeQuantity", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "完成进度", |
| | | prop: "completionStatus", |
| | | dataType: "slot", |
| | | slot: "completionStatus", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "计划开始时间", |
| | | prop: "planStartTime", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "计划结束时间", |
| | | prop: "planEndTime", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "实际开始时间", |
| | | prop: "actualStartTime", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "实际结束时间", |
| | | prop: "actualEndTime", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "操作", |
| | | width: "200", |
| | | align: "center", |
| | | dataType: "action", |
| | | fixed: "right", |
| | | operation: [ |
| | | { |
| | | name: "编辑", |
| | | clickFun: row => { |
| | | handleEdit(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "流转卡", |
| | | clickFun: row => { |
| | | showTransferCard(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "报工", |
| | | clickFun: row => { |
| | | showReportDialog(row); |
| | | }, |
| | | disabled: row => row.planQuantity <= 0, |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | const qrCodeUrl = ref(""); |
| | | const qrRowData = ref(null); |
| | | const editDialogVisible = ref(false); |
| | | const transferCardVisible = ref(false); |
| | | const transferCardData = ref([]); |
| | | const transferCardQrUrl = ref(""); |
| | | const transferCardRowData = ref(null); |
| | | const reportDialogVisible = ref(false); |
| | | const userOptions = ref([]); |
| | | const reportForm = reactive({ |
| | | planQuantity: 0, |
| | | quantity: 0, |
| | | userName: "", |
| | | workOrderId: "", |
| | | reportWork: "", |
| | | productProcessRouteItemId: "", |
| | | userId: "", |
| | | productMainId: null, |
| | | }); |
| | | const currentReportRowData = ref(null); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | total: 0, |
| | | }); |
| | | const {proxy} = getCurrentInstance(); |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | workOrderNo: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | const toProgressPercentage = val => { |
| | | const n = Number(val); |
| | | if (!Number.isFinite(n)) return 0; |
| | | if (n <= 0) return 0; |
| | | if (n >= 100) return 100; |
| | | return Math.round(n); |
| | | const currentUserId = ref(""); |
| | | const deviceOptions = ref([]) |
| | | const currentUserName = ref(""); |
| | | |
| | | const ensureCurrentUser = async () => { |
| | | if (currentUserId.value) return; |
| | | try { |
| | | const res = await getUserProfile(); |
| | | if (res?.code === 200) { |
| | | currentUserId.value = String(res?.data?.userId ?? ""); |
| | | currentUserName.value = String(res?.data?.nickName ?? ""); |
| | | } |
| | | } catch (err) { |
| | | console.error("获取用户信息失败", err); |
| | | } |
| | | }; |
| | | |
| | | // 机台获取 |
| | | const getDeviceList = () => { |
| | | getDeviceLedger().then(res => { |
| | | deviceOptions.value = Array.isArray(res?.data) ? res.data : [] |
| | | }) |
| | | } |
| | | |
| | | const handleDeviceChange = (val) => { |
| | | const device = deviceOptions.value.find(item => item.id === val) |
| | | reportForm.deviceName = device?.deviceName || "" |
| | | reportForm.deviceId = val || "" |
| | | } |
| | | |
| | | const normalizeArray = (val) => { |
| | | if (val === null || val === undefined) return []; |
| | | if (Array.isArray(val)) return val; |
| | | if (typeof val === "string") { |
| | | return val |
| | | .split(/[,,;;\s]+/g) |
| | | .map((s) => s.trim()) |
| | | .filter(Boolean); |
| | | } |
| | | return [val]; |
| | | }; |
| | | |
| | | const isCurrentUserReportWorker = (row) => { |
| | | const uid = String(currentUserId.value || ""); |
| | | if (!uid) return false; |
| | | if (!row) return false; |
| | | |
| | | const candidateIds = [ |
| | | row.reportUserIds, |
| | | row.reportWorkerIds, |
| | | row.userIdList, |
| | | row.reportUserId, |
| | | row.userId, |
| | | ] |
| | | .flatMap((v) => normalizeArray(v)) |
| | | .map((v) => String(v)) |
| | | .filter(Boolean); |
| | | |
| | | if (candidateIds.includes(uid)) return true; |
| | | |
| | | const candidateNames = [ |
| | | row.userNames, |
| | | row.reportUserNames, |
| | | row.reportWorkerNames, |
| | | row.userName, |
| | | ] |
| | | .flatMap((v) => normalizeArray(v)) |
| | | .map((v) => String(v)) |
| | | .filter(Boolean); |
| | | |
| | | if (currentUserName.value && candidateNames.includes(currentUserName.value)) { |
| | | return true; |
| | | } |
| | | |
| | | if (Array.isArray(row.reportWorkerList)) { |
| | | const list = row.reportWorkerList |
| | | .map((item) => String(item?.userId ?? item?.id ?? "")) |
| | | .filter(Boolean); |
| | | if (list.includes(uid)) return true; |
| | | const nameList = row.reportWorkerList |
| | | .map((item) => String(item?.userName ?? item?.nickName ?? "")) |
| | | .filter(Boolean); |
| | | if (currentUserName.value && nameList.includes(currentUserName.value)) return true; |
| | | } |
| | | |
| | | return false; |
| | | }; |
| | | |
| | | const canOperateByReportWorker = computed(() => { |
| | | return (row) => isCurrentUserReportWorker(row); |
| | | }); |
| | | |
| | | const isRowScheduled = (row) => { |
| | | const ids = normalizeArray(row?.userIds) |
| | | .map((val) => String(val)) |
| | | .filter(Boolean); |
| | | if (!ids.length) return false; |
| | | return ids.some((val) => val !== "0"); |
| | | }; |
| | | |
| | | const buildBaseScheduleUsersByRow = (row) => { |
| | | if (!row) return []; |
| | | |
| | | if (Array.isArray(row?.reportWorkerList) && row.reportWorkerList.length > 0) { |
| | | const mapped = row.reportWorkerList |
| | | .map((item) => { |
| | | const userId = String(item?.userId ?? item?.id ?? "").trim(); |
| | | const nickName = String(item?.userName ?? item?.nickName ?? "").trim(); |
| | | return { userId, nickName: nickName || userId }; |
| | | }) |
| | | .filter((item) => item.userId); |
| | | const uniq = new Map(); |
| | | mapped.forEach((item) => uniq.set(String(item.userId), item)); |
| | | return Array.from(uniq.values()); |
| | | } |
| | | |
| | | const configuredIds = [ |
| | | row.reportUserIds, |
| | | row.reportWorkerIds, |
| | | row.userIdList, |
| | | row.reportUserId, |
| | | row.userId, |
| | | ] |
| | | .flatMap((v) => normalizeArray(v)) |
| | | .map((v) => String(v).trim()) |
| | | .filter(Boolean); |
| | | |
| | | if (configuredIds.length > 0) { |
| | | const uniqIds = Array.from(new Set(configuredIds)); |
| | | return uniqIds.map((id) => { |
| | | const user = userTeamOptions.value.find((u) => String(u.userId) === String(id)); |
| | | return { userId: String(id), nickName: user?.nickName || String(id) }; |
| | | }); |
| | | } |
| | | |
| | | return userTeamOptions.value.map((u) => ({ |
| | | userId: String(u.userId), |
| | | nickName: u.nickName, |
| | | })); |
| | | }; |
| | | |
| | | const resolveScheduleUserName = (userId) => { |
| | | const uid = String(userId ?? "").trim(); |
| | | if (!uid) return ""; |
| | | const inBase = baseScheduleUsers.value.find((u) => String(u.userId) === uid); |
| | | if (inBase?.nickName) return inBase.nickName; |
| | | const inTeam = userTeamOptions.value.find((u) => String(u.userId) === uid); |
| | | return inTeam?.nickName || uid; |
| | | }; |
| | | |
| | | const buildScheduleUserOptionsByDeviceId = (deviceId) => { |
| | | const device = deviceOptions.value.find((item) => String(item.id) === String(deviceId)); |
| | | |
| | | const operatorIds = device?.operatorId |
| | | ? String(device.operatorId) |
| | | .split(/[,,;;\s]+/g) |
| | | .map((id) => id.trim()) |
| | | .filter(Boolean) |
| | | : []; |
| | | |
| | | if (!operatorIds.length) { |
| | | return [...baseScheduleUsers.value]; |
| | | } |
| | | |
| | | return baseScheduleUsers.value.filter((user) => |
| | | operatorIds.includes(String(user.userId)) |
| | | ); |
| | | }; |
| | | |
| | | const createScheduleRow = (preset = {}) => { |
| | | const deviceId = |
| | | preset?.deviceId === null || preset?.deviceId === undefined |
| | | ? "" |
| | | : String(preset.deviceId); |
| | | |
| | | const userOptions = deviceId |
| | | ? buildScheduleUserOptionsByDeviceId(deviceId) |
| | | : [...baseScheduleUsers.value]; |
| | | |
| | | const userIds = normalizeArray(preset?.userIds) |
| | | .map((val) => String(val)) |
| | | .filter(Boolean) |
| | | .filter((uid) => userOptions.some((user) => String(user.userId) === String(uid))); |
| | | |
| | | return { |
| | | id: preset?.id ?? "", |
| | | deviceId, |
| | | deviceName: preset?.deviceName ?? "", |
| | | userIds, |
| | | userOptions, |
| | | startTime: preset?.startTime ?? dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | deleting: false, |
| | | }; |
| | | const progressColor = percentage => { |
| | | const p = toProgressPercentage(percentage); |
| | | if (p < 30) return "#f56c6c"; |
| | | if (p < 50) return "#e6a23c"; |
| | | if (p < 80) return "#409eff"; |
| | | return "#67c23a"; |
| | | }; |
| | | let editrow = ref(null); |
| | | }; |
| | | |
| | | // 查询列表 |
| | | /** 搜索按钮操作 */ |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | const addScheduleRow = (preset) => { |
| | | if (preset) { |
| | | scheduleRows.value.push(createScheduleRow(preset)); |
| | | return; |
| | | } |
| | | |
| | | const first = scheduleRows.value[0] || {}; |
| | | const deviceId = first.deviceId || currentReportRowData.value?.deviceId || ""; |
| | | const userOptions = deviceId ? buildScheduleUserOptionsByDeviceId(deviceId) : [...baseScheduleUsers.value]; |
| | | const defaultUserId = userOptions[0]?.userId ? String(userOptions[0].userId) : ""; |
| | | |
| | | scheduleRows.value.push( |
| | | createScheduleRow({ |
| | | id: "", |
| | | deviceId, |
| | | deviceName: first.deviceName || currentReportRowData.value?.deviceName || "", |
| | | userIds: defaultUserId ? [defaultUserId] : [], |
| | | startTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }) |
| | | ); |
| | | }; |
| | | |
| | | const refreshScheduleRows = async () => { |
| | | const workOrderRow = currentReportRowData.value; |
| | | |
| | | if (!workOrderRow?.id) { |
| | | schedulePage.current = 1; |
| | | schedulePage.total = 0; |
| | | scheduleRows.value = [createScheduleRow({})]; |
| | | return; |
| | | } |
| | | |
| | | scheduleLoading.value = true; |
| | | try { |
| | | const res = await productionMachineRecordListPage({ |
| | | workOrderId: workOrderRow.id, |
| | | current: schedulePage.current, |
| | | size: schedulePage.size, |
| | | }); |
| | | |
| | | const records = Array.isArray(res?.data?.records) ? res.data.records : []; |
| | | const apiTotal = Number(res?.data?.total); |
| | | schedulePage.total = |
| | | Number.isFinite(apiTotal) && apiTotal > 0 |
| | | ? apiTotal |
| | | : records.length; |
| | | |
| | | const lastPage = Math.max(1, Math.ceil((schedulePage.total || 0) / schedulePage.size)); |
| | | if (schedulePage.current > lastPage) { |
| | | schedulePage.current = lastPage; |
| | | await refreshScheduleRows(); |
| | | return; |
| | | } |
| | | |
| | | const rows = buildScheduleRowsFromRecords(records); |
| | | |
| | | scheduleRows.value = rows.length > 0 ? rows : [createScheduleRow({})]; |
| | | } catch (error) { |
| | | console.error("获取排产记录失败", error); |
| | | schedulePage.total = 0; |
| | | scheduleRows.value = [createScheduleRow({})]; |
| | | ElMessage.error("获取排产记录失败"); |
| | | } finally { |
| | | scheduleLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const removeScheduleRow = async (row) => { |
| | | if (!row) return; |
| | | |
| | | if (!row.id) { |
| | | scheduleRows.value = scheduleRows.value.filter((item) => item !== row); |
| | | if (!scheduleRows.value.length) { |
| | | scheduleRows.value = [createScheduleRow({})]; |
| | | } |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | await ElMessageBox.confirm("确定删除这条排产记录吗?", "提示", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | }); |
| | | } catch { |
| | | return; |
| | | } |
| | | |
| | | row.deleting = true; |
| | | try { |
| | | const res = await deleteProductionMachineRecord([row.id]); |
| | | if (res?.code !== undefined && res.code !== 200) { |
| | | ElMessage.error(res?.msg || "删除失败"); |
| | | return; |
| | | } |
| | | |
| | | ElMessage.success("删除成功"); |
| | | await refreshScheduleRows(); |
| | | getList(); |
| | | } catch (error) { |
| | | console.error("删除排产记录失败", error); |
| | | ElMessage.error("删除失败,请重试"); |
| | | } finally { |
| | | row.deleting = false; |
| | | } |
| | | }; |
| | | |
| | | const handleScheduleUserChange = (userIds, row) => { |
| | | row.userIds = normalizeArray(userIds).map((val) => String(val)).filter(Boolean); |
| | | }; |
| | | |
| | | const handleScheduleDeviceChange = (deviceId, row) => { |
| | | const device = deviceOptions.value.find((item) => String(item.id) === String(deviceId)); |
| | | |
| | | row.deviceId = deviceId === null || deviceId === undefined ? "" : String(deviceId); |
| | | row.deviceName = device?.deviceName || ""; |
| | | row.userOptions = row.deviceId ? buildScheduleUserOptionsByDeviceId(row.deviceId) : [...baseScheduleUsers.value]; |
| | | |
| | | row.userIds = normalizeArray(row.userIds) |
| | | .map((uid) => String(uid)) |
| | | .filter((uid) => row.userOptions.some((user) => String(user.userId) === String(uid))); |
| | | }; |
| | | |
| | | const handleSchedulePagination = ({page, limit}) => { |
| | | schedulePage.current = page; |
| | | schedulePage.size = limit; |
| | | refreshScheduleRows(); |
| | | }; |
| | | |
| | | const validateScheduleRows = () => { |
| | | if (!scheduleRows.value.length) { |
| | | ElMessage.warning("请至少添加一条上机信息"); |
| | | return false; |
| | | } |
| | | |
| | | for (let index = 0; index < scheduleRows.value.length; index += 1) { |
| | | const row = scheduleRows.value[index]; |
| | | |
| | | if (!row.deviceId) { |
| | | ElMessage.warning(`第${index + 1}行请选择机台`); |
| | | return false; |
| | | } |
| | | |
| | | if (!Array.isArray(row.userIds) || row.userIds.length === 0) { |
| | | ElMessage.warning(`第${index + 1}行请选择上机人`); |
| | | return false; |
| | | } |
| | | |
| | | if (!row.startTime) { |
| | | ElMessage.warning(`第${index + 1}行请选择上机时间`); |
| | | return false; |
| | | } |
| | | |
| | | if (!dayjs(row.startTime).isValid()) { |
| | | ElMessage.warning(`第${index + 1}行上机时间格式不正确`); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | |
| | | return true; |
| | | }; |
| | | |
| | | const buildMachineRecordPayload = (workOrderRow, scheduleRow, sortIndex = 0) => { |
| | | const processId = |
| | | workOrderRow?.processId ?? |
| | | workOrderRow?.productProcessRouteItemId ?? |
| | | reportForm.productProcessRouteItemId; |
| | | |
| | | const operatorIds = normalizeArray(scheduleRow?.userIds) |
| | | .map((val) => String(val).trim()) |
| | | .filter(Boolean) |
| | | .join(","); |
| | | |
| | | const nickName = normalizeArray(scheduleRow?.userIds) |
| | | .map((uid) => resolveScheduleUserName(uid)) |
| | | .filter(Boolean) |
| | | .join(","); |
| | | |
| | | const payload = { |
| | | workOrderId: workOrderRow?.id, |
| | | processId, |
| | | machineId: scheduleRow.deviceId ? Number(scheduleRow.deviceId) : undefined, |
| | | deviceName: scheduleRow.deviceName, |
| | | operatorId: operatorIds || undefined, |
| | | nickName: nickName || "", |
| | | machineStartTime: scheduleRow.startTime, |
| | | reportStatus: false, |
| | | remark: `排产序号:${sortIndex + 1}`, |
| | | }; |
| | | const pagination = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | |
| | | if (scheduleRow.id) { |
| | | payload.id = scheduleRow.id; |
| | | } |
| | | |
| | | return payload; |
| | | }; |
| | | |
| | | const mapMachineRecordToScheduleRow = (record) => { |
| | | const id = record?.id ?? ""; |
| | | const deviceId = record?.machineId ?? record?.deviceId ?? ""; |
| | | const deviceName = record?.deviceName ?? record?.machineName ?? ""; |
| | | const startTime = record?.machineStartTime ?? record?.startTime ?? ""; |
| | | const userIds = normalizeArray(record?.operatorId ?? record?.operatorIds ?? record?.userId) |
| | | .map((val) => String(val)) |
| | | .filter(Boolean); |
| | | |
| | | return createScheduleRow({ |
| | | id, |
| | | deviceId: deviceId === null || deviceId === undefined ? "" : String(deviceId), |
| | | deviceName, |
| | | userIds, |
| | | startTime, |
| | | }); |
| | | }; |
| | | |
| | | const buildScheduleRowsFromRecords = (records) => { |
| | | const list = Array.isArray(records) ? records : []; |
| | | const grouped = new Map(); |
| | | |
| | | list.forEach((record) => { |
| | | const row = mapMachineRecordToScheduleRow(record); |
| | | const key = `${row.deviceId}__${row.startTime}__${row.deviceName}`; |
| | | |
| | | if (!grouped.has(key)) { |
| | | grouped.set(key, row); |
| | | return; |
| | | } |
| | | |
| | | const existing = grouped.get(key); |
| | | existing.ids = Array.from(new Set([existing.id, row.id].filter(Boolean))); |
| | | existing.userIds = Array.from( |
| | | new Set([...(existing?.userIds || []), ...(row?.userIds || [])].map((v) => String(v))) |
| | | ).filter(Boolean); |
| | | |
| | | if (!existing.deviceName && row.deviceName) existing.deviceName = row.deviceName; |
| | | }); |
| | | |
| | | return Array.from(grouped.values()).sort( |
| | | (a, b) => dayjs(a.startTime).valueOf() - dayjs(b.startTime).valueOf() |
| | | ); |
| | | }; |
| | | |
| | | const openScheduleDialog = async (row) => { |
| | | currentReportRowData.value = row; |
| | | baseScheduleUsers.value = buildBaseScheduleUsersByRow(row); |
| | | userTemp.value = [...baseScheduleUsers.value]; |
| | | schedulePage.current = 1; |
| | | scheduleRows.value = []; |
| | | scheduleDialogVisible.value = true; |
| | | |
| | | await refreshScheduleRows(); |
| | | }; |
| | | |
| | | const handleSaveSchedule = async () => { |
| | | if (scheduleSaving.value) return; |
| | | if (!validateScheduleRows()) return; |
| | | |
| | | const workOrderRow = currentReportRowData.value; |
| | | if (!workOrderRow?.id) { |
| | | ElMessage.warning("缺少工单信息,无法保存排产"); |
| | | return; |
| | | } |
| | | |
| | | const sortedRows = [...scheduleRows.value].sort((a, b) => dayjs(a.startTime).valueOf() - dayjs(b.startTime).valueOf()); |
| | | |
| | | scheduleSaving.value = true; |
| | | try { |
| | | const productionMachineRecord = sortedRows.map((scheduleRow, index) => |
| | | buildMachineRecordPayload(workOrderRow, scheduleRow, index) |
| | | ); |
| | | |
| | | const res = await addProductionMachineRecord({productionMachineRecord}); |
| | | if (res?.code !== undefined && res.code !== 200) { |
| | | ElMessage.error(res?.msg || "保存排产失败"); |
| | | return; |
| | | } |
| | | |
| | | proxy.$modal.msgSuccess("排产已保存"); |
| | | await refreshScheduleRows(); |
| | | getList(); |
| | | }; |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | const params = { ...searchForm.value, ...page }; |
| | | productWorkOrderPage(params) |
| | | } catch (error) { |
| | | console.error("保存排产失败", error); |
| | | ElMessage.error("保存排产失败,请重试"); |
| | | } finally { |
| | | scheduleSaving.value = false; |
| | | } |
| | | }; |
| | | |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "工单类型", |
| | | prop: "workOrderType", |
| | | width: "80", |
| | | }, |
| | | { |
| | | label: "工单编号", |
| | | prop: "workOrderNo", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "生产订单号", |
| | | prop: "productOrderNpsNo", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "产品名称", |
| | | prop: "productName", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "规格", |
| | | prop: "model", |
| | | }, |
| | | { |
| | | label: "单位", |
| | | prop: "unit", |
| | | }, |
| | | { |
| | | label: "工序名称", |
| | | prop: "processName", |
| | | }, |
| | | { |
| | | label: "需求数量", |
| | | prop: "planQuantity", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "完成数量", |
| | | prop: "completeQuantity", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "完成进度", |
| | | prop: "completionStatus", |
| | | dataType: "slot", |
| | | slot: "completionStatus", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "计划开始时间", |
| | | prop: "planStartTime", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "计划结束时间", |
| | | prop: "planEndTime", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "实际开始时间", |
| | | prop: "actualStartTime", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "实际结束时间", |
| | | prop: "actualEndTime", |
| | | width: "140", |
| | | }, |
| | | { |
| | | label: "操作", |
| | | width: "200", |
| | | align: "center", |
| | | dataType: "action", |
| | | fixed: "right", |
| | | operation: [ |
| | | { |
| | | name: "编辑", |
| | | clickFun: row => { |
| | | handleEdit(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "流转卡", |
| | | clickFun: row => { |
| | | downloadAndPrintWorkOrder(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "附件", |
| | | clickFun: row => { |
| | | openWorkOrderFiles(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "报工", |
| | | clickFun: row => { |
| | | showReportDialog(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "生产排产", |
| | | clickFun: row => { |
| | | openScheduleDialog(row); |
| | | }, |
| | | }, |
| | | // { |
| | | // name:"审核", |
| | | // color: "#f56c6c", |
| | | // clickFun: row => { |
| | | // handleAudit(row); |
| | | // }, |
| | | // disabled: row => Number(row?.auditStatus) === 1, |
| | | // } |
| | | ], |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | const scheduleRows = ref([]); |
| | | const scheduleLoading = ref(false); |
| | | const scheduleSaving = ref(false); |
| | | const schedulePage = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | const editDialogVisible = ref(false); |
| | | const transferCardVisible = ref(false); |
| | | const transferCardQrUrl = ref(""); |
| | | const scheduleDialogVisible = ref(false); |
| | | const transferCardRowData = ref(null); |
| | | const baseScheduleUsers = ref([]); |
| | | const userTemp = ref([]); |
| | | const reportDialogVisible = ref(false); |
| | | const auditDialogVisible = ref(false); |
| | | const auditRowData = ref(null); |
| | | const auditTableData = ref([]); |
| | | const auditLoading = ref(false); |
| | | const workOrderFilesRef = ref(null); |
| | | const reportFormRef = ref(null); |
| | | const userOptions = ref([]); |
| | | const userTeamOptions = ref([]); |
| | | const reportForm = reactive({ |
| | | planQuantity: 0, |
| | | quantity: null, |
| | | scrapQty: null, |
| | | startTime: "", |
| | | endTime: "", |
| | | userName: "", |
| | | workOrderId: "", |
| | | reportWork: "", |
| | | productProcessRouteItemId: "", |
| | | userId: "", |
| | | productMainId: null, |
| | | teamList: [], |
| | | deviceId: null, |
| | | }); |
| | | |
| | | // 本次生产数量验证规则 |
| | | const validateQuantity = (rule, value, callback) => { |
| | | if (value === null || value === undefined || value === "") { |
| | | callback(new Error("请输入本次生产数量")); |
| | | return; |
| | | } |
| | | const num = Number(value); |
| | | // 整数且大于等于1 |
| | | if (isNaN(num) || !Number.isInteger(num) || num < 1) { |
| | | callback(new Error("本次生产数量必须大于等于1")); |
| | | return; |
| | | } |
| | | callback(); |
| | | }; |
| | | |
| | | // 报废数量验证规则 |
| | | const validateScrapQty = (rule, value, callback) => { |
| | | if (value === null || value === undefined || value === "") { |
| | | callback(); |
| | | return; |
| | | } |
| | | const num = Number(value); |
| | | // 整数且大于等于0 |
| | | if (isNaN(num) || !Number.isInteger(num) || num < 0) { |
| | | callback(new Error("报废数量必须大于等于0")); |
| | | return; |
| | | } |
| | | callback(); |
| | | }; |
| | | |
| | | // 审核 |
| | | const handleAudit = (row) => { |
| | | if (Number(row?.auditStatus) === 1) { |
| | | ElMessage.warning("该工单已审核"); |
| | | return; |
| | | } |
| | | auditRowData.value = row; |
| | | const workOrderNo = row?.workOrderNo; |
| | | const related = workOrderNo |
| | | ? tableData.value.filter(r => r?.workOrderNo === workOrderNo) |
| | | : []; |
| | | auditTableData.value = related.length > 0 ? related : [row]; |
| | | auditDialogVisible.value = true; |
| | | }; |
| | | |
| | | const submitAudit = async (result) => { |
| | | const current = auditRowData.value; |
| | | if (!current) return; |
| | | if (auditLoading.value) return; |
| | | |
| | | const confirmText = result === 1 ? "确定审核通过吗?" : "确定审核不通过吗?"; |
| | | try { |
| | | await ElMessageBox.confirm(confirmText, "提示", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | }); |
| | | } catch { |
| | | return; |
| | | } |
| | | |
| | | auditLoading.value = true; |
| | | try { |
| | | const updates = auditTableData.value.map(item => { |
| | | const id = item?.id; |
| | | if (!id) return Promise.resolve(); |
| | | return updateProductWorkOrder({id, auditStatus: result}); |
| | | }); |
| | | await Promise.all(updates); |
| | | ElMessage.success("审核成功"); |
| | | auditDialogVisible.value = false; |
| | | getList(); |
| | | } finally { |
| | | auditLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // 查看详情 |
| | | const handleView = (row) => { |
| | | const {workOrderId} = row; |
| | | router.push({ |
| | | path: "/productionManagement/workOrderDetail", |
| | | query: {workOrderId}, |
| | | }); |
| | | } |
| | | |
| | | // 验证规则 |
| | | const reportFormRules = { |
| | | quantity: [{required: true, validator: validateQuantity, trigger: "blur"}], |
| | | scrapQty: [{validator: validateScrapQty, trigger: "blur"}], |
| | | startTime: [{required: true, message: "请选择开始时间", trigger: "change"}], |
| | | endTime: [{required: true, message: "请选择结束时间", trigger: "change"}], |
| | | auditUserId: [{required: true, message: "请选择审核人", trigger: "change"}], |
| | | teamList: [{required: true, message: "请选择班组", trigger: "change"}], |
| | | deviceId: [{required: true, message: "请选择设备", trigger: "change"}], |
| | | }; |
| | | |
| | | // 处理本次生产数量输入,限制必须大于等于1 |
| | | const handleQuantityInput = value => { |
| | | if (value === "" || value === null || value === undefined) { |
| | | reportForm.quantity = null; |
| | | return; |
| | | } |
| | | const num = Number(value); |
| | | if (isNaN(num)) { |
| | | return; |
| | | } |
| | | // 如果小于1,清除 |
| | | if (num < 1) { |
| | | reportForm.quantity = null; |
| | | return; |
| | | } |
| | | // 如果是小数取整数部分 |
| | | if (!Number.isInteger(num)) { |
| | | const intValue = Math.floor(num); |
| | | // 如果取整后小于1,清除 |
| | | if (intValue < 1) { |
| | | reportForm.quantity = null; |
| | | return; |
| | | } |
| | | reportForm.quantity = intValue; |
| | | return; |
| | | } |
| | | reportForm.quantity = num; |
| | | }; |
| | | |
| | | // 处理报废数量 |
| | | const handleScrapQtyInput = value => { |
| | | if (value === "" || value === null || value === undefined) { |
| | | reportForm.scrapQty = null; |
| | | return; |
| | | } |
| | | const num = Number(value); |
| | | // 如果是NaN,保持原值 |
| | | if (isNaN(num)) { |
| | | return; |
| | | } |
| | | // 如果是负数,清除输入 |
| | | if (num < 0) { |
| | | reportForm.scrapQty = null; |
| | | return; |
| | | } |
| | | // 如果是小数,取整数部分 |
| | | if (!Number.isInteger(num)) { |
| | | reportForm.scrapQty = Math.floor(num); |
| | | return; |
| | | } |
| | | // 有效的非负整数(包括0) |
| | | reportForm.scrapQty = num; |
| | | }; |
| | | const currentReportRowData = ref(null); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | total: 0, |
| | | }); |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | workOrderNo: "", |
| | | }, |
| | | }); |
| | | const {searchForm} = toRefs(data); |
| | | const toProgressPercentage = val => { |
| | | const n = Number(val); |
| | | if (!Number.isFinite(n)) return 0; |
| | | if (n <= 0) return 0; |
| | | if (n >= 100) return 100; |
| | | return Math.round(n); |
| | | }; |
| | | const progressColor = percentage => { |
| | | const p = toProgressPercentage(percentage); |
| | | if (p < 30) return "#f56c6c"; |
| | | if (p < 50) return "#e6a23c"; |
| | | if (p < 80) return "#409eff"; |
| | | return "#67c23a"; |
| | | }; |
| | | let editrow = ref(null); |
| | | |
| | | // 查询列表 |
| | | /** 搜索按钮操作 */ |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | const pagination = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | const params = {...searchForm.value, ...page}; |
| | | productWorkOrderPage(params) |
| | | .then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records; |
| | |
| | | .catch(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | }; |
| | | |
| | | const showTransferCard = async row => { |
| | | transferCardRowData.value = row; |
| | | const qrContent = String(row.id); |
| | | // 下载并打印工单流转卡(文件流) |
| | | const downloadAndPrintWorkOrder = async row => { |
| | | if (!row || !row.id) { |
| | | proxy.$modal.msgError("缺少工单ID,无法下载流转卡"); |
| | | return; |
| | | } |
| | | const fileName = row.workOrderNo |
| | | ? `工单流转卡_${row.workOrderNo}.xlsx` |
| | | : "工单流转卡.xlsx"; |
| | | try { |
| | | // 调用接口,以 responseType: 'blob' 获取文件流 |
| | | const blob = await downProductWorkOrder(row.id); |
| | | |
| | | transferCardQrUrl.value = await QRCode.toDataURL(qrContent); |
| | | transferCardVisible.value = true; |
| | | }; |
| | | if (!blob) { |
| | | proxy.$modal.msgError("未获取到流转卡文件"); |
| | | return; |
| | | } |
| | | |
| | | const printTransferCard = () => { |
| | | window.print(); |
| | | }; |
| | | // 创建 Blob URL |
| | | const fileBlob = |
| | | blob instanceof Blob |
| | | ? blob |
| | | : new Blob([blob], {type: blob.type || "application/octet-stream"}); |
| | | const url = window.URL.createObjectURL(fileBlob); |
| | | |
| | | const handleEdit = row => { |
| | | editrow.value = JSON.parse(JSON.stringify(row)); |
| | | editDialogVisible.value = true; |
| | | }; |
| | | // 创建隐藏 iframe,用于触发浏览器打印 |
| | | const iframe = document.createElement("iframe"); |
| | | iframe.style.position = "fixed"; |
| | | iframe.style.right = "0"; |
| | | iframe.style.bottom = "0"; |
| | | iframe.style.width = "0"; |
| | | iframe.style.height = "0"; |
| | | iframe.style.border = "0"; |
| | | iframe.src = url; |
| | | document.body.appendChild(iframe); |
| | | |
| | | const handleUpdate = () => { |
| | | updateProductWorkOrder(editrow.value) |
| | | iframe.onload = () => { |
| | | try { |
| | | iframe.contentWindow?.focus(); |
| | | iframe.contentWindow?.print(); |
| | | } catch (e) { |
| | | console.error("自动调用打印失败", e); |
| | | // 退而求其次,打开新窗口由用户手动打印 |
| | | window.open(url); |
| | | } |
| | | }; |
| | | } catch (e) { |
| | | console.error("下载工单流转卡失败", e); |
| | | proxy.$modal.msgError("下载工单流转卡失败"); |
| | | } |
| | | }; |
| | | |
| | | const showTransferCard = async row => { |
| | | transferCardRowData.value = row; |
| | | const qrContent = String(row.id); |
| | | |
| | | transferCardQrUrl.value = await QRCode.toDataURL(qrContent); |
| | | transferCardVisible.value = true; |
| | | }; |
| | | |
| | | const printTransferCard = () => { |
| | | window.print(); |
| | | }; |
| | | |
| | | const handleEdit = row => { |
| | | // if (!isCurrentUserReportWorker(row)) { |
| | | // ElMessage.warning("当前用户不是该工单的报工人,无法编辑"); |
| | | // return; |
| | | // } |
| | | editrow.value = JSON.parse(JSON.stringify(row)); |
| | | editDialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleUpdate = () => { |
| | | updateProductWorkOrder(editrow.value) |
| | | .then(res => { |
| | | proxy.$modal.msgSuccess("提交成功"); |
| | | editDialogVisible.value = false; |
| | |
| | | confirmButtonText: "确定", |
| | | }); |
| | | }); |
| | | }; |
| | | }; |
| | | |
| | | const showReportDialog = row => { |
| | | currentReportRowData.value = row; |
| | | reportForm.planQuantity = row.planQuantity; |
| | | reportForm.quantity = row.quantity; |
| | | reportForm.productProcessRouteItemId = row.productProcessRouteItemId; |
| | | reportForm.workOrderId = row.id; |
| | | reportForm.reportWork = row.reportWork; |
| | | reportForm.productMainId = row.productMainId; |
| | | // 获取当前登录用户信息,设置为默认选中 |
| | | getUserProfile() |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | reportForm.userId = res.data.userId; |
| | | reportForm.userName = res.data.userName; |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("获取用户信息失败", err); |
| | | }); |
| | | const showReportDialog = row => { |
| | | // if (!isCurrentUserReportWorker(row)) { |
| | | // ElMessage.warning("当前用户不是该工单的报工人,无法报工"); |
| | | // return; |
| | | // } |
| | | currentReportRowData.value = row; |
| | | reportForm.planQuantity = row.planQuantity - row.completeQuantity; |
| | | reportForm.quantity = |
| | | row.quantity !== undefined && row.quantity !== null ? row.quantity : null; |
| | | reportForm.productProcessRouteItemId = row.productProcessRouteItemId; |
| | | reportForm.workOrderId = row.id; |
| | | reportForm.reportWork = row.reportWork; |
| | | reportForm.productMainId = row.productMainId; |
| | | reportForm.startTime = ""; |
| | | reportForm.endTime = ""; |
| | | reportForm.replenishQty = 0; |
| | | reportForm.teamList = []; |
| | | reportForm.scrapQty = 0; |
| | | |
| | | reportDialogVisible.value = true; |
| | | }; |
| | | nextTick(() => { |
| | | reportFormRef.value?.clearValidate(); |
| | | }); |
| | | ensureCurrentUser().then(() => { |
| | | reportForm.userId = currentUserId.value; |
| | | reportForm.userName = currentUserName.value; |
| | | }); |
| | | |
| | | const handleReport = () => { |
| | | reportDialogVisible.value = true; |
| | | }; |
| | | |
| | | const openWorkOrderFiles = row => { |
| | | workOrderFilesRef.value?.openDialog(row); |
| | | }; |
| | | |
| | | const handleReport = () => { |
| | | reportFormRef.value?.validate(valid => { |
| | | if (!valid) { |
| | | return false; |
| | | } |
| | | |
| | | if (reportForm.planQuantity <= 0) { |
| | | ElMessageBox.alert("待生产数量为0,无法报工", "提示", { |
| | | confirmButtonText: "确定", |
| | | }); |
| | | return; |
| | | } |
| | | if (!reportForm.quantity || reportForm.quantity <= 0) { |
| | | ElMessageBox.alert("请输入有效的本次生产数量", "提示", { |
| | | |
| | | // 验证本次生产数量 |
| | | if ( |
| | | reportForm.quantity === null || |
| | | reportForm.quantity === undefined || |
| | | reportForm.quantity === "" |
| | | ) { |
| | | ElMessageBox.alert("请输入本次生产数量", "提示", { |
| | | confirmButtonText: "确定", |
| | | }); |
| | | return; |
| | | } |
| | | if (reportForm.quantity > reportForm.planQuantity) { |
| | | |
| | | const quantity = Number(reportForm.quantity); |
| | | const scrapQty = |
| | | reportForm.scrapQty === null || |
| | | reportForm.scrapQty === undefined || |
| | | reportForm.scrapQty === "" |
| | | ? 0 |
| | | : Number(reportForm.scrapQty); |
| | | |
| | | // 本次生产数量 |
| | | if (isNaN(quantity) || !Number.isInteger(quantity) || quantity < 1) { |
| | | ElMessageBox.alert("本次生产数量必须大于等于1", "提示", { |
| | | confirmButtonText: "确定", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // 报废数量必须是整数且大于等于0 |
| | | if (isNaN(scrapQty) || !Number.isInteger(scrapQty) || scrapQty < 0) { |
| | | ElMessageBox.alert("报废数量必须大于等于0", "提示", { |
| | | confirmButtonText: "确定", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | if (quantity > reportForm.planQuantity) { |
| | | ElMessageBox.alert("本次生产数量不能超过待生产数量", "提示", { |
| | | confirmButtonText: "确定", |
| | | }); |
| | | return; |
| | | } |
| | | // console.log(reportForm); |
| | | addProductMain(reportForm).then(res => { |
| | | |
| | | if (!reportForm.startTime || !reportForm.endTime) { |
| | | ElMessageBox.alert("开始时间和结束时间不能为空", "提示", { |
| | | confirmButtonText: "确定", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | if (dayjs(reportForm.startTime).isSame(dayjs(reportForm.endTime)) || dayjs(reportForm.startTime).isAfter(dayjs(reportForm.endTime))) { |
| | | ElMessageBox.alert("开始时间必须小于结束时间", "提示", { |
| | | confirmButtonText: "确定", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | const submitData = { |
| | | ...reportForm, |
| | | quantity: quantity, |
| | | scrapQty: scrapQty, |
| | | }; |
| | | addProductMain(submitData).then(res => { |
| | | if (res.code === 200) { |
| | | proxy.$modal.msgSuccess("报工成功"); |
| | | reportDialogVisible.value = false; |
| | |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | }); |
| | | }; |
| | | |
| | | // 获取用户列表 |
| | | const getUserList = () => { |
| | | userListNoPageByTenantId() |
| | | // 获取用户列表 |
| | | const getUserList = () => { |
| | | userListNoPageByTenantId() |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | userOptions.value = res.data || []; |
| | | const list = Array.isArray(res.data) ? res.data : [] |
| | | userOptions.value = [ |
| | | {nickName: "任意用户", userId: "-1"}, |
| | | ...deepClone(list) |
| | | ] |
| | | userTeamOptions.value = deepClone(list) |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("获取用户列表失败", err); |
| | | }); |
| | | }; |
| | | }; |
| | | |
| | | // 用户选择变化时更新 userName |
| | | const handleUserChange = (userId) => { |
| | | if (userId) { |
| | | const selectedUser = userOptions.value.find(user => user.userId === userId); |
| | | if (selectedUser) { |
| | | reportForm.userName = selectedUser.userName; |
| | | } |
| | | } else { |
| | | reportForm.userName = ""; |
| | | // 用户选择变化时更新 userName |
| | | const handleUserChange = userId => { |
| | | if (userId) { |
| | | const selectedUser = userOptions.value.find(user => user.userId === userId); |
| | | if (selectedUser) { |
| | | reportForm.userName = selectedUser.nickName; |
| | | } |
| | | }; |
| | | } else { |
| | | reportForm.userName = ""; |
| | | } |
| | | }; |
| | | // 审核人 |
| | | const handleReviewerIdChange = userId => { |
| | | if (userId) { |
| | | const selectedUser = userOptions.value.find(user => user.userId === userId); |
| | | if (selectedUser) { |
| | | reportForm.auditUserName = selectedUser.nickName; |
| | | } |
| | | } else { |
| | | reportForm.auditUserName = ""; |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | getUserList(); |
| | | }); |
| | | onMounted(() => { |
| | | ensureCurrentUser(); |
| | | getList(); |
| | | getUserList(); |
| | | getDeviceList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .search_form { |
| | | margin-bottom: 20px; |
| | | .search-row { |
| | | display: flex; |
| | | gap: 20px; |
| | | align-items: center; |
| | | .search-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | } |
| | | } |
| | | .search_form { |
| | | margin-bottom: 20px; |
| | | |
| | | .transfer-card-title { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | text-align: center; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .transfer-card-container { |
| | | .search-row { |
| | | display: flex; |
| | | gap: 20px; |
| | | height: 350px; |
| | | .transfer-card-info { |
| | | flex: 1; |
| | | overflow: auto; |
| | | .info-group { |
| | | width: 50%; |
| | | float: left; |
| | | } |
| | | .info-item { |
| | | display: flex; |
| | | margin-bottom: 15px; |
| | | .info-label { |
| | | width: 120px; |
| | | font-weight: bold; |
| | | margin-right: 20px; |
| | | } |
| | | .info-value { |
| | | flex: 1; |
| | | } |
| | | } |
| | | } |
| | | .transfer-card-qr { |
| | | width: 240px; |
| | | align-items: center; |
| | | |
| | | .search-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: flex-start; |
| | | gap: 10px; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .transfer-card-title { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | text-align: center; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .transfer-card-container { |
| | | display: flex; |
| | | gap: 20px; |
| | | height: 350px; |
| | | |
| | | .transfer-card-info { |
| | | flex: 1; |
| | | overflow: auto; |
| | | |
| | | .info-group { |
| | | width: 50%; |
| | | float: left; |
| | | } |
| | | |
| | | .info-item { |
| | | display: flex; |
| | | margin-bottom: 15px; |
| | | |
| | | .info-label { |
| | | width: 120px; |
| | | font-weight: bold; |
| | | margin-right: 20px; |
| | | } |
| | | |
| | | .info-value { |
| | | flex: 1; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .transfer-card-qr { |
| | | width: 240px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: flex-start; |
| | | } |
| | | } |
| | | </style> |
| | | |
| | | <style lang="scss"> |
| | | @media print { |
| | | @page { |
| | | size: landscape; |
| | | <style lang="scss"> |
| | | @media print { |
| | | @page { |
| | | size: landscape; |
| | | } |
| | | body * { |
| | | visibility: hidden; |
| | | } |
| | | .el-dialog__wrapper, |
| | | .el-dialog, |
| | | .el-dialog__body, |
| | | .transfer-card-title, |
| | | .transfer-card-container, |
| | | .transfer-card-container *, |
| | | .info-item, |
| | | .info-label, |
| | | .info-value { |
| | | visibility: visible; |
| | | } |
| | | .print-button-container { |
| | | visibility: hidden; |
| | | } |
| | | .el-dialog__wrapper { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | margin: 0; |
| | | } |
| | | .el-dialog { |
| | | width: 100% !important; |
| | | max-width: 800px; |
| | | margin: 0 auto !important; |
| | | } |
| | | .el-dialog__header, |
| | | .el-dialog__footer { |
| | | display: none; |
| | | } |
| | | .el-dialog__body { |
| | | padding: 20px; |
| | | } |
| | | .transfer-card-container { |
| | | height: auto; |
| | | display: flex; |
| | | gap: 20px; |
| | | } |
| | | .transfer-card-info { |
| | | flex: 1; |
| | | |
| | | .info-group { |
| | | width: 100%; |
| | | float: none; |
| | | margin-bottom: 20px; |
| | | } |
| | | body * { |
| | | visibility: hidden; |
| | | } |
| | | .el-dialog__wrapper, |
| | | .el-dialog, |
| | | .el-dialog__body, |
| | | .transfer-card-title, |
| | | .transfer-card-container, |
| | | .transfer-card-container *, |
| | | .info-item, |
| | | .info-label, |
| | | .info-value { |
| | | visibility: visible; |
| | | } |
| | | .print-button-container { |
| | | visibility: hidden; |
| | | } |
| | | .el-dialog__wrapper { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | margin: 0; |
| | | } |
| | | .el-dialog { |
| | | width: 100% !important; |
| | | max-width: 800px; |
| | | margin: 0 auto !important; |
| | | } |
| | | .el-dialog__header, |
| | | .el-dialog__footer { |
| | | display: none; |
| | | } |
| | | .el-dialog__body { |
| | | padding: 20px; |
| | | } |
| | | .transfer-card-container { |
| | | height: auto; |
| | | |
| | | .info-item { |
| | | display: flex; |
| | | gap: 20px; |
| | | } |
| | | .transfer-card-info { |
| | | flex: 1; |
| | | .info-group { |
| | | width: 100%; |
| | | float: none; |
| | | margin-bottom: 20px; |
| | | margin-bottom: 10px; |
| | | |
| | | .info-label { |
| | | width: 100px; |
| | | font-weight: bold; |
| | | margin-right: 15px; |
| | | white-space: nowrap; |
| | | } |
| | | .info-item { |
| | | display: flex; |
| | | margin-bottom: 10px; |
| | | .info-label { |
| | | width: 100px; |
| | | font-weight: bold; |
| | | margin-right: 15px; |
| | | white-space: nowrap; |
| | | } |
| | | .info-value { |
| | | flex: 1; |
| | | word-break: break-word; |
| | | } |
| | | |
| | | .info-value { |
| | | flex: 1; |
| | | word-break: break-word; |
| | | } |
| | | } |
| | | .transfer-card-qr { |
| | | width: 160px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: flex-start; |
| | | } |
| | | .qr-container img { |
| | | width: 140px !important; |
| | | height: 140px !important; |
| | | } |
| | | } |
| | | .transfer-card-qr { |
| | | width: 160px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: flex-start; |
| | | } |
| | | .qr-container img { |
| | | width: 140px !important; |
| | | height: 140px !important; |
| | | } |
| | | } |
| | | </style> |