| | |
| | | params: { modelType } |
| | | }) |
| | | } |
| | | |
| | | // 获取按人员/班组统计质量数据 |
| | | export function getPersonGroupRateStatistics() { |
| | | return request({ |
| | | url: '/qualityReport/getPersonGroupRateStatistics', |
| | | method: 'get' |
| | | }) |
| | | } |
| | |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | label: "补修理工时", |
| | | prop: "repairWorkHour", |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | label: "生产数量", |
| | | prop: "quantity", |
| | | minWidth: 100, |
| | |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | label: "工资", |
| | | prop: "wages", |
| | | label: "原工资", |
| | | prop: "originalWages", |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | label: "修正工资", |
| | | prop: "adjustAmount", |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | |
| | | <el-upload |
| | | v-model:file-list="fileList" |
| | | class="upload-demo" |
| | | :action="uploadUrl" |
| | | :http-request="uploadFile" |
| | | :on-success="handleUploadSuccess" |
| | | :on-error="handleUploadError" |
| | | :before-upload="beforeUpload" |
| | |
| | | <script setup> |
| | | import { ref, reactive, getCurrentInstance } from "vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import axios from "axios"; |
| | | import { getToken } from "@/utils/auth.js"; |
| | | import PIMTable from "@/components/PIMTable/PIMTable.vue"; |
| | | import filePreview from "@/components/filePreview/index.vue"; |
| | |
| | | Authorization: "Bearer " + getToken(), |
| | | }); |
| | | const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); |
| | | const currentWorkOrderNo = ref(""); |
| | | |
| | | const beforeUpload = file => { |
| | | const isImage = file?.type?.startsWith("image/"); |
| | |
| | | const openDialog = row => { |
| | | dialogVisible.value = true; |
| | | currentWorkOrderId.value = row.id; |
| | | currentWorkOrderNo.value = row.workOrderNo || row.work_order_no || ""; |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | |
| | | proxy.$modal.msgError("文件上传失败"); |
| | | } |
| | | |
| | | const createWatermarkImage = async file => { |
| | | const sourceImage = await loadImage(URL.createObjectURL(file)); |
| | | const canvas = document.createElement("canvas"); |
| | | const width = sourceImage.naturalWidth || sourceImage.width; |
| | | const height = sourceImage.naturalHeight || sourceImage.height; |
| | | canvas.width = width; |
| | | canvas.height = height; |
| | | const ctx = canvas.getContext("2d"); |
| | | ctx.drawImage(sourceImage, 0, 0, width, height); |
| | | |
| | | const watermarkText = [ |
| | | "天津市玮苓乐器有限公司", |
| | | `${new Date().toLocaleString()}`, |
| | | ]; |
| | | |
| | | ctx.save(); |
| | | ctx.globalAlpha = 0.28; |
| | | ctx.fillStyle = "#ffffff"; |
| | | ctx.font = `bold ${Math.max(18, Math.floor(width / 28))}px Arial`; |
| | | ctx.textAlign = "right"; |
| | | ctx.textBaseline = "bottom"; |
| | | const padding = Math.max(24, Math.floor(width / 40)); |
| | | const lineHeight = Math.max(24, Math.floor(width / 20)); |
| | | const x = width - padding; |
| | | const y = height - padding; |
| | | watermarkText.reverse().forEach((text, index) => { |
| | | ctx.fillText(text, x, y - index * lineHeight); |
| | | }); |
| | | ctx.restore(); |
| | | |
| | | const blob = await new Promise(resolve => canvas.toBlob(resolve, file.type || "image/jpeg", 0.92)); |
| | | return new File([blob], file.name, { type: file.type || "image/jpeg" }); |
| | | }; |
| | | |
| | | const loadImage = src => |
| | | new Promise((resolve, reject) => { |
| | | const image = new Image(); |
| | | image.crossOrigin = "anonymous"; |
| | | image.onload = () => resolve(image); |
| | | image.onerror = reject; |
| | | image.src = src; |
| | | }); |
| | | |
| | | const uploadFile = async options => { |
| | | try { |
| | | const watermarkedFile = await createWatermarkImage(options.file); |
| | | const formData = new FormData(); |
| | | formData.append("file", watermarkedFile); |
| | | const response = await axios.post(uploadUrl.value, formData, { |
| | | headers: { |
| | | "Content-Type": "multipart/form-data", |
| | | Authorization: "Bearer " + getToken(), |
| | | }, |
| | | }); |
| | | options.onSuccess(response.data); |
| | | } catch (error) { |
| | | options.onError(error); |
| | | } |
| | | }; |
| | | |
| | | const handleDelete = () => { |
| | | if (selectedRows.value.length === 0) { |
| | | proxy.$modal.msgWarning("请选择数据"); |
| | |
| | | </script> |
| | | |
| | | <style scoped></style> |
| | | |
| | |
| | | <el-upload |
| | | v-model:file-list="fileList" |
| | | class="upload-demo" |
| | | :action="uploadUrl" |
| | | :http-request="uploadFile" |
| | | :on-success="handleUploadSuccess" |
| | | :on-error="handleUploadError" |
| | | :before-upload="beforeUpload" |
| | |
| | | <script setup> |
| | | import { ref, reactive, getCurrentInstance } from "vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import axios from "axios"; |
| | | import { getToken } from "@/utils/auth.js"; |
| | | import PIMTable from "@/components/PIMTable/PIMTable.vue"; |
| | | import filePreview from "@/components/filePreview/index.vue"; |
| | |
| | | Authorization: "Bearer " + getToken(), |
| | | }); |
| | | const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); |
| | | const currentWorkOrderNo = ref(""); |
| | | |
| | | const beforeUpload = file => { |
| | | const isImage = file?.type?.startsWith("image/"); |
| | |
| | | const openDialog = row => { |
| | | dialogVisible.value = true; |
| | | currentWorkOrderId.value = row.id; |
| | | currentWorkOrderNo.value = row.workOrderNo || row.work_order_no || ""; |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | |
| | | proxy.$modal.msgError("文件上传失败"); |
| | | } |
| | | |
| | | const createWatermarkImage = async file => { |
| | | const sourceImage = await loadImage(URL.createObjectURL(file)); |
| | | const canvas = document.createElement("canvas"); |
| | | const width = sourceImage.naturalWidth || sourceImage.width; |
| | | const height = sourceImage.naturalHeight || sourceImage.height; |
| | | canvas.width = width; |
| | | canvas.height = height; |
| | | const ctx = canvas.getContext("2d"); |
| | | ctx.drawImage(sourceImage, 0, 0, width, height); |
| | | |
| | | const watermarkText = [ |
| | | "天津市玮苓乐器有限公司", |
| | | `${new Date().toLocaleString()}`, |
| | | ]; |
| | | |
| | | ctx.save(); |
| | | ctx.globalAlpha = 0.28; |
| | | ctx.fillStyle = "#ffffff"; |
| | | ctx.font = `bold ${Math.max(18, Math.floor(width / 28))}px Arial`; |
| | | ctx.textAlign = "right"; |
| | | ctx.textBaseline = "bottom"; |
| | | const padding = Math.max(24, Math.floor(width / 40)); |
| | | const lineHeight = Math.max(24, Math.floor(width / 20)); |
| | | const x = width - padding; |
| | | const y = height - padding; |
| | | watermarkText.reverse().forEach((text, index) => { |
| | | ctx.fillText(text, x, y - index * lineHeight); |
| | | }); |
| | | ctx.restore(); |
| | | |
| | | const blob = await new Promise(resolve => canvas.toBlob(resolve, file.type || "image/jpeg", 0.92)); |
| | | return new File([blob], file.name, { type: file.type || "image/jpeg" }); |
| | | }; |
| | | |
| | | const loadImage = src => |
| | | new Promise((resolve, reject) => { |
| | | const image = new Image(); |
| | | image.crossOrigin = "anonymous"; |
| | | image.onload = () => resolve(image); |
| | | image.onerror = reject; |
| | | image.src = src; |
| | | }); |
| | | |
| | | const uploadFile = async options => { |
| | | try { |
| | | const watermarkedFile = await createWatermarkImage(options.file); |
| | | const formData = new FormData(); |
| | | formData.append("file", watermarkedFile); |
| | | const response = await axios.post(uploadUrl.value, formData, { |
| | | headers: { |
| | | "Content-Type": "multipart/form-data", |
| | | Authorization: "Bearer " + getToken(), |
| | | }, |
| | | }); |
| | | options.onSuccess(response.data); |
| | | } catch (error) { |
| | | options.onError(error); |
| | | } |
| | | }; |
| | | |
| | | const handleDelete = () => { |
| | | if (selectedRows.value.length === 0) { |
| | | proxy.$modal.msgWarning("请选择数据"); |
| | |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped></style> |
| | | <style scoped></style> |
| | |
| | | <el-select v-model="form.dealResult" |
| | | placeholder="请选择" |
| | | clearable> |
| | | <el-option :label="item.label" |
| | | :value="item.value" |
| | | v-for="item in rejection_handling" |
| | | :key="item.value" /> |
| | | <el-option label="报工" value="报工" /> |
| | | <el-option label="报废" value="报废" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | <el-select v-model="form.dealType" |
| | | placeholder="请选择" |
| | | clearable |
| | | :disabled="!form.dealResult" |
| | | style="width: 100%"> |
| | | <el-option label="轻微返工" :value="0" /> |
| | | <el-option label="严重返工" :value="1" /> |
| | | <el-option label="报废" :value="2" /> |
| | | <el-option v-for="item in availableDealTypeOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | :precision="2" |
| | | style="width: 100%" |
| | | :disabled="form.dealType !== 0" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="重新生成新单:" |
| | | prop="regenerateNewOrder"> |
| | | <el-select v-model="form.regenerateNewOrder" |
| | | placeholder="请选择" |
| | | clearable |
| | | style="width: 100%"> |
| | | <el-option label="否" :value="0" /> |
| | | <el-option label="是" :value="1" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref(""); |
| | | const { rejection_handling } = proxy.useDict("rejection_handling"); |
| | | const data = reactive({ |
| | | form: { |
| | | checkTime: "", |
| | |
| | | inspectType: "", |
| | | defectivePhenomena: "", |
| | | dealResult: "", |
| | | dealType: 0, |
| | | dealType: null, |
| | | repairWorkHour: 0, |
| | | regenerateNewOrder: 0, |
| | | dealName: "", |
| | |
| | | const modelOptions = ref([]); |
| | | const userList = ref([]); // 检验员/处理人下拉列表 |
| | | const watermarkText = computed(() => { |
| | | const userName = userStore.nickName || userStore.user?.nickName || "未知人员"; |
| | | const now = new Date(); |
| | | const pad = (n) => String(n).padStart(2, "0"); |
| | | return `不合格处理\n${userName}\n${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`; |
| | | return `天津市玮苓乐器有限公司\n${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`; |
| | | }); |
| | | watch(() => form.value.dealType, (val) => { |
| | | const availableDealTypeOptions = computed(() => { |
| | | if (form.value.dealResult === "报废") { |
| | | return [{ label: "报废", value: 2 }]; |
| | | } |
| | | if (form.value.dealResult === "报工") { |
| | | return [ |
| | | { label: "轻微返工", value: 0 }, |
| | | { label: "严重返工", value: 1 }, |
| | | ]; |
| | | } |
| | | return []; |
| | | }); |
| | | |
| | | watch(() => form.value.dealResult, val => { |
| | | if (val === "报废") { |
| | | form.value.dealType = 2; |
| | | form.value.repairWorkHour = 0; |
| | | return; |
| | | } |
| | | if (val === "报工" && form.value.dealType === 2) { |
| | | form.value.dealType = null; |
| | | } |
| | | }); |
| | | |
| | | watch(() => form.value.dealType, val => { |
| | | if (val !== 0) { |
| | | form.value.repairWorkHour = 0; |
| | | } |
| | | if (val === 2) { |
| | | form.value.dealResult = "报废"; |
| | | } |
| | | if ((val === 0 || val === 1) && !form.value.dealResult) { |
| | | form.value.dealResult = "报工"; |
| | | } |
| | | }); |
| | | |
| | |
| | | dealName: "", |
| | | dealTime: "", |
| | | dealResult: "", |
| | | dealType: 0, |
| | | dealType: null, |
| | | repairWorkHour: 0, |
| | | regenerateNewOrder: 0, |
| | | photoUrls: [], |
| | |
| | | } |
| | | getProductOptions(); |
| | | if (operationType.value === "edit") { |
| | | getQualityUnqualifiedInfo(row.id).then(res => { |
| | | const { inspectState, ...rest } = res.data || {}; |
| | | form.value = { |
| | | ...rest, |
| | | photoUrls: rest.photoUrls |
| | | getQualityUnqualifiedInfo(row.id).then(res => { |
| | | const { inspectState, ...rest } = res.data || {}; |
| | | form.value = { |
| | | ...rest, |
| | | photoUrls: rest.photoUrls |
| | | ? String(rest.photoUrls).split(",").filter(Boolean).map((url, index) => ({ |
| | | name: `image-${index + 1}`, |
| | | url, |
| | | })) |
| | | })) |
| | | : [], |
| | | }; |
| | | }); |
| | | } |
| | | }; |
| | | if (Number(form.value.dealType) === 0) { |
| | | form.value.regenerateNewOrder = 0; |
| | | } else if (Number(form.value.dealType) === 1 || Number(form.value.dealType) === 2) { |
| | | form.value.regenerateNewOrder = 1; |
| | | } |
| | | if (!form.value.dealResult) { |
| | | form.value.dealResult = Number(form.value.dealType) === 2 ? "报废" : "报工"; |
| | | } |
| | | }); |
| | | } |
| | | }; |
| | | const getProductOptions = () => { |
| | | productTreeList().then(res => { |
| | |
| | | if (valid) { |
| | | // 状态字段不在表单填写,也不传给后端 |
| | | const { inspectState, ...payload } = form.value || {}; |
| | | if (!payload.dealResult) { |
| | | proxy.$modal.msgError("请选择处理结果"); |
| | | return; |
| | | } |
| | | if (payload.dealResult === "报工" && (payload.dealType === null || payload.dealType === undefined || payload.dealType === 2)) { |
| | | proxy.$modal.msgError("报工时请选择轻微返工或严重返工"); |
| | | return; |
| | | } |
| | | if (payload.dealResult === "报废") { |
| | | payload.dealType = 2; |
| | | } |
| | | if (payload.dealType === 0 && (!payload.repairWorkHour || payload.repairWorkHour <= 0)) { |
| | | proxy.$modal.msgError("轻微返工必须填写大于0的修理工时"); |
| | | return; |
| | |
| | | <el-col :span="12"> |
| | | <el-form-item label="处理结果:" prop="dealResult"> |
| | | <el-select v-model="form.dealResult" placeholder="请选择" clearable> |
| | | <el-option :label="item.label" :value="item.value" v-for="item in filteredRejectionHandling" :key="item.value" /> |
| | | <el-option :label="item.label" :value="item.value" v-for="item in autoDealResultOptions" :key="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="不合格类型:" prop="dealType"> |
| | | <el-input :model-value="dealTypeLabel(form.dealType)" disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="修理工时:" prop="repairWorkHour"> |
| | | <el-input-number |
| | | v-model="form.repairWorkHour" |
| | | :min="0" |
| | | :precision="2" |
| | | style="width: 100%" |
| | | :disabled="Number(form.dealType) !== 0" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {ref, reactive, toRefs, computed} from "vue"; |
| | | import { ref, reactive, toRefs, computed, getCurrentInstance } from "vue"; |
| | | import {productTreeList} from "@/api/basicData/product.js"; |
| | | import { |
| | | getQualityUnqualifiedInfo, |
| | |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | |
| | | const { rejection_handling } = proxy.useDict("rejection_handling") |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | | const data = reactive({ |
| | |
| | | inspectType: '', |
| | | defectivePhenomena: '', |
| | | dealResult: '', |
| | | dealType: null, |
| | | repairWorkHour: 0, |
| | | regenerateNewOrder: 0, |
| | | dealName: '', |
| | | dealTime: '', |
| | | method: undefined |
| | |
| | | checkResult: [{ required: false, message: "请输入", trigger: "blur" }], |
| | | defectivePhenomena: [{ required: true, message: "请输入", trigger: "blur" }], |
| | | dealResult: [{ required: true, message: "请输入", trigger: "blur" }], |
| | | repairWorkHour: [{ required: false, message: "请输入", trigger: "blur" }], |
| | | dealName: [{ required: true, message: "请选择处理人", trigger: "change" }], |
| | | dealTime: [{ required: true, message: "请输入", trigger: "change" }], |
| | | }, |
| | |
| | | const productOptions = ref([]); |
| | | const userList = ref([]); // 处理人下拉列表 |
| | | |
| | | const filteredRejectionHandling = computed(() => { |
| | | const data = rejection_handling.value; |
| | | if (form.value.method) { |
| | | return data.filter(item => item && item.label && item.label !== '返工' && item.label !== '返修') |
| | | const autoDealResultOptions = computed(() => { |
| | | if (Number(form.value.dealType) === 2) { |
| | | return [{ label: "报废", value: "报废" }]; |
| | | } |
| | | return data |
| | | if (Number(form.value.dealType) === 0 || Number(form.value.dealType) === 1) { |
| | | return [{ label: "报工", value: "报工" }]; |
| | | } |
| | | return []; |
| | | }) |
| | | |
| | | const dealTypeLabel = (val) => { |
| | | const type = Number(val); |
| | | if (type === 0) { |
| | | return "轻微返工"; |
| | | } |
| | | if (type === 1) { |
| | | return "严重返工"; |
| | | } |
| | | if (type === 2) { |
| | | return "报废"; |
| | | } |
| | | return "-"; |
| | | }; |
| | | |
| | | |
| | | // 打开弹框 |
| | |
| | | userList.value = []; |
| | | } |
| | | dialogFormVisible.value = true; |
| | | form.value = {}; |
| | | form.value = { |
| | | checkTime: "", |
| | | process: "", |
| | | checkName: "", |
| | | productName: "", |
| | | productId: "", |
| | | model: "", |
| | | unit: "", |
| | | quantity: "", |
| | | checkCompany: "", |
| | | checkResult: "", |
| | | inspectType: '', |
| | | defectivePhenomena: '', |
| | | dealResult: '', |
| | | dealType: null, |
| | | repairWorkHour: 0, |
| | | regenerateNewOrder: 0, |
| | | dealName: '', |
| | | dealTime: '', |
| | | method: undefined |
| | | }; |
| | | getProductOptions(); |
| | | if (operationType.value === 'edit') { |
| | | getQualityUnqualifiedInfo(row.id).then(res => { |
| | | const { inspectState, ...rest } = (res.data || {}) |
| | | // 有数据就显示默认值,没有就不显示 |
| | | form.value = { ...rest } |
| | | const dealType = rest.dealType === '' || rest.dealType === undefined || rest.dealType === null |
| | | ? null |
| | | : Number(rest.dealType); |
| | | // 用后端返回值回填,避免把不合格类型展示成空 |
| | | form.value = { ...form.value, ...rest, dealType } |
| | | if (!form.value.dealResult) { |
| | | form.value.dealResult = dealType === 2 ? "报废" : "报工"; |
| | | } |
| | | }) |
| | | } |
| | | } |
| | |
| | | if (valid) { |
| | | // 状态字段不在表单填写,也不传给后端;处理统一走 /deal 接口 |
| | | const { inspectState, ...payload } = (form.value || {}) |
| | | if (Number(payload.dealType) === 2) { |
| | | payload.dealResult = "报废"; |
| | | } else { |
| | | payload.dealResult = "报工"; |
| | | } |
| | | if (payload.dealType === 0 && (!payload.repairWorkHour || payload.repairWorkHour <= 0)) { |
| | | proxy.$modal.msgError("轻微返工必须填写大于0的修理工时"); |
| | | return; |
| | | } |
| | | qualityUnqualifiedDeal(payload).then(() => { |
| | | proxy.$modal.msgSuccess("提交成功"); |
| | | closeDia(); |
| | |
| | | |
| | | <style scoped> |
| | | |
| | | </style> |
| | | </style> |
| | |
| | | </div> |
| | | <FormDia ref="formDia" @close="handleQuery"></FormDia> |
| | | <InspectionFormDia ref="inspectionFormDia" @close="handleQuery"></InspectionFormDia> |
| | | <el-dialog v-model="uploadDialogVisible" title="拍照上传附件" width="680px"> |
| | | <el-form label-width="120px"> |
| | | <el-form-item label="不合格图片:"> |
| | | <AttachmentUploadImage |
| | | v-model:fileList="uploadPhotoList" |
| | | :limit="6" |
| | | button-text="拍照/上传图片" |
| | | :watermark-text="watermarkText" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="closeUploadDialog">取消</el-button> |
| | | <el-button type="primary" :loading="uploadSaving" @click="submitUploadDialog">保存</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | <el-dialog v-model="photoPreviewVisible" :title="photoPreviewTitle" width="900px"> |
| | | <div v-if="photoPreviewList.length" class="photo-preview-grid"> |
| | | <el-image |
| | | v-for="(url, index) in photoPreviewList" |
| | | :key="`${url}-${index}`" |
| | | :src="url" |
| | | fit="cover" |
| | | class="photo-preview-item" |
| | | :preview-src-list="photoPreviewList" |
| | | preview-teleported |
| | | /> |
| | | </div> |
| | | <el-empty v-else description="暂无附件照片" /> |
| | | <template #footer> |
| | | <el-button @click="photoPreviewVisible = false">关闭</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import {onMounted, ref, reactive, toRefs, nextTick, getCurrentInstance} from "vue"; |
| | | import { computed, onMounted, ref, reactive, toRefs, nextTick, getCurrentInstance } from "vue"; |
| | | import FormDia from "@/views/qualityManagement/nonconformingManagement/components/formDia.vue"; |
| | | import {ElMessageBox} from "element-plus"; |
| | | import {qualityUnqualifiedDel, qualityUnqualifiedListPage} from "@/api/qualityManagement/nonconformingManagement.js"; |
| | | import AttachmentUploadImage from "@/components/AttachmentUpload/image/index.vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { getQualityUnqualifiedInfo, qualityUnqualifiedDel, qualityUnqualifiedListPage, qualityUnqualifiedUpdate } from "@/api/qualityManagement/nonconformingManagement.js"; |
| | | import InspectionFormDia from "@/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | const data = reactive({ |
| | |
| | | label: "操作", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 100, |
| | | operation: [ |
| | | { |
| | | name: "处理", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openInspectionForm("edit", row); |
| | | width: 240, |
| | | operation: [ |
| | | { |
| | | name: "处理", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openInspectionForm("edit", row); |
| | | }, |
| | | disabled: (row) => row.inspectState === 1, |
| | | }, |
| | | disabled: (row) => row.inspectState === 1, |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | { |
| | | name: "拍照上传", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openUploadDialog(row); |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | | const selectedRows = ref([]); |
| | | const tableLoading = ref(false); |
| | |
| | | }); |
| | | const formDia = ref() |
| | | const inspectionFormDia = ref() |
| | | const photoPreviewVisible = ref(false) |
| | | const photoPreviewTitle = ref("") |
| | | const photoPreviewList = ref([]) |
| | | const uploadDialogVisible = ref(false) |
| | | const uploadSaving = ref(false) |
| | | const uploadPhotoList = ref([]) |
| | | const currentUploadRow = ref(null) |
| | | const { proxy } = getCurrentInstance() |
| | | const userStore = useUserStore() |
| | | const watermarkText = computed(() => { |
| | | const now = new Date(); |
| | | const pad = (n) => String(n).padStart(2, "0"); |
| | | return `天津市玮苓乐器有限公司 |
| | | ${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`; |
| | | }) |
| | | |
| | | const changeDaterange = (value) => { |
| | | searchForm.value.entryDateStart = undefined; |
| | |
| | | inspectionFormDia.value?.openDialog(type, row) |
| | | }) |
| | | }; |
| | | const openPhotoPreview = row => { |
| | | const urls = String(row.photoUrls || "") |
| | | .split(",") |
| | | .map(item => item.trim()) |
| | | .filter(Boolean); |
| | | photoPreviewList.value = urls; |
| | | photoPreviewTitle.value = `附件照片${row.productName ? ` - ${row.productName}` : ""}`; |
| | | photoPreviewVisible.value = true; |
| | | }; |
| | | const openUploadDialog = async (row) => { |
| | | if (!row?.id) { |
| | | proxy.$modal.msgWarning("未找到当前数据"); |
| | | return; |
| | | } |
| | | try { |
| | | const res = await getQualityUnqualifiedInfo(row.id); |
| | | const detail = res.data || {}; |
| | | currentUploadRow.value = detail; |
| | | uploadPhotoList.value = detail.photoUrls |
| | | ? String(detail.photoUrls).split(",").filter(Boolean).map((url, index) => ({ |
| | | name: `image-${index + 1}`, |
| | | url, |
| | | })) |
| | | : []; |
| | | uploadDialogVisible.value = true; |
| | | } catch (e) { |
| | | proxy.$modal.msgError("加载附件信息失败"); |
| | | } |
| | | }; |
| | | const closeUploadDialog = () => { |
| | | uploadDialogVisible.value = false; |
| | | uploadSaving.value = false; |
| | | uploadPhotoList.value = []; |
| | | currentUploadRow.value = null; |
| | | }; |
| | | const submitUploadDialog = async () => { |
| | | if (!currentUploadRow.value?.id) { |
| | | proxy.$modal.msgWarning("未找到当前数据"); |
| | | return; |
| | | } |
| | | const payload = { |
| | | ...currentUploadRow.value, |
| | | photoUrls: Array.isArray(uploadPhotoList.value) |
| | | ? uploadPhotoList.value.map(item => item.url || item.previewURL || item.previewUrl || item).filter(Boolean).join(",") |
| | | : "", |
| | | }; |
| | | uploadSaving.value = true; |
| | | try { |
| | | await qualityUnqualifiedUpdate(payload); |
| | | proxy.$modal.msgSuccess("保存成功"); |
| | | closeUploadDialog(); |
| | | getList(); |
| | | } catch (e) { |
| | | uploadSaving.value = false; |
| | | } |
| | | }; |
| | | |
| | | // 删除 |
| | | const handleDelete = () => { |
| | |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped></style> |
| | | <style scoped> |
| | | .photo-preview-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); |
| | | gap: 12px; |
| | | } |
| | | |
| | | .photo-preview-item { |
| | | width: 100%; |
| | | height: 180px; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | } |
| | | </style> |
| | |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | <div class=charts-container> |
| | | <el-row :gutter=20> |
| | | <el-col :span=24> |
| | | <el-card class=chart-card shadow=hover> |
| | | <template #header> |
| | | <div class=card-header> |
| | | <div class=chart-title-line></div> |
| | | <span>按人员/班组统计质量数据</span> |
| | | </div> |
| | | </template> |
| | | <el-row :gutter=20> |
| | | <el-col :span=12> |
| | | <div class=quality-rate-block> |
| | | <div class=quality-rate-block-title>按人员统计</div> |
| | | <el-table :data=personGroupRateData.personList size=small border class=quality-rate-table> |
| | | <el-table-column prop=dimensionName label=人员 min-width=120 /> |
| | | <el-table-column prop=totalCount label=总数 min-width=80 /> |
| | | <el-table-column prop=qualifiedCount label=合格数 min-width=90 /> |
| | | <el-table-column prop=unqualifiedCount label=不合格数 min-width=100 /> |
| | | <el-table-column prop=reworkCount label=返工数 min-width=90 /> |
| | | <el-table-column prop=scrapCount label=报废数 min-width=90 /> |
| | | <el-table-column label=合格率 min-width=90><template #default="scope">{{ formatRateValue(scope.row.passRate) }}</template></el-table-column> |
| | | <el-table-column label=返工率 min-width=90><template #default="scope">{{ formatRateValue(scope.row.reworkRate) }}</template></el-table-column> |
| | | <el-table-column label=报废率 min-width=90><template #default="scope">{{ formatRateValue(scope.row.scrapRate) }}</template></el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span=12> |
| | | <div class=quality-rate-block> |
| | | <div class=quality-rate-block-title>按班组统计</div> |
| | | <el-table :data=personGroupRateData.groupList size=small border class=quality-rate-table> |
| | | <el-table-column prop=dimensionName label=班组 min-width=120 /> |
| | | <el-table-column prop=totalCount label=总数 min-width=80 /> |
| | | <el-table-column prop=qualifiedCount label=合格数 min-width=90 /> |
| | | <el-table-column prop=unqualifiedCount label=不合格数 min-width=100 /> |
| | | <el-table-column prop=reworkCount label=返工数 min-width=90 /> |
| | | <el-table-column prop=scrapCount label=报废数 min-width=90 /> |
| | | <el-table-column label=合格率 min-width=90><template #default="scope">{{ formatRateValue(scope.row.passRate) }}</template></el-table-column> |
| | | <el-table-column label=返工率 min-width=90><template #default="scope">{{ formatRateValue(scope.row.reworkRate) }}</template></el-table-column> |
| | | <el-table-column label=报废率 min-width=90><template #default="scope">{{ formatRateValue(scope.row.scrapRate) }}</template></el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import { ref, reactive, onMounted, nextTick } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import * as echarts from "echarts"; |
| | | import { getInspectStatistics, getPassRateStatistics, getMonthlyPassRateStatistics, getYearlyPassRateStatistics, getMonthlyCompletionDetails, getTopParameters } from "@/api/reportAnalysis/qualityReport"; |
| | | import { getInspectStatistics, getPassRateStatistics, getMonthlyPassRateStatistics, getYearlyPassRateStatistics, getMonthlyCompletionDetails, getTopParameters, getPersonGroupRateStatistics } from "@/api/reportAnalysis/qualityReport"; |
| | | |
| | | // 响应式数据 |
| | | const filterForm = reactive({ |
| | |
| | | const yearlyPassRateData = ref([]); |
| | | const monthlyCompletionDetailsData = ref([]); |
| | | const topParametersData = ref({ totalCount: 0, list: [] }); |
| | | const personGroupRateData = ref({ personList: [], groupList: [] }); |
| | | const activeTab = ref("raw"); |
| | | |
| | | const getParameterColor = (index) => { |
| | |
| | | } |
| | | return field === 'completionRate' || field === 'passRate' ? '0%' : 0; |
| | | }; |
| | | const formatRateValue = value => { |
| | | return value || value === 0 ? Number(value).toFixed(2) + '%' : '0.00%'; |
| | | }; |
| | | |
| | | const fetchPersonGroupRateStatisticsData = async () => { |
| | | try { |
| | | const res = await getPersonGroupRateStatistics(); |
| | | if (res.code === 200 && res.data) { |
| | | personGroupRateData.value = { |
| | | personList: res.data.personList || [], |
| | | groupList: res.data.groupList || [] |
| | | }; |
| | | } |
| | | } catch (error) { |
| | | console.error("Failed to fetch person/group rate statistics:", error); |
| | | } |
| | | }; |
| | | |
| | | |
| | | const fetchInspectStatisticsData = async () => { |
| | | try { |
| | |
| | | |
| | | watch(activeTab, () => { |
| | | fetchTopParametersData(); |
| | | fetchPersonGroupRateStatisticsData(); |
| | | }); |
| | | |
| | | |
| | |
| | | fetchYearlyPassRateData(); |
| | | fetchMonthlyCompletionDetailsData(); |
| | | fetchTopParametersData(); |
| | | fetchPersonGroupRateStatisticsData(); |
| | | nextTick(() => { |
| | | initSampleChart(); |
| | | initEquipmentChart(); |
| | |
| | | border-radius: 3px; |
| | | } |
| | | |
| | | .quality-rate-block { |
| | | min-height: 320px; |
| | | } |
| | | |
| | | .quality-rate-block-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .quality-rate-table { |
| | | width: 100%; |
| | | } |
| | | |
| | | .chart-container { |
| | | height: 250px; |
| | | width: 100%; |