huminmin
2026-07-15 a52ebf49d0a304697ec7af4f4c3e6b6e13f269e7
修改生产核算,不合格管理,报表管理
已修改9个文件
600 ■■■■ 文件已修改
src/api/productionManagement/productionProductMain.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/reportAnalysis/qualityReport.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionCosting/index.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrder/components/filesDia.vue 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/components/filesDia.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/components/formDia.vue 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue 96 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/index.vue 157 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/reportManagement/index.vue 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionProductMain.js
@@ -9,3 +9,4 @@
        params: query,
    });
}
src/api/reportAnalysis/qualityReport.js
@@ -50,3 +50,11 @@
    params: { modelType }
  })
}
// 获取按人员/班组统计质量数据
export function getPersonGroupRateStatistics() {
  return request({
    url: '/qualityReport/getPersonGroupRateStatistics',
    method: 'get'
  })
}
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,
    },
    {
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>
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>
<style scoped></style>
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;
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>
</style>
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>
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%;