From a52ebf49d0a304697ec7af4f4c3e6b6e13f269e7 Mon Sep 17 00:00:00 2001
From: huminmin <mac@MacBook-Pro.local>
Date: 星期三, 15 七月 2026 15:28:40 +0800
Subject: [PATCH] 修改生产核算,不合格管理,报表管理

---
 src/views/qualityManagement/nonconformingManagement/components/formDia.vue           |  105 ++++++---
 src/views/productionManagement/workOrder/components/filesDia.vue                     |   66 ++++++
 src/views/reportAnalysis/reportManagement/index.vue                                  |   86 ++++++++
 src/views/productionManagement/workOrderManagement/components/filesDia.vue           |   67 ++++++
 src/api/productionManagement/productionProductMain.js                                |    1 
 src/views/productionManagement/productionCosting/index.vue                           |   14 +
 src/api/reportAnalysis/qualityReport.js                                              |    8 
 src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue |   96 ++++++++-
 src/views/qualityManagement/nonconformingManagement/index.vue                        |  157 ++++++++++++++-
 9 files changed, 529 insertions(+), 71 deletions(-)

diff --git a/src/api/productionManagement/productionProductMain.js b/src/api/productionManagement/productionProductMain.js
index 0493f8b..902d3e9 100644
--- a/src/api/productionManagement/productionProductMain.js
+++ b/src/api/productionManagement/productionProductMain.js
@@ -9,3 +9,4 @@
         params: query,
     });
 }
+
diff --git a/src/api/reportAnalysis/qualityReport.js b/src/api/reportAnalysis/qualityReport.js
index 66a7540..add1e3a 100644
--- a/src/api/reportAnalysis/qualityReport.js
+++ b/src/api/reportAnalysis/qualityReport.js
@@ -50,3 +50,11 @@
     params: { modelType }
   })
 }
+
+// 鑾峰彇鎸変汉鍛�/鐝粍缁熻璐ㄩ噺鏁版嵁
+export function getPersonGroupRateStatistics() {
+  return request({
+    url: '/qualityReport/getPersonGroupRateStatistics',
+    method: 'get'
+  })
+}
diff --git a/src/views/productionManagement/productionCosting/index.vue b/src/views/productionManagement/productionCosting/index.vue
index be247ce..53dfcf7 100644
--- a/src/views/productionManagement/productionCosting/index.vue
+++ b/src/views/productionManagement/productionCosting/index.vue
@@ -226,6 +226,11 @@
       minWidth: 100,
     },
     {
+      label: "琛ヤ慨鐞嗗伐鏃�",
+      prop: "repairWorkHour",
+      minWidth: 120,
+    },
+    {
       label: "鐢熶骇鏁伴噺",
       prop: "quantity",
       minWidth: 100,
@@ -236,8 +241,13 @@
       minWidth: 100,
     },
     {
-      label: "宸ヨ祫",
-      prop: "wages",
+      label: "鍘熷伐璧�",
+      prop: "originalWages",
+      minWidth: 100,
+    },
+    {
+      label: "淇宸ヨ祫",
+      prop: "adjustAmount",
       minWidth: 100,
     },
     {
diff --git a/src/views/productionManagement/workOrder/components/filesDia.vue b/src/views/productionManagement/workOrder/components/filesDia.vue
index 1336861..80e9fe6 100644
--- a/src/views/productionManagement/workOrder/components/filesDia.vue
+++ b/src/views/productionManagement/workOrder/components/filesDia.vue
@@ -5,7 +5,7 @@
         <el-upload
           v-model:file-list="fileList"
           class="upload-demo"
-          :action="uploadUrl"
+          :http-request="uploadFile"
           :on-success="handleUploadSuccess"
           :on-error="handleUploadError"
           :before-upload="beforeUpload"
@@ -46,6 +46,7 @@
 <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";
@@ -104,6 +105,7 @@
   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/");
@@ -116,6 +118,7 @@
 const openDialog = row => {
   dialogVisible.value = true;
   currentWorkOrderId.value = row.id;
+  currentWorkOrderNo.value = row.workOrderNo || row.work_order_no || "";
   page.current = 1;
   getList();
 };
@@ -171,6 +174,66 @@
   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("璇烽�夋嫨鏁版嵁");
@@ -199,4 +262,3 @@
 </script>
 
 <style scoped></style>
-
diff --git a/src/views/productionManagement/workOrderManagement/components/filesDia.vue b/src/views/productionManagement/workOrderManagement/components/filesDia.vue
index 84d8bd4..80e9fe6 100644
--- a/src/views/productionManagement/workOrderManagement/components/filesDia.vue
+++ b/src/views/productionManagement/workOrderManagement/components/filesDia.vue
@@ -5,7 +5,7 @@
         <el-upload
           v-model:file-list="fileList"
           class="upload-demo"
-          :action="uploadUrl"
+          :http-request="uploadFile"
           :on-success="handleUploadSuccess"
           :on-error="handleUploadError"
           :before-upload="beforeUpload"
@@ -46,6 +46,7 @@
 <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";
@@ -104,6 +105,7 @@
   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/");
@@ -116,6 +118,7 @@
 const openDialog = row => {
   dialogVisible.value = true;
   currentWorkOrderId.value = row.id;
+  currentWorkOrderNo.value = row.workOrderNo || row.work_order_no || "";
   page.current = 1;
   getList();
 };
@@ -171,6 +174,66 @@
   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("璇烽�夋嫨鏁版嵁");
@@ -198,4 +261,4 @@
 });
 </script>
 
-<style scoped></style>
\ No newline at end of file
+<style scoped></style>
diff --git a/src/views/qualityManagement/nonconformingManagement/components/formDia.vue b/src/views/qualityManagement/nonconformingManagement/components/formDia.vue
index 9263626..bf380bb 100644
--- a/src/views/qualityManagement/nonconformingManagement/components/formDia.vue
+++ b/src/views/qualityManagement/nonconformingManagement/components/formDia.vue
@@ -122,10 +122,8 @@
               <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>
@@ -137,10 +135,12 @@
               <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>
@@ -152,20 +152,6 @@
                                :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>
@@ -239,7 +225,6 @@
 
   const dialogFormVisible = ref(false);
   const operationType = ref("");
-  const { rejection_handling } = proxy.useDict("rejection_handling");
   const data = reactive({
     form: {
       checkTime: "",
@@ -255,7 +240,7 @@
       inspectType: "",
       defectivePhenomena: "",
       dealResult: "",
-      dealType: 0,
+      dealType: null,
       repairWorkHour: 0,
       regenerateNewOrder: 0,
       dealName: "",
@@ -280,14 +265,43 @@
   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 = "鎶ュ伐";
     }
   });
 
@@ -308,7 +322,7 @@
         dealName: "",
         dealTime: "",
         dealResult: "",
-        dealType: 0,
+        dealType: null,
         repairWorkHour: 0,
         regenerateNewOrder: 0,
         photoUrls: [],
@@ -327,19 +341,27 @@
     }
     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 => {
@@ -392,6 +414,17 @@
       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;
diff --git a/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue b/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue
index 8f4492a..d767cb2 100644
--- a/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue
+++ b/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue
@@ -82,8 +82,26 @@
           <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>
@@ -121,7 +139,7 @@
 </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,
@@ -131,7 +149,6 @@
 const { proxy } = getCurrentInstance()
 const emit = defineEmits(['close'])
 
-const { rejection_handling } = proxy.useDict("rejection_handling")
 const dialogFormVisible = ref(false);
 const operationType = ref('')
 const data = reactive({
@@ -149,6 +166,9 @@
     inspectType: '',
     defectivePhenomena: '',
     dealResult: '',
+    dealType: null,
+    repairWorkHour: 0,
+    regenerateNewOrder: 0,
     dealName: '',
     dealTime: '',
     method: undefined
@@ -165,6 +185,7 @@
     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" }],
   },
@@ -173,13 +194,29 @@
 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 "-";
+};
 
 
 // 鎵撳紑寮规
@@ -194,13 +231,39 @@
     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 ? "鎶ュ簾" : "鎶ュ伐";
+      }
     })
   }
 }
@@ -246,6 +309,15 @@
     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();
@@ -266,4 +338,4 @@
 
 <style scoped>
 
-</style>
\ No newline at end of file
+</style>
diff --git a/src/views/qualityManagement/nonconformingManagement/index.vue b/src/views/qualityManagement/nonconformingManagement/index.vue
index ea9a2cb..7cc984e 100644
--- a/src/views/qualityManagement/nonconformingManagement/index.vue
+++ b/src/views/qualityManagement/nonconformingManagement/index.vue
@@ -55,16 +55,51 @@
     </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({
@@ -199,19 +234,26 @@
     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);
@@ -222,7 +264,21 @@
 });
 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;
@@ -281,6 +337,62 @@
     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 = () => {
@@ -325,4 +437,17 @@
 });
 </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>
diff --git a/src/views/reportAnalysis/reportManagement/index.vue b/src/views/reportAnalysis/reportManagement/index.vue
index cd8ddd5..5704f43 100644
--- a/src/views/reportAnalysis/reportManagement/index.vue
+++ b/src/views/reportAnalysis/reportManagement/index.vue
@@ -273,6 +273,54 @@
         </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>
 
@@ -280,7 +328,7 @@
 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({
@@ -294,6 +342,7 @@
 const yearlyPassRateData = ref([]);
 const monthlyCompletionDetailsData = ref([]);
 const topParametersData = ref({ totalCount: 0, list: [] });
+const personGroupRateData = ref({ personList: [], groupList: [] });
 const activeTab = ref("raw");
 
 const getParameterColor = (index) => {
@@ -321,6 +370,24 @@
   }
   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 {
@@ -426,6 +493,7 @@
 
 watch(activeTab, () => {
   fetchTopParametersData();
+  fetchPersonGroupRateStatisticsData();
 });
 
 
@@ -884,6 +952,7 @@
   fetchYearlyPassRateData();
   fetchMonthlyCompletionDetailsData();
   fetchTopParametersData();
+  fetchPersonGroupRateStatisticsData();
   nextTick(() => {
     initSampleChart();
     initEquipmentChart();
@@ -1037,6 +1106,21 @@
   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%;

--
Gitblit v1.9.3