云
2026-09-17 0558f330f7e3781b91d00ed4a12e60d9fa469d87
feat(mes): 增加质检指标层级结构与多批次录入功能

- 新增 MesQcIndicatorItemType 枚举定义条目类型(录入项/分组项)
- 在质检指标表单中增加条目类型选择与所属分组关联选择功能
- 实现质检指标的树形层级展示,支持分组项与录入项结构
- 添加公式文本、单位文本、排序号等新字段配置
- 增加采样地点、证书编号、执行标准、复核人、审核人等字段
- 实现检测结果的多测量批次录入,支持同一指标多次检测
- 添加默认执行标准配置到质检方案模板
- 重构检测值录入界面,支持分组展示与批量测量管理
- 新增检验报告单打印数据接口与相关模型定义
已修改21个文件
已添加1个文件
1298 ■■■■■ 文件已修改
src/api/mes/qc/indicator/index.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/indicatorresult/index.ts 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/ipqc/index.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/iqc/index.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/oqc/index.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/rqc/index.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/template/index.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/template/indicator/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/biz-mes-enum.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/components/qc-report-print.vue 325 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/indicator/data.ts 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/indicatorresult/components/form.vue 393 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ipqc/data.ts 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ipqc/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/iqc/data.ts 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/iqc/index.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/data.ts 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/rqc/data.ts 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/rqc/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/template/data.ts 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/template/modules/indicator-list.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/indicator/index.ts
@@ -8,11 +8,17 @@
    id?: number; // ç¼–号
    code?: string; // æ£€æµ‹é¡¹ç¼–码
    name?: string; // æ£€æµ‹é¡¹åç§°
    parentId?: number; // çˆ¶æŒ‡æ ‡ç¼–号(0=顶级)
    itemType?: number; // æ¡ç›®ç±»åž‹ï¼š1=录入项,2=分组项
    formulaText?: string; // å…¬å¼å±•示文本(打印用,不参与计算)
    unitText?: string; // å•位文本(如 kg/m³、/%)
    sortOrder?: number; // åŒçº§æŽ’序号
    type?: number; // æ£€æµ‹é¡¹ç±»åž‹
    tool?: string; // æ£€æµ‹å·¥å…·
    resultType?: number; // ç»“果值类型
    resultSpecification?: string; // ç»“果值属性
    remark?: string; // å¤‡æ³¨
    parentName?: string; // çˆ¶æŒ‡æ ‡åç§°ï¼ˆåˆ—表/详情回填)
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
}
@@ -47,6 +53,13 @@
  return requestClient.delete(`/mes/qc/indicator/delete?id=${id}`);
}
/** æŸ¥è¯¢è´¨æ£€æŒ‡æ ‡å…¨é‡åˆ—表(含分组层级,供树表/下拉使用) */
export function getIndicatorList() {
  return requestClient.get<MesQcIndicatorApi.Indicator[]>(
    '/mes/qc/indicator/list',
  );
}
/** å¯¼å‡ºè´¨æ£€æŒ‡æ ‡ */
export function exportIndicator(params: any) {
  return requestClient.download('/mes/qc/indicator/export-excel', { params });
src/api/mes/qc/indicatorresult/index.ts
@@ -3,18 +3,35 @@
import { requestClient } from '#/api/request';
export namespace MesQcIndicatorResultApi {
  /** MES æ£€éªŒç»“果明细 */
  /** å¹³è¡Œæµ‹é‡æ‰¹æ¬¡æ˜Žç»†ï¼ˆåŒä¸€æŒ‡æ ‡çš„一次测量) */
  export interface Measure {
    detailId?: number; // æ˜Žç»†ç¼–号
    resultId?: number; // å…³è”检验结果 ID
    measureNo?: number; // æµ‹é‡æ‰¹æ¬¡å·ï¼ˆä»Ž 1 å¼€å§‹ï¼‰
    value?: string; // æ£€æµ‹å€¼
    remark?: string; // å¤‡æ³¨
  }
  /** MES æ£€éªŒç»“果明细(树形:分组项下挂子项;平行测量放入 measures) */
  export interface IndicatorResultDetail {
    id?: number; // ç¼–号
    id?: number; // ç¼–号(第一批明细 ID,兼容旧字段)
    resultId?: number; // å…³è”检验结果 ID
    indicatorId?: number; // æ£€æµ‹æŒ‡æ ‡ ID
    value?: string; // æ£€æµ‹å€¼ï¼ˆç»Ÿä¸€å­˜ä¸ºå­—符串)
    value?: string; // æ£€æµ‹å€¼ï¼ˆç¬¬ä¸€æ‰¹ï¼Œå…¼å®¹æ—§å­—段)
    valueNumber?: number; // UI æ•°å€¼ç»‘定(提交前转字符串)
    measureNo?: number; // æµ‹é‡æ‰¹æ¬¡å·ï¼ˆæäº¤ç”¨ï¼Œé»˜è®¤ 1)
    remark?: string; // å¤‡æ³¨
    // å…³è”查询字段(来自 indicator)
    indicatorName?: string; // æ£€æµ‹æŒ‡æ ‡åç§°
    valueType?: number; // è´¨æ£€å€¼ç±»åž‹
    valueSpecification?: string; // å€¼å±žæ€§
    parentId?: number; // çˆ¶æŒ‡æ ‡ç¼–号(0=顶级)
    itemType?: number; // æ¡ç›®ç±»åž‹ï¼š1=录入项,2=分组项
    formulaText?: string; // å…¬å¼å±•示文本
    unitText?: string; // å•位文本
    sortOrder?: number; // åŒçº§æŽ’序号
    measures?: Measure[]; // å¹³è¡Œæµ‹é‡æ‰¹æ¬¡åˆ—表(按 measureNo å‡åºï¼‰
    children?: IndicatorResultDetail[]; // å­é¡¹åˆ—表(分组项下挂)
  }
  /** MES æ£€éªŒç»“æžœ */
@@ -71,3 +88,65 @@
export function deleteIndicatorResult(id: number) {
  return requestClient.delete(`/mes/qc/indicator-result/delete?id=${id}`);
}
// ==================== æ£€éªŒæŠ¥å‘Šå•(打印数据) ====================
export namespace MesQcReportApi {
  /** æŠ¥å‘Šæµ‹é‡å€¼ */
  export interface ReportMeasure {
    measureNo?: number; // æµ‹é‡æ‰¹æ¬¡å·
    value?: string; // æ£€æµ‹å€¼
    remark?: string; // å¤‡æ³¨
  }
  /** æŠ¥å‘Šæ£€éªŒé¡¹ï¼ˆæ ‘形) */
  export interface ReportItem {
    indicatorId?: number;
    indicatorCode?: string;
    indicatorName?: string;
    parentId?: number;
    itemType?: number; // 1=录入项,2=分组项
    formulaText?: string;
    unitText?: string;
    sortOrder?: number;
    checkMethod?: string; // æ£€æµ‹æ–¹æ³•
    standardValue?: string; // æ ‡å‡†å€¼
    thresholdMin?: string; // é˜ˆå€¼ä¸‹é™
    thresholdMax?: string; // é˜ˆå€¼ä¸Šé™
    measures?: ReportMeasure[];
    children?: ReportItem[];
  }
  /** æ£€éªŒæŠ¥å‘Šå• */
  export interface QcReport {
    qcType?: number;
    docCode?: string;
    docName?: string;
    itemCode?: string;
    itemName?: string;
    itemSpecification?: string;
    batchNo?: string;
    samplingLocation?: string;
    certificateNo?: string;
    executeStandard?: string;
    samplingTime?: Date;
    batchQuantity?: number;
    checkResult?: number;
    inspectorUserId?: number;
    inspectorName?: string;
    auditUserId?: number;
    auditName?: string;
    approveUserId?: number;
    approveName?: string;
    remark?: string;
    measureNos?: number[]; // åˆ—头:测量批次号列表
    items?: ReportItem[];
  }
}
/** æŸ¥è¯¢æ£€éªŒæŠ¥å‘Šå•数据(打印用) */
export function getQcReport(qcId: number, qcType: number, resultId?: number) {
  return requestClient.get<MesQcReportApi.QcReport>('/mes/qc/indicator-result/report', {
    params: { qcId, qcType, resultId },
  });
}
src/api/mes/qc/ipqc/index.ts
@@ -43,6 +43,13 @@
    inspectDate?: number; // æ£€æµ‹æ—¥æœŸ
    inspectorUserId?: number; // æ£€æµ‹äººå‘˜ç”¨æˆ· ID
    inspectorNickname?: string; // æ£€æµ‹äººå‘˜æ˜µç§°ï¼ˆå…³è”查询)
    auditUserId?: number; // å¤æ ¸äºº ID
    auditNickname?: string; // å¤æ ¸äººæ˜µç§°ï¼ˆå…³è”查询)
    approveUserId?: number; // å®¡æ ¸äºº ID
    approveNickname?: string; // å®¡æ ¸äººæ˜µç§°ï¼ˆå…³è”查询)
    samplingLocation?: string; // é‡‡æ ·åœ°ç‚¹
    certificateNo?: string; // è¯ä¹¦ç¼–号/合格证号
    executeStandard?: string; // æ‰§è¡Œæ ‡å‡†
    status?: number; // çŠ¶æ€
    remark?: string; // å¤‡æ³¨
  }
src/api/mes/qc/iqc/index.ts
@@ -37,6 +37,13 @@
    inspector?: string; // æ£€æµ‹äººå‘˜ï¼ˆæ˜µç§°ï¼‰
    inspectorUserId?: number; // æ£€æµ‹äººå‘˜ ID
    inspectorNickname?: string; // æ£€æµ‹äººå‘˜æ˜µç§°ï¼ˆå…³è”查询)
    auditUserId?: number; // å¤æ ¸äºº ID
    auditNickname?: string; // å¤æ ¸äººæ˜µç§°ï¼ˆå…³è”查询)
    approveUserId?: number; // å®¡æ ¸äºº ID
    approveNickname?: string; // å®¡æ ¸äººæ˜µç§°ï¼ˆå…³è”查询)
    samplingLocation?: string; // é‡‡æ ·åœ°ç‚¹
    certificateNo?: string; // è¯ä¹¦ç¼–号/合格证号
    executeStandard?: string; // æ‰§è¡Œæ ‡å‡†
    status?: number; // çŠ¶æ€
    remark?: string; // å¤‡æ³¨
  }
src/api/mes/qc/oqc/index.ts
@@ -38,6 +38,13 @@
    inspectDate?: number; // æ£€æµ‹æ—¥æœŸ
    inspectorUserId?: number; // æ£€æµ‹äººå‘˜ç”¨æˆ· ID
    inspectorNickname?: string; // æ£€æµ‹äººå‘˜æ˜µç§°ï¼ˆå…³è”查询)
    auditUserId?: number; // å¤æ ¸äºº ID
    auditNickname?: string; // å¤æ ¸äººæ˜µç§°ï¼ˆå…³è”查询)
    approveUserId?: number; // å®¡æ ¸äºº ID
    approveNickname?: string; // å®¡æ ¸äººæ˜µç§°ï¼ˆå…³è”查询)
    samplingLocation?: string; // é‡‡æ ·åœ°ç‚¹
    certificateNo?: string; // è¯ä¹¦ç¼–号/合格证号
    executeStandard?: string; // æ‰§è¡Œæ ‡å‡†
    status?: number; // çŠ¶æ€
    remark?: string; // å¤‡æ³¨
  }
src/api/mes/qc/rqc/index.ts
@@ -27,6 +27,13 @@
    inspectDate?: number; // æ£€æµ‹æ—¥æœŸ
    inspectorUserId?: number; // æ£€æµ‹äººå‘˜ç”¨æˆ· ID
    inspectorNickname?: string; // æ£€æµ‹äººå‘˜æ˜µç§°ï¼ˆå…³è”查询)
    auditUserId?: number; // å¤æ ¸äºº ID
    auditNickname?: string; // å¤æ ¸äººæ˜µç§°ï¼ˆå…³è”查询)
    approveUserId?: number; // å®¡æ ¸äºº ID
    approveNickname?: string; // å®¡æ ¸äººæ˜µç§°ï¼ˆå…³è”查询)
    samplingLocation?: string; // é‡‡æ ·åœ°ç‚¹
    certificateNo?: string; // è¯ä¹¦ç¼–号/合格证号
    executeStandard?: string; // æ‰§è¡Œæ ‡å‡†
    status?: number; // çŠ¶æ€
    remark?: string; // å¤‡æ³¨
    // ç¼ºé™·ç»Ÿè®¡
src/api/mes/qc/template/index.ts
@@ -10,6 +10,7 @@
    name?: string; // æ–¹æ¡ˆåç§°
    types?: number[]; // æ£€æµ‹ç§ç±»
    status?: number; // çŠ¶æ€
    defaultExecuteStandard?: string; // é»˜è®¤æ‰§è¡Œæ ‡å‡†ï¼ˆå»ºå•时带入主表)
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
src/api/mes/qc/template/indicator/index.ts
@@ -14,6 +14,8 @@
    thresholdMax?: number; // è¯¯å·®ä¸Šé™
    thresholdMin?: number; // è¯¯å·®ä¸‹é™
    docUrl?: string; // è¯´æ˜Žå›¾URL
    sortOrder?: number; // æŽ’序号(方案内展示顺序)
    judgeFlag?: boolean; // æ˜¯å¦å‚与判定提示(分组/参数行=false)
    remark?: string; // å¤‡æ³¨
    indicatorCode?: string; // æ£€æµ‹é¡¹ç¼–码(JOIN)
    indicatorName?: string; // æ£€æµ‹é¡¹åç§°ï¼ˆJOIN)
src/packages/constants/src/biz-mes-enum.ts
@@ -476,6 +476,12 @@
  FILE: 5,
} as const;
/** MES è´¨æ£€æŒ‡æ ‡æ¡ç›®ç±»åž‹æžšä¸¾ï¼š1=录入项,2=分组项(对齐后端 MesQcIndicatorItemTypeEnum) */
export const MesQcIndicatorItemType = {
  RECORD: 1,
  GROUP: 2,
} as const;
/** MES ä¸šåŠ¡ç±»åž‹å¸¸é‡ */
export const MesBizTypeEnum = {
  // WM ä»“库模块 [100, 200)
src/views/mes/qc/components/qc-report-print.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,325 @@
<script lang="ts" setup>
import type { MesQcReportApi } from '#/api/mes/qc/indicatorresult';
import { computed, nextTick, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { useUserStore } from '@vben/stores';
import { formatDateTime } from '@vben/utils';
import { getQcReport } from '#/api/mes/qc/indicatorresult';
/**
 * MES æ£€éªŒæŠ¥å‘Šå•打印组件(化工检验记录版式:表头 + æ£€éªŒé¡¹çŸ©é˜µ + å¹³è¡Œæµ‹é‡åˆ— + ç­¾å­—区)
 *
 * æ‰“印样式参考 erp/sale/order/modules/print.vue(Teleport + body class + @media print)。
 */
defineOptions({ name: 'MesQcReportPrint' });
const userStore = useUserStore();
/** æ‰å¹³åŒ–后的报告行 */
interface PrintRow {
  kind: 'group' | 'record';
  indicatorId?: number;
  name?: string;
  unit?: string;
  method?: string; // åˆ†ç»„=公式文本,录入项=检测方法
  standard?: string; // æ ‡å‡†å€¼ï¼ˆå«é˜ˆå€¼åŒºé—´ï¼‰
  values: Record<number, string>; // measureNo -> æ£€æµ‹å€¼
  remarks: Record<number, string>; // measureNo -> å¤‡æ³¨
}
const report = ref<MesQcReportApi.QcReport>({});
const measureNos = ref<number[]>([1]);
const printTime = ref('');
/** æ ‡å‡†å€¼å±•示:标准值 + é˜ˆå€¼åŒºé—´ */
function formatStandard(item: MesQcReportApi.ReportItem) {
  const parts: string[] = [];
  if (item.standardValue != null) {
    parts.push(String(item.standardValue));
  }
  if (item.thresholdMin != null || item.thresholdMax != null) {
    parts.push(`${item.thresholdMin ?? ''}~${item.thresholdMax ?? ''}`);
  }
  return parts.join(' ');
}
/** å°†æ£€éªŒé¡¹æ ‘扁平化为打印行(分组行 + å…¶å­å½•入行;顶级录入行独立成行) */
const printRows = computed<PrintRow[]>(() => {
  const rows: PrintRow[] = [];
  const pushRecord = (item: MesQcReportApi.ReportItem, method: string) => {
    const values: Record<number, string> = {};
    const remarks: Record<number, string> = {};
    (item.measures ?? []).forEach((m) => {
      if (m.measureNo != null) {
        values[m.measureNo] = m.value ?? '';
        remarks[m.measureNo] = m.remark ?? '';
      }
    });
    rows.push({
      kind: 'record',
      indicatorId: item.indicatorId,
      name: item.indicatorName,
      unit: item.unitText,
      method,
      standard: formatStandard(item),
      values,
      remarks,
    });
  };
  const walk = (items: MesQcReportApi.ReportItem[] = []) => {
    items.forEach((item) => {
      if (item.itemType === 2) {
        rows.push({
          kind: 'group',
          indicatorId: item.indicatorId,
          name: item.indicatorName,
          method: item.formulaText,
          values: {},
          remarks: {},
        });
        walk(item.children ?? []);
      } else {
        pushRecord(item, item.checkMethod ?? '');
      }
    });
  };
  walk(report.value.items ?? []);
  return rows;
});
/** æ£€æµ‹ç»“果文案(字典 mes_qc_check_result) */
const checkResultText = computed(() => {
  if (report.value.checkResult == null) {
    return '-';
  }
  return (
    getDictLabel(DICT_TYPE.MES_QC_CHECK_RESULT, report.value.checkResult) || '-'
  );
});
function waitForPaint() {
  return new Promise<void>((resolve) => {
    requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
  });
}
function removePrintMode() {
  document.body.classList.remove('mes-qc-report-printing');
}
/** æ‰“印:加载报告数据后切到打印模式调用浏览器打印 */
async function print(qcId: number, qcType: number, resultId?: number) {
  const data = await getQcReport(qcId, qcType, resultId);
  report.value = data;
  measureNos.value = data.measureNos?.length ? data.measureNos : [1];
  printTime.value = formatDateTime(new Date());
  await nextTick();
  await waitForPaint();
  document.body.classList.add('mes-qc-report-printing');
  window.addEventListener('afterprint', removePrintMode, { once: true });
  window.print();
}
defineExpose({ print });
</script>
<template>
  <Teleport to="body">
    <div
      id="mesQcReportPrint"
      class="mes-qc-report-print pointer-events-none fixed left-0 top-0 z-[-1] w-full bg-white text-[#303133] opacity-0"
    >
      <h2 class="m-0 mb-1 text-center text-[1.6em] font-bold leading-[1.2]">
        {{ report.docName || '检验报告' }}
      </h2>
      <div class="mb-2 text-center text-sm">
        {{ report.itemName || '' }}
        <span v-if="report.itemSpecification">/{{ report.itemSpecification }}</span>
        <span v-if="report.executeStandard"> 执行标准:{{ report.executeStandard }}</span>
      </div>
      <!-- è¡¨å¤´ä¿¡æ¯æ  -->
      <div
        class="mb-3 grid grid-cols-3 gap-x-6 gap-y-1 border border-solid border-[#dcdfe6] p-3 text-sm leading-[1.6]"
      >
        <div>检验单号:{{ report.docCode || '-' }}</div>
        <div>物料编码:{{ report.itemCode || '-' }}</div>
        <div>批次号:{{ report.batchNo || '-' }}</div>
        <div>采样地点:{{ report.samplingLocation || '-' }}</div>
        <div>证书编号:{{ report.certificateNo || '-' }}</div>
        <div>批量:{{ report.batchQuantity ?? '-' }}</div>
        <div>
          é‡‡æ ·/检验时间:{{ formatDateTime(report.samplingTime) || '-' }}
        </div>
        <div>检测结论:{{ checkResultText }}</div>
        <div>打印时间:{{ printTime || '-' }}</div>
      </div>
      <!-- æ£€éªŒé¡¹çŸ©é˜µ -->
      <table class="w-full border-collapse text-[13px] leading-[1.5]">
        <thead>
          <tr>
            <th class="cell bg">检验项目</th>
            <th class="cell w-16 bg">单位</th>
            <th class="cell bg">方法/公式</th>
            <th class="cell w-24 bg">标准值</th>
            <th v-for="no in measureNos" :key="`h-${no}`" class="cell w-20 bg">
              æµ‹é‡{{ no }}
            </th>
            <th class="cell bg">备注</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(row, i) in printRows" :key="row.indicatorId ?? i">
            <template v-if="row.kind === 'group'">
              <td class="cell group-name" colspan="3">{{ row.name }}</td>
              <td class="cell group-formula" :colspan="measureNos.length + 2">
                {{ row.method || '' }}
              </td>
            </template>
            <template v-else>
              <td class="cell record-name">{{ row.name }}</td>
              <td class="cell text-center">{{ row.unit || '-' }}</td>
              <td class="cell">{{ row.method || '-' }}</td>
              <td class="cell text-center">{{ row.standard || '-' }}</td>
              <td
                v-for="no in measureNos"
                :key="`v-${row.indicatorId}-${no}`"
                class="cell text-center"
              >
                {{ row.values[no] || '' }}
              </td>
              <td class="cell">
                <template v-if="measureNos.length === 1">
                  {{ row.remarks[measureNos[0]!] || '' }}
                </template>
                <template v-else>
                  <div v-for="no in measureNos" :key="`r-${row.indicatorId}-${no}`">
                    {{ no }}:{{ row.remarks[no] || '' }}
                  </div>
                </template>
              </td>
            </template>
          </tr>
          <tr v-if="printRows.length === 0">
            <td class="cell text-center" :colspan="6 + measureNos.length">
              æš‚无检验项
            </td>
          </tr>
        </tbody>
      </table>
      <!-- å¤‡æ³¨ + ç­¾å­—区 -->
      <div class="mt-3 border border-solid border-[#dcdfe6] p-3 text-sm leading-[1.6]">
        å¤‡æ³¨ï¼š{{ report.remark || '-' }}
      </div>
      <div
        class="mt-4 grid grid-cols-3 gap-x-10 gap-y-4 border border-dashed border-[#dcdfe6] p-4 text-sm leading-[1.8]"
      >
        <div>
          åˆ†æžè€…:{{ report.inspectorName || '-' }}
        </div>
        <div>
          å¤æ ¸äººï¼š{{ report.auditName || '-' }}
        </div>
        <div>
          å®¡æ ¸äººï¼š{{ report.approveName || '-' }}
        </div>
        <div>
          ç­¾å­—:<span class="sig"></span>
        </div>
        <div>
          ç­¾å­—:<span class="sig"></span>
        </div>
        <div>
          ç­¾å­—:<span class="sig"></span>
        </div>
      </div>
      <div class="mt-4 flex justify-between text-xs text-[#909399]">
        <div>打印人员:{{ userStore.userInfo?.nickname || '-' }}</div>
        <div>打印时间:{{ printTime || '-' }}</div>
      </div>
    </div>
  </Teleport>
</template>
<style scoped>
@page {
  margin: 8mm 10mm;
}
.mes-qc-report-print {
  .cell {
    padding: 4px 6px;
    text-align: left;
    border: 1px solid #dcdfe6;
  }
  .cell.bg {
    font-weight: bold;
    background: #f5f7fa;
    text-align: center;
  }
  .cell.text-center {
    text-align: center;
  }
  .group-name {
    font-weight: bold;
    background: #fafafa;
  }
  .group-formula {
    font-style: italic;
    color: #555;
    background: #fafafa;
  }
  .record-name {
    padding-left: 20px;
  }
  .sig {
    display: inline-block;
    width: 90px;
    border-bottom: 1px solid #303133;
  }
}
@media print {
  :global(body.mes-qc-report-printing) {
    padding: 0 !important;
    margin: 0 !important;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  :global(body.mes-qc-report-printing *) {
    visibility: hidden !important;
  }
  :global(body.mes-qc-report-printing .mes-qc-report-print),
  :global(body.mes-qc-report-printing .mes-qc-report-print *) {
    visibility: visible !important;
  }
  :global(body.mes-qc-report-printing .mes-qc-report-print) {
    position: absolute;
    top: 0;
    left: 0;
    z-index: auto;
    box-sizing: border-box;
    width: 100%;
    padding: 0 !important;
    margin: 0 !important;
    pointer-events: auto;
    opacity: 1;
  }
}
</style>
src/views/mes/qc/indicator/data.ts
@@ -7,12 +7,14 @@
import {
  DICT_TYPE,
  MesAutoCodeRuleCode,
  MesQcIndicatorItemType,
  MesQcResultValueType,
} from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
import { getIndicatorList } from '#/api/mes/qc/indicator';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { QcIndicatorResultSpecificationInput } from './components';
@@ -63,6 +65,42 @@
      rules: 'required',
    },
    {
      fieldName: 'itemType',
      label: '条目类型',
      component: 'RadioGroup',
      defaultValue: MesQcIndicatorItemType.RECORD,
      componentProps: {
        options: [
          { label: '录入项', value: MesQcIndicatorItemType.RECORD },
          { label: '分组项', value: MesQcIndicatorItemType.GROUP },
        ],
        optionType: 'button',
      },
      rules: 'required',
    },
    {
      fieldName: 'parentId',
      label: '所属分组',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: async () => {
          const list = await getIndicatorList();
          // ä»…可选择分组项作为父级,且分组项本身不可再挂父级(仅两级)
          return list
            .filter((i) => i.itemType === MesQcIndicatorItemType.GROUP)
            .map((i) => ({ label: i.name, value: i.id }));
        },
        placeholder: '不选则为顶级项',
      },
      dependencies: {
        triggerFields: ['itemType'],
        // åˆ†ç»„项必须为顶级,隐藏并清空上级
        show: (values) => values.itemType !== MesQcIndicatorItemType.GROUP,
        if: (values) => values.itemType !== MesQcIndicatorItemType.GROUP,
      },
    },
    {
      fieldName: 'type',
      label: '检测项类型',
      component: 'Select',
@@ -74,12 +112,45 @@
      rules: 'required',
    },
    {
      fieldName: 'formulaText',
      label: '公式文本',
      component: 'Input',
      componentProps: {
        maxlength: 500,
        placeholder: '如 w3 = c Ã— V Ã— 0.06005 / m Ã— 100%(仅展示,不参与计算)',
      },
      dependencies: {
        triggerFields: ['itemType'],
        if: (values) => values.itemType === MesQcIndicatorItemType.GROUP,
      },
    },
    {
      fieldName: 'tool',
      label: '检测工具',
      component: 'Input',
      componentProps: {
        maxlength: 100,
        placeholder: '请输入检测工具',
      },
    },
    {
      fieldName: 'unitText',
      label: '单位文本',
      component: 'Input',
      componentProps: {
        maxlength: 50,
        placeholder: '如 %、g、无,仅展示不参与计算',
      },
    },
    {
      fieldName: 'sortOrder',
      label: '排序号',
      component: 'InputNumber',
      defaultValue: 0,
      componentProps: {
        min: 0,
        precision: 0,
        placeholder: '同级排序,数字小的在前',
      },
    },
    {
@@ -95,6 +166,11 @@
        placeholder: '请选择结果值类型',
      },
      rules: 'required',
      dependencies: {
        triggerFields: ['itemType'],
        // åˆ†ç»„项不录入结果值,隐藏并跳过校验
        if: (values) => values.itemType !== MesQcIndicatorItemType.GROUP,
      },
    },
    {
      fieldName: 'resultSpecification',
@@ -183,6 +259,19 @@
      minWidth: 160,
    },
    {
      field: 'itemType',
      title: '条目类型',
      width: 90,
      formatter: ({ cellValue }) =>
        cellValue === MesQcIndicatorItemType.GROUP ? '分组项' : '录入项',
    },
    {
      field: 'parentName',
      title: '所属分组',
      width: 120,
      formatter: ({ cellValue }) => cellValue || '-',
    },
    {
      field: 'type',
      title: '检测项类型',
      width: 120,
@@ -194,6 +283,12 @@
      width: 140,
    },
    {
      field: 'unitText',
      title: '单位',
      width: 80,
      formatter: ({ cellValue }) => cellValue || '-',
    },
    {
      field: 'resultType',
      title: '结果值类型',
      width: 120,
src/views/mes/qc/indicatorresult/components/form.vue
@@ -6,10 +6,11 @@
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { MesQcResultValueType } from '@vben/constants';
import { MesQcIndicatorItemType, MesQcResultValueType } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import {
  Button,
  Form as AForm,
  Divider,
  Input,
@@ -36,11 +37,36 @@
  qcType: number;
}
/** å¯ç¼–辑测量批次(在接口结构上增加数值绑定字段) */
interface EditMeasure {
  detailId?: number;
  resultId?: number;
  measureNo: number;
  value?: string;
  valueNumber?: number;
  remark?: string;
}
/** å¯ç¼–辑检验项(树形节点) */
interface EditItem {
  indicatorId?: number;
  indicatorName?: string;
  valueType?: number;
  valueSpecification?: string;
  parentId?: number;
  itemType?: number;
  formulaText?: string;
  unitText?: string;
  sortOrder?: number;
  measures: EditMeasure[];
  children?: EditItem[];
}
defineOptions({ name: 'QcIndicatorResultForm' });
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const items = ref<MesQcIndicatorResultApi.IndicatorResultDetail[]>([]);
const roots = ref<EditItem[]>([]);
const ctxData = ref<CtxData>();
const getTitle = computed(() =>
@@ -63,6 +89,19 @@
  wrapperClass: 'grid-cols-2',
});
/** æ˜¯å¦åˆ†ç»„项(分组项不录入值,仅承载层级与公式文本) */
function isGroup(item: EditItem) {
  return item.itemType === MesQcIndicatorItemType.GROUP;
}
/** æ˜¯å¦æ•°å€¼åž‹å½•入(决定 InputNumber ä¸Žæ–‡æœ¬æŽ§ä»¶ï¼‰ */
function isNumeric(item: EditItem) {
  return (
    item.valueType === MesQcResultValueType.FLOAT ||
    item.valueType === MesQcResultValueType.INTEGER
  );
}
/** èŽ·å–å­—å…¸é€‰é¡¹ï¼ˆvalueSpecification ä¸ºç³»ç»Ÿå­—典类型名) */
function getValueOptions(dictType?: string) {
  return dictType
@@ -71,6 +110,75 @@
        value: String(value),
      }))
    : [];
}
/** å°†æŽ¥å£è¿”回的明细树转换为可编辑结构:分组递归、录入项保证至少一个测量批次 */
function toEditItems(
  items: MesQcIndicatorResultApi.IndicatorResultDetail[] = [],
): EditItem[] {
  return items.map((item) => {
    const node: EditItem = {
      indicatorId: item.indicatorId,
      indicatorName: item.indicatorName,
      valueType: item.valueType,
      valueSpecification: item.valueSpecification,
      parentId: item.parentId,
      itemType: item.itemType,
      formulaText: item.formulaText,
      unitText: item.unitText,
      sortOrder: item.sortOrder,
      measures: [],
      children: item.children ? toEditItems(item.children) : undefined,
    };
    if (isGroup(node)) {
      return node;
    }
    const measures = (item.measures ?? []).map((m) => ({
      detailId: m.detailId,
      resultId: m.resultId,
      measureNo: m.measureNo ?? 1,
      value: m.value,
      valueNumber:
        isNumeric(node) && m.value !== null && m.value !== undefined
          ? Number(m.value)
          : undefined,
      remark: m.remark,
    }));
    node.measures = measures.length > 0 ? measures : [{ measureNo: 1 }];
    return node;
  });
}
/** æ”¶é›†æ‰€æœ‰å½•入项(叶子)用于提交与展示统计 */
function collectLeaves(nodes: EditItem[]): EditItem[] {
  const leaves: EditItem[] = [];
  const walk = (list: EditItem[]) => {
    list.forEach((n) => {
      if (isGroup(n)) {
        walk(n.children ?? []);
      } else {
        leaves.push(n);
      }
    });
  };
  walk(nodes);
  return leaves;
}
/** æ–°å¢žä¸€ä¸ªæµ‹é‡æ‰¹æ¬¡ */
function addMeasure(item: EditItem) {
  const nextNo =
    item.measures.reduce((max, m) => Math.max(max, m.measureNo ?? 0), 0) + 1;
  item.measures.push({ measureNo: nextNo });
}
/** åˆ é™¤æŒ‡å®šæµ‹é‡æ‰¹æ¬¡ï¼ˆè‡³å°‘保留一个) */
function removeMeasure(item: EditItem, index: number) {
  if (item.measures.length <= 1) {
    message.warning('至少保留一个测量批次');
    return;
  }
  item.measures.splice(index, 1);
}
const [Modal, modalApi] = useVbenModal({
@@ -83,25 +191,26 @@
      return;
    }
    modalApi.lock();
    // æäº¤è¡¨å•
    const head =
      (await formApi.getValues()) as MesQcIndicatorResultApi.IndicatorResult;
    const submitItems = items.value.map((item) => {
      const submit: MesQcIndicatorResultApi.IndicatorResultDetail = {
        id: item.id,
        indicatorId: item.indicatorId,
        remark: item.remark,
      };
      if (
        item.valueType === MesQcResultValueType.FLOAT ||
        item.valueType === MesQcResultValueType.INTEGER
      ) {
        submit.value =
          item.valueNumber === null ? undefined : String(item.valueNumber);
      } else {
        submit.value = item.value;
      }
      return submit;
    // å±•开:每个录入项的每个测量批次 â†’ ä¸€æ¡æ˜Žç»†
    const submitItems: MesQcIndicatorResultApi.IndicatorResultDetail[] = [];
    collectLeaves(roots.value).forEach((item) => {
      item.measures.forEach((measure) => {
        const value = isNumeric(item)
          ? measure.valueNumber === null || measure.valueNumber === undefined
            ? undefined
            : String(measure.valueNumber)
          : measure.value;
        submitItems.push({
          id: measure.detailId,
          resultId: measure.resultId,
          indicatorId: item.indicatorId,
          measureNo: measure.measureNo,
          value,
          remark: measure.remark,
        });
      });
    });
    const payload: MesQcIndicatorResultApi.IndicatorResult = {
      ...head,
@@ -117,7 +226,6 @@
        await createIndicatorResult(payload);
        message.success($t('common.createSuccess'));
      }
      // å…³é—­å¹¶æç¤º
      await modalApi.close();
      emit('success');
    } finally {
@@ -127,11 +235,10 @@
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      ctxData.value = undefined;
      items.value = [];
      roots.value = [];
      return;
    }
    formApi.setState({ schema: useQcIndicatorResultFormSchema(formApi) });
    // åŠ è½½æ•°æ®
    const data = modalApi.getData<CtxData>();
    ctxData.value = data;
    formType.value = data.formType;
@@ -142,17 +249,7 @@
        data.qcType,
        data.id,
      );
      // å›žå¡«æ•°å€¼å­—段(用于 InputNumber ç»‘定)
      items.value = (detail.items ?? []).map((item) => ({
        ...item,
        valueNumber:
          (item.valueType === MesQcResultValueType.FLOAT ||
            item.valueType === MesQcResultValueType.INTEGER) &&
          item.value !== null
            ? Number(item.value)
            : undefined,
      }));
      // è®¾ç½®åˆ° values
      roots.value = toEditItems(detail.items);
      await formApi.setValues({
        id: detail.id,
        code: detail.code,
@@ -164,6 +261,18 @@
    }
  },
});
/** å•个测量值输入控件绑定(数值型走 valueNumber,其余走 value) */
function bindMeasure(item: EditItem, measure: EditMeasure) {
  return isNumeric(item) ? measure.valueNumber : measure.value;
}
function onMeasureInput(item: EditItem, measure: EditMeasure, v: any) {
  if (isNumeric(item)) {
    measure.valueNumber = v === null ? undefined : Number(v);
  } else {
    measure.value = v;
  }
}
</script>
<template>
@@ -171,48 +280,182 @@
    <div class="px-4">
      <Form />
      <Divider>检测值</Divider>
      <!-- æ£€æµ‹å€¼æŽ§ä»¶ç”±æ¯è¡Œ valueType é©±åŠ¨ï¼Œé€è¡Œæ¸²æŸ“ï¼šFLOAT/INTEGER èµ° InputNumber,DICT èµ° Select å¹¶æŒ‰ valueSpecification è§£æžé€‰é¡¹ -->
      <AForm :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
        <div
          v-for="(item, index) in items"
          :key="item.indicatorId ?? index"
          class="mb-2"
        >
          <AForm.Item
            :label="`检测项${index + 1}:${item.indicatorName ?? ''}`"
          >
            <InputNumber
              v-if="
                item.valueType === MesQcResultValueType.FLOAT ||
                item.valueType === MesQcResultValueType.INTEGER
              "
              v-model:value="item.valueNumber"
              :precision="item.valueType === MesQcResultValueType.FLOAT ? 4 : 0"
              class="!w-full"
              placeholder="请输入"
            />
            <Textarea
              v-else-if="item.valueType === MesQcResultValueType.TEXT"
              v-model:value="item.value"
              :rows="2"
              placeholder="请输入检测值"
            />
            <Select
              v-else-if="item.valueType === MesQcResultValueType.DICT"
              v-model:value="item.value"
              allow-clear
              :options="getValueOptions(item.valueSpecification)"
              placeholder="请选择"
            />
            <Input
              v-else-if="item.valueType === MesQcResultValueType.FILE"
              v-model:value="item.value"
              placeholder="请输入文件地址"
            />
            <Input v-else v-model:value="item.value" placeholder="请输入" />
          </AForm.Item>
        </div>
      </AForm>
      <div v-if="roots.length === 0" class="py-4 text-center text-gray-400">
        è¯¥è´¨æ£€å•未配置检测指标
      </div>
      <div v-else class="qc-result-tree">
        <template v-for="(node, ni) in roots" :key="node.indicatorId ?? ni">
          <!-- åˆ†ç»„项:标题行 + å…¬å¼æ–‡æœ¬ï¼Œä¸‹æŒ‚子录入项 -->
          <div v-if="isGroup(node)">
            <div
              class="mb-2 flex items-center justify-between rounded bg-gray-100 px-3 py-1.5 font-medium"
            >
              <span>{{ node.indicatorName }}</span>
              <span v-if="node.formulaText" class="text-xs text-gray-500">
                {{ node.formulaText }}
              </span>
            </div>
            <div
              v-for="(child, ci) in node.children ?? []"
              :key="child.indicatorId ?? ci"
              class="mb-3 ml-4"
            >
              <div class="mb-1 flex items-center justify-between">
                <span class="font-medium">
                  {{ child.indicatorName }}
                  <span v-if="child.unitText" class="text-xs text-gray-500">
                    ï¼ˆ{{ child.unitText }})
                  </span>
                </span>
                <Button
                  size="small"
                  type="link"
                  @click="addMeasure(child)"
                >
                  + æµ‹é‡æ‰¹æ¬¡
                </Button>
              </div>
              <div
                v-for="(measure, mi) in child.measures"
                :key="mi"
                class="mb-1 flex items-center gap-2"
              >
                <span class="w-16 shrink-0 text-right text-xs text-gray-500">
                  ç¬¬ {{ measure.measureNo }} æ¬¡
                </span>
                <InputNumber
                  v-if="child.valueType === MesQcResultValueType.FLOAT || child.valueType === MesQcResultValueType.INTEGER"
                  :value="bindMeasure(child, measure)"
                  :precision="child.valueType === MesQcResultValueType.FLOAT ? 4 : 0"
                  class="!w-48"
                  placeholder="请输入"
                  @update:value="(v) => onMeasureInput(child, measure, v)"
                />
                <Textarea
                  v-else-if="child.valueType === MesQcResultValueType.TEXT"
                  v-model:value="measure.value"
                  :rows="1"
                  class="!w-48"
                  placeholder="请输入检测值"
                />
                <Select
                  v-else-if="child.valueType === MesQcResultValueType.DICT"
                  v-model:value="measure.value"
                  allow-clear
                  class="!w-48"
                  :options="getValueOptions(child.valueSpecification)"
                  placeholder="请选择"
                />
                <Input
                  v-else-if="child.valueType === MesQcResultValueType.FILE"
                  v-model:value="measure.value"
                  class="!w-48"
                  placeholder="请输入文件地址"
                />
                <Input
                  v-else
                  v-model:value="measure.value"
                  class="!w-48"
                  placeholder="请输入"
                />
                <Input
                  v-model:value="measure.remark"
                  class="flex-1"
                  placeholder="备注"
                />
                <Button
                  size="small"
                  type="text"
                  danger
                  :disabled="child.measures.length <= 1"
                  @click="removeMeasure(child, mi)"
                >
                  åˆ é™¤
                </Button>
              </div>
            </div>
          </div>
          <!-- é¡¶çº§å½•入项 -->
          <div v-else class="mb-3">
            <div class="mb-1 flex items-center justify-between">
              <span class="font-medium">
                {{ node.indicatorName }}
                <span v-if="node.unitText" class="text-xs text-gray-500">
                  ï¼ˆ{{ node.unitText }})
                </span>
              </span>
              <Button size="small" type="link" @click="addMeasure(node)">
                + æµ‹é‡æ‰¹æ¬¡
              </Button>
            </div>
            <div
              v-for="(measure, mi) in node.measures"
              :key="mi"
              class="mb-1 flex items-center gap-2"
            >
              <span class="w-16 shrink-0 text-right text-xs text-gray-500">
                ç¬¬ {{ measure.measureNo }} æ¬¡
              </span>
              <InputNumber
                v-if="node.valueType === MesQcResultValueType.FLOAT || node.valueType === MesQcResultValueType.INTEGER"
                :value="bindMeasure(node, measure)"
                :precision="node.valueType === MesQcResultValueType.FLOAT ? 4 : 0"
                class="!w-48"
                placeholder="请输入"
                @update:value="(v) => onMeasureInput(node, measure, v)"
              />
              <Textarea
                v-else-if="node.valueType === MesQcResultValueType.TEXT"
                v-model:value="measure.value"
                :rows="1"
                class="!w-48"
                placeholder="请输入检测值"
              />
              <Select
                v-else-if="node.valueType === MesQcResultValueType.DICT"
                v-model:value="measure.value"
                allow-clear
                class="!w-48"
                :options="getValueOptions(node.valueSpecification)"
                placeholder="请选择"
              />
              <Input
                v-else-if="node.valueType === MesQcResultValueType.FILE"
                v-model:value="measure.value"
                class="!w-48"
                placeholder="请输入文件地址"
              />
              <Input
                v-else
                v-model:value="measure.value"
                class="!w-48"
                placeholder="请输入"
              />
              <Input
                v-model:value="measure.remark"
                class="flex-1"
                placeholder="备注"
              />
              <Button
                size="small"
                type="text"
                danger
                :disabled="node.measures.length <= 1"
                @click="removeMeasure(node, mi)"
              >
                åˆ é™¤
              </Button>
            </div>
          </div>
        </template>
      </div>
    </div>
  </Modal>
</template>
<style scoped>
.qc-result-tree {
  max-height: 60vh;
  overflow-y: auto;
}
</style>
src/views/mes/qc/ipqc/data.ts
@@ -369,6 +369,57 @@
      rules: 'required',
    },
    {
      fieldName: 'samplingLocation',
      label: '采样地点',
      component: 'Input',
      componentProps: {
        maxlength: 100,
        placeholder: '请输入采样地点',
      },
    },
    {
      fieldName: 'certificateNo',
      label: '证书编号',
      component: 'Input',
      componentProps: {
        maxlength: 50,
        placeholder: '请输入证书编号',
      },
    },
    {
      fieldName: 'executeStandard',
      label: '执行标准',
      component: 'Input',
      componentProps: {
        maxlength: 50,
        placeholder: '如 GB/T 6027,选择质检方案后自动带入',
      },
    },
    {
      fieldName: 'auditUserId',
      label: '复核人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        placeholder: '请选择复核人',
        valueField: 'id',
      },
    },
    {
      fieldName: 'approveUserId',
      label: '审核人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        placeholder: '请选择审核人',
        valueField: 'id',
      },
    },
    {
      fieldName: 'checkResult',
      label: '检测结果',
      component: 'Select',
src/views/mes/qc/ipqc/index.vue
@@ -2,8 +2,10 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcIpqcApi } from '#/api/mes/qc/ipqc';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum } from '@vben/constants';
import { MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { Button, message } from 'ant-design-vue';
@@ -11,6 +13,7 @@
import { deleteIpqc, exportIpqc, getIpqcPage } from '#/api/mes/qc/ipqc';
import { $t } from '#/locales';
import QcExportRangeModal from '#/views/mes/qc/components/export-range-modal.vue';
import QcReportPrint from '#/views/mes/qc/components/qc-report-print.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
@@ -25,9 +28,16 @@
  destroyOnClose: true,
});
const printRef = ref<InstanceType<typeof QcReportPrint> | null>(null);
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ‰“印检验报告 */
async function handlePrint(row: MesQcIpqcApi.Ipqc) {
  await printRef.value?.print(row.id!, MesQcTypeEnum.IPQC);
}
/** åˆ›å»ºè¿‡ç¨‹æ£€éªŒå• */
@@ -106,6 +116,7 @@
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ExportRangeModal />
    <QcReportPrint ref="printRef" />
    <Grid table-title="过程检验单列表">
      <template #toolbar-tools>
        <TableAction
@@ -156,6 +167,13 @@
              },
            },
            {
              label: '打印报告',
              type: 'link',
              icon: 'ant-design:printer-outlined',
              auth: ['mes:qc-ipqc:query'],
              onClick: handlePrint.bind(null, row),
            },
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
src/views/mes/qc/iqc/data.ts
@@ -274,6 +274,57 @@
      rules: 'required',
    },
    {
      fieldName: 'samplingLocation',
      label: '采样地点',
      component: 'Input',
      componentProps: {
        maxlength: 100,
        placeholder: '请输入采样地点',
      },
    },
    {
      fieldName: 'certificateNo',
      label: '证书编号',
      component: 'Input',
      componentProps: {
        maxlength: 50,
        placeholder: '请输入证书编号',
      },
    },
    {
      fieldName: 'executeStandard',
      label: '执行标准',
      component: 'Input',
      componentProps: {
        maxlength: 50,
        placeholder: '如 GB/T 6027,选择质检方案后自动带入',
      },
    },
    {
      fieldName: 'auditUserId',
      label: '复核人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        placeholder: '请选择复核人',
        valueField: 'id',
      },
    },
    {
      fieldName: 'approveUserId',
      label: '审核人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        placeholder: '请选择审核人',
        valueField: 'id',
      },
    },
    {
      fieldName: 'checkResult',
      label: '检测结果',
      component: 'Select',
src/views/mes/qc/iqc/index.vue
@@ -2,11 +2,11 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcIqcApi } from '#/api/mes/qc/iqc';
import { onMounted } from 'vue';
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum } from '@vben/constants';
import { MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { Button, message } from 'ant-design-vue';
@@ -14,6 +14,7 @@
import { deleteIqc, exportIqc, getIqcPage } from '#/api/mes/qc/iqc';
import { $t } from '#/locales';
import QcExportRangeModal from '#/views/mes/qc/components/export-range-modal.vue';
import QcReportPrint from '#/views/mes/qc/components/qc-report-print.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
@@ -21,6 +22,7 @@
defineOptions({ name: 'MesQcIqc' });
const route = useRoute();
const printRef = ref<InstanceType<typeof QcReportPrint> | null>(null);
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
@@ -65,6 +67,11 @@
  } finally {
    hideLoading();
  }
}
/** æ‰“印检验报告 */
async function handlePrint(row: MesQcIqcApi.Iqc) {
  await printRef.value?.print(row.id!, MesQcTypeEnum.IQC);
}
/** å¯¼å‡ºè¡¨æ ¼ï¼ˆå¼¹çª—选检验时间范围) */
@@ -120,6 +127,7 @@
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ExportRangeModal />
    <QcReportPrint ref="printRef" />
    <Grid table-title="来料检验单列表">
      <template #toolbar-tools>
        <TableAction
@@ -170,6 +178,13 @@
              },
            },
            {
              label: '打印报告',
              type: 'link',
              icon: 'ant-design:printer-outlined',
              auth: ['mes:qc-iqc:query'],
              onClick: handlePrint.bind(null, row),
            },
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
src/views/mes/qc/oqc/data.ts
@@ -300,6 +300,57 @@
      rules: 'required',
    },
    {
      fieldName: 'samplingLocation',
      label: '采样地点',
      component: 'Input',
      componentProps: {
        maxlength: 100,
        placeholder: '请输入采样地点',
      },
    },
    {
      fieldName: 'certificateNo',
      label: '证书编号',
      component: 'Input',
      componentProps: {
        maxlength: 50,
        placeholder: '请输入证书编号',
      },
    },
    {
      fieldName: 'executeStandard',
      label: '执行标准',
      component: 'Input',
      componentProps: {
        maxlength: 50,
        placeholder: '如 GB/T 6027,选择质检方案后自动带入',
      },
    },
    {
      fieldName: 'auditUserId',
      label: '复核人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        placeholder: '请选择复核人',
        valueField: 'id',
      },
    },
    {
      fieldName: 'approveUserId',
      label: '审核人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        placeholder: '请选择审核人',
        valueField: 'id',
      },
    },
    {
      fieldName: 'checkResult',
      label: '检测结果',
      component: 'Select',
src/views/mes/qc/oqc/index.vue
@@ -2,8 +2,10 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcOqcApi } from '#/api/mes/qc/oqc';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum } from '@vben/constants';
import { MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { Button, message } from 'ant-design-vue';
@@ -11,6 +13,7 @@
import { deleteOqc, exportOqc, getOqcPage } from '#/api/mes/qc/oqc';
import { $t } from '#/locales';
import QcExportRangeModal from '#/views/mes/qc/components/export-range-modal.vue';
import QcReportPrint from '#/views/mes/qc/components/qc-report-print.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
@@ -25,9 +28,16 @@
  destroyOnClose: true,
});
const printRef = ref<InstanceType<typeof QcReportPrint> | null>(null);
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ‰“印检验报告 */
async function handlePrint(row: MesQcOqcApi.Oqc) {
  await printRef.value?.print(row.id!, MesQcTypeEnum.OQC);
}
/** åˆ›å»ºå‡ºè´§æ£€éªŒå• */
@@ -106,6 +116,7 @@
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ExportRangeModal />
    <QcReportPrint ref="printRef" />
    <Grid table-title="出货检验单列表">
      <template #toolbar-tools>
        <TableAction
@@ -156,6 +167,13 @@
              },
            },
            {
              label: '打印报告',
              type: 'link',
              icon: 'ant-design:printer-outlined',
              auth: ['mes:qc-oqc:query'],
              onClick: handlePrint.bind(null, row),
            },
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
src/views/mes/qc/rqc/data.ts
@@ -267,6 +267,57 @@
      rules: 'selectRequired',
    },
    {
      fieldName: 'samplingLocation',
      label: '采样地点',
      component: 'Input',
      componentProps: {
        maxlength: 100,
        placeholder: '请输入采样地点',
      },
    },
    {
      fieldName: 'certificateNo',
      label: '证书编号',
      component: 'Input',
      componentProps: {
        maxlength: 50,
        placeholder: '请输入证书编号',
      },
    },
    {
      fieldName: 'executeStandard',
      label: '执行标准',
      component: 'Input',
      componentProps: {
        maxlength: 50,
        placeholder: '如 GB/T 6027,选择质检方案后自动带入',
      },
    },
    {
      fieldName: 'auditUserId',
      label: '复核人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        placeholder: '请选择复核人',
        valueField: 'id',
      },
    },
    {
      fieldName: 'approveUserId',
      label: '审核人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        placeholder: '请选择审核人',
        valueField: 'id',
      },
    },
    {
      fieldName: 'checkResult',
      label: '检测结果',
      component: 'Select',
src/views/mes/qc/rqc/index.vue
@@ -2,8 +2,10 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcRqcApi } from '#/api/mes/qc/rqc';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum } from '@vben/constants';
import { MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { Button, message } from 'ant-design-vue';
@@ -11,6 +13,7 @@
import { deleteRqc, exportRqc, getRqcPage } from '#/api/mes/qc/rqc';
import { $t } from '#/locales';
import QcExportRangeModal from '#/views/mes/qc/components/export-range-modal.vue';
import QcReportPrint from '#/views/mes/qc/components/qc-report-print.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
@@ -25,9 +28,16 @@
  destroyOnClose: true,
});
const printRef = ref<InstanceType<typeof QcReportPrint> | null>(null);
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ‰“印检验报告 */
async function handlePrint(row: MesQcRqcApi.Rqc) {
  await printRef.value?.print(row.id!, MesQcTypeEnum.RQC);
}
/** åˆ›å»ºé€€è´§æ£€éªŒå• */
@@ -106,6 +116,7 @@
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ExportRangeModal />
    <QcReportPrint ref="printRef" />
    <Grid table-title="退货检验单列表">
      <template #toolbar-tools>
        <TableAction
@@ -156,6 +167,13 @@
              },
            },
            {
              label: '打印报告',
              type: 'link',
              icon: 'ant-design:printer-outlined',
              auth: ['mes:qc-rqc:query'],
              onClick: handlePrint.bind(null, row),
            },
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
src/views/mes/qc/template/data.ts
@@ -91,6 +91,15 @@
      rules: z.array(z.number()).min(1, '检测种类不能为空'),
    },
    {
      fieldName: 'defaultExecuteStandard',
      label: '默认执行标准',
      component: 'Input',
      componentProps: {
        maxlength: 50,
        placeholder: '如 GB/T 6027,新建检验单选择本方案后自动带入执行标准',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
@@ -249,6 +258,17 @@
      width: 100,
    },
    {
      field: 'sortOrder',
      title: '排序号',
      width: 80,
    },
    {
      field: 'judgeFlag',
      title: '参与判定',
      width: 90,
      slots: { default: 'judgeFlag' },
    },
    {
      title: '操作',
      width: 130,
      fixed: 'right',
@@ -336,6 +356,29 @@
      },
    },
    {
      fieldName: 'sortOrder',
      label: '排序号',
      component: 'InputNumber',
      defaultValue: 0,
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 0,
        placeholder: '方案内展示顺序,小的在前',
      },
    },
    {
      fieldName: 'judgeFlag',
      label: '参与判定',
      component: 'Switch',
      defaultValue: true,
      componentProps: {
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
      help: '分组行/中间参数行设为否,仅最终结果项参与合格判定提示',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
src/views/mes/qc/template/modules/indicator-list.vue
@@ -4,12 +4,13 @@
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { message, Switch } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteTemplateIndicator,
  getTemplateIndicatorPage,
  updateTemplateIndicator,
} from '#/api/mes/qc/template/indicator';
import { $t } from '#/locales';
@@ -39,6 +40,16 @@
/** ç¼–辑指标项 */
function handleEdit(row: MesQcTemplateIndicatorApi.TemplateIndicator) {
  formModalApi.setData({ ...row, templateId: props.templateId }).open();
}
/** åˆ‡æ¢æ˜¯å¦å‚与判定 */
async function handleJudgeFlagChange(
  row: MesQcTemplateIndicatorApi.TemplateIndicator,
  checked: boolean,
) {
  await updateTemplateIndicator({ ...row, judgeFlag: checked });
  message.success(`检测项[${row.indicatorName}]å·²${checked ? '参与' : '不参与'}判定`);
  handleRefresh();
}
/** åˆ é™¤æŒ‡æ ‡é¡¹ */
@@ -100,6 +111,17 @@
          ]"
        />
      </template>
      <template #judgeFlag="{ row }">
        <Switch
          :checked="row.judgeFlag !== false"
          :disabled="readonly"
          checked-children="是"
          un-checked-children="否"
          @change="
            (checked: boolean) => handleJudgeFlagChange(row, checked)
          "
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[