gaoluyang
4 天以前 4f894ae17a8c2d9c8f5d1a7161415a728c5170b5
Merge remote-tracking branch 'origin/dev_南通_苏珩' into dev_南通_苏珩
已添加5个文件
已修改26个文件
1081 ■■■■■ 文件已修改
src/api/mdm/item/index.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/task/index.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/source-link/index.ts 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/productissue/line/index.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/productsales/line/index.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/upload/file-upload.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/upload/typing.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/modules/form.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/detail/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/data.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/modules/item-form.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/mps/modules/form.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/modules/form.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/modules/scrap-modal.vue 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/modules/technical-files.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/components/qc-attachments.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/components/qc-source-link.vue 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ipqc/data.ts 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ipqc/modules/form.vue 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/iqc/data.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/iqc/modules/form.vue 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/data.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/modules/form.vue 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/rqc/data.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/rqc/modules/form.vue 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/WorkstationSelectDialog.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/ProductIssueTab.vue 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productsales/data.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productsales/modules/form.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mdm/item/index.ts
@@ -151,3 +151,8 @@
    params: { itemId },
  });
}
/** ä»Žç‰©æ–™ç§»é™¤æŠ€æœ¯æ–‡ä»¶ï¼›å·²ç”Ÿæˆå·¥å•仍保留文件快照。 */
export function deleteTechnicalFile(itemId: number, id: number) {
  return requestClient.delete('/mdm/item/technical-file/delete', { params: { itemId, id } });
}
src/api/mes/pro/task/index.ts
@@ -234,6 +234,13 @@
  );
}
/** æŒ‰å·¥ä½œç«™ç»Ÿè®¡å½“前用户可见的未完成任务。 */
export function getUnfinishedWorkstationCounts() {
  return requestClient.get<Array<{ workstationId: number; pendingCount: number }>>(
    '/mes/pro/task/unfinished-workstation-counts',
  );
}
/** æ ¹æ®å·¥ä½œç«™å’Œæ•°é‡è®¡ç®—生产时长 */
export function calculateTaskDuration(workstationId: number, quantity: number) {
  return requestClient.get<{ duration: number }>(
src/api/mes/qc/source-link/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
import { requestClient } from '#/api/request';
export namespace MesQcSourceLinkApi {
  /** å…³è”类型:feedback = ç”Ÿäº§æŠ¥å·¥ï¼Œsales = é”€å”®å‡ºåº“ */
  export type LinkType = 'feedback' | 'sales';
  /** å¯å…³è”的待检来源明细候选 */
  export interface Candidate {
    lineId: number; // å¾…检明细 ID,提交关联时回传
    documentId: number; // æ¥æºå•据 ID
    documentCode: string; // æ¥æºå•据编号
    reelNo: string; // ç›˜å·
    quantity: number; // å¾…检数量
  }
}
/** æŸ¥è¯¢å¯å…³è”的待检来源候选 */
export function getSourceLinkCandidates(
  type: MesQcSourceLinkApi.LinkType,
  qcId: number,
) {
  return requestClient.get<MesQcSourceLinkApi.Candidate[]>(
    `/mes/qc/source-link/${type}-candidates`,
    { params: { qcId } },
  );
}
/** å°†è´¨æ£€å•关联到指定来源明细 */
export function linkSource(
  type: MesQcSourceLinkApi.LinkType,
  qcId: number,
  lineId: number,
) {
  return requestClient.post<boolean>(
    `/mes/qc/source-link/${type}`,
    null,
    { params: { lineId, qcId } },
  );
}
src/api/mes/wm/productissue/line/index.ts
@@ -36,6 +36,14 @@
  >('/mes/wm/product-issue-line/page', { params });
}
/** æŸ¥è¯¢é¢†æ–™å•全部明细,用于打印。 */
export function getProductIssueLinesByIssue(issueId: number) {
  return requestClient.get<MesWmProductIssueLineApi.ProductIssueLine[]>(
    '/mes/wm/product-issue-line/list-by-issue',
    { params: { issueId } },
  );
}
/** æŸ¥è¯¢é¢†æ–™å‡ºåº“单行详情 */
export function getProductIssueLine(id: number) {
  return requestClient.get<MesWmProductIssueLineApi.ProductIssueLine>(
src/api/mes/wm/productsales/line/index.ts
@@ -19,7 +19,9 @@
    batchId?: number; // æ‰¹æ¬¡ç¼–号
    batchCode?: string; // æ‰¹æ¬¡å·
    reelNo?: string; // é”€å”®æ˜Žç»†ç›˜å·
    oqcCheckFlag?: boolean; // æ˜¯å¦æ£€éªŒ
    oqcCheckFlag?: boolean; // æ˜¯å¦éœ€è¦å‡ºè´§æ£€éªŒ
    oqcId?: number; // å…³è”的出货检验单 ID
    qualityStatus?: number; // è´¨é‡çŠ¶æ€ï¼ˆå­—å…¸ mes_wm_quality_status)
    remark?: string; // å¤‡æ³¨
  }
}
src/components/upload/file-upload.vue
@@ -77,7 +77,7 @@
      isInnerOperate.value = false;
      return;
    }
    let value: string[] = [];
    let value: Array<number | string> = [];
    if (v) {
      if (Array.isArray(v)) {
        value = v;
src/components/upload/typing.ts
@@ -23,14 +23,14 @@
  helpText?: string;
  listType?: UploadListType;
  maxNumber?: number; // æœ€å¤§æ•°é‡çš„æ–‡ä»¶ï¼ŒInfinity不限制
  modelValue?: string | string[] | number[]; // v-model æ”¯æŒ
  modelValue?: string | number | string[] | number[]; // v-model æ”¯æŒ
  maxSize?: number; // æ–‡ä»¶æœ€å¤§å¤šå°‘MB
  multiple?: boolean; // æ˜¯å¦æ”¯æŒå¤šé€‰
  resultField?: string; // support xxx.xxx.xx
  returnText?: boolean; // æ˜¯å¦è¿”回文件文本内容
  showDescription?: boolean; // æ˜¯å¦æ˜¾ç¤ºä¸‹é¢çš„æè¿°
  showDownloadIcon?: boolean; // æ˜¯å¦æ˜¾ç¤ºä¸‹è½½æŒ‰é’®
  value?: string | string[] | number[];
  value?: string | number | string[] | number[];
  valueKey?: string; // è¿”回值的字段名,默认 'url',设为 'id' æ—¶è¿”回 blobId
  valuePrefix?: string; // valueKey è¿”回值的前缀
}
src/views/basicData/mdm/modules/form.vue
@@ -3,12 +3,13 @@
import { useVbenModal } from "#/packages/effects/common-ui/src";
import { message } from "ant-design-vue";
import { message, Popconfirm } from "ant-design-vue";
import { useVbenForm } from '#/adapter/form';
import {
  createItem,
  createTechnicalFile,
  deleteTechnicalFile,
  getItem,
  getTechnicalFileList,
  updateItem,
@@ -25,7 +26,7 @@
const emit = defineEmits(['success']);
const formData = ref<MdmItemApi.Item>();
const technicalFiles = ref<MdmItemApi.TechnicalFile[]>([]);
const technicalUploadValue = ref<number[]>([]);
const technicalUploadValue = ref<number | number[] | string>([]);
const [Form, formApi] = useVbenForm({
  commonConfig: {
@@ -95,20 +96,31 @@
});
async function handleTechnicalFileChange(value: unknown) {
  if (!formData.value?.id || !Array.isArray(value)) {
  if (!formData.value?.id) {
    return;
  }
  const blobId = value.find((item): item is number => typeof item === 'number');
  if (!blobId) {
  // å•文件上传组件返回单个 ID;兼容数组和数字字符串。
  const rawId = Array.isArray(value) ? value[0] : value;
  const blobId = typeof rawId === 'number' ? rawId :
    typeof rawId === 'string' && /^\d+$/.test(rawId) ? Number(rawId) : 0;
  if (!Number.isSafeInteger(blobId) || blobId <= 0) {
    return;
  }
  try {
    await createTechnicalFile(formData.value.id, blobId);
    technicalFiles.value = await getTechnicalFileList(formData.value.id);
    message.success('技术文件已保存,历史版本已保留');
    message.success('技术文件已添加');
  } finally {
    technicalUploadValue.value = [];
  }
}
async function handleDeleteTechnicalFile(id: number) {
  const itemId = formData.value?.id;
  if (!itemId) return;
  await deleteTechnicalFile(itemId, id);
  technicalFiles.value = await getTechnicalFileList(itemId);
  message.success('技术文件已移除');
}
</script>
@@ -116,14 +128,14 @@
  <Modal class="w-[1000px]" :title="formData?.id ? $t('ui.actionTitle.edit', ['物料']) : $t('ui.actionTitle.create', ['物料'])">
    <Form class="mx-4" />
    <div v-if="formData?.id" class="mx-4 mb-4 rounded border p-4">
      <div class="mb-3 text-base font-medium">技术文件历史</div>
      <div class="mb-3 text-base font-medium">技术文件</div>
      <FileUpload
        v-model:value="technicalUploadValue"
        value-key="id"
        :max-number="1"
        :multiple="false"
        :accept="['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.dwg', '.png', '.jpg', '.jpeg']"
        help-text="上传新文件会生成新的版本,历史文件仍可查看"
        :accept="['pdf', 'doc', 'docx', 'xls', 'xlsx', 'dwg', 'png', 'jpg', 'jpeg']"
        help-text="每次上传会追加一份技术文件,原文件继续保留"
        @change="handleTechnicalFileChange"
      />
      <a-table
@@ -133,12 +145,15 @@
        :pagination="false"
        row-key="id"
      >
        <a-table-column key="versionNo" title="版本" data-index="versionNo" />
        <a-table-column key="versionNo" title="上传序号" data-index="versionNo" />
        <a-table-column key="fileName" title="文件名" data-index="fileName" />
        <a-table-column key="createTime" title="上传时间" data-index="createTime" />
        <a-table-column key="action" title="操作">
          <template #default="{ record }">
            <a :href="record.downloadURL || record.previewURL" target="_blank">查看文件</a>
            <Popconfirm title="移除该技术文件?已有工单仍保留原文件,新工单将不再带入。" ok-text="移除" cancel-text="取消" @confirm="handleDeleteTechnicalFile(record.id)">
              <a class="ml-3 text-red-500">删除</a>
            </Popconfirm>
          </template>
        </a-table-column>
      </a-table>
src/views/crm/contract/detail/index.vue
@@ -6,6 +6,7 @@
import { useRoute, useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { useAccess } from '@vben/access';
import { useTabs } from '@vben/hooks';
import { Card, Tabs } from 'ant-design-vue';
@@ -29,6 +30,7 @@
import ContractDetailsInfo from './modules/info.vue';
const props = defineProps<{ id?: number }>();
const { hasAccessByRoles } = useAccess();
const route = useRoute();
const router = useRouter();
@@ -123,7 +125,7 @@
            label: '导出 Word',
            type: 'primary',
            icon: 'lucide:download',
            auth: ['crm:contract:query'],
            auth: hasAccessByRoles(['super_admin']) ? [] : ['crm:contract:query'],
            onClick: () => downloadContractWord(contractId, contract.no),
          },
          {
src/views/crm/contract/index.vue
@@ -6,6 +6,7 @@
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { useAccess } from '@vben/access';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Modal, Select, Tabs, Tag, Tooltip } from 'ant-design-vue';
@@ -28,6 +29,7 @@
import ImportForm from './modules/import-form.vue';
const { push } = useRouter();
const { hasAccessByRoles } = useAccess();
const sceneType = ref('1');
const [FormModal, formModalApi] = useVbenModal({
@@ -336,7 +338,7 @@
              label: '导出 Word',
              type: 'link',
              icon: 'lucide:download',
              auth: ['crm:contract:query'],
              auth: hasAccessByRoles(['super_admin']) ? [] : ['crm:contract:query'],
              onClick: () => downloadContractWord(row.id, row.no),
            },
            {
src/views/erp/sale/order/data.ts
@@ -325,13 +325,6 @@
      formatter: 'formatAmount2',
    },
    {
      field: 'totalPrice',
      title: '税额合计',
      minWidth: 120,
      fixed: 'right',
      formatter: 'formatAmount2',
    },
    {
      title: '操作',
      width: 80,
      fixed: 'right',
src/views/erp/sale/order/modules/item-form.vue
@@ -48,10 +48,6 @@
      (sum, item) => sum + (item.taxPrice || 0),
      0,
    ),
    totalPrice: tableData.value.reduce(
      (sum, item) => sum + (item.totalPrice || 0),
      0,
    ),
  };
});
@@ -325,9 +321,6 @@
              é‡‘额:{{ erpPriceInputFormatter(summaries.totalProductPrice) }}
            </span>
            <span>税额:{{ erpPriceInputFormatter(summaries.taxPrice) }}</span>
            <span>
              ç¨Žé¢åˆè®¡ï¼š{{ erpPriceInputFormatter(summaries.totalPrice) }}
            </span>
          </div>
        </div>
      </div>
src/views/mes/pro/mps/modules/form.vue
@@ -12,6 +12,7 @@
import { useVbenForm } from '#/adapter/form';
import { createMps, getMps, updateMps } from '#/api/mes/pro/mps';
import { $t } from '#/locales';
import TechnicalFiles from '../../workorder/modules/technical-files.vue';
import PredictDelivery from '../../workorder/modules/ai/predict-delivery.vue';
import PredictDuration from '../../workorder/modules/ai/predict-duration.vue';
@@ -22,6 +23,7 @@
const emit = defineEmits(['success']);
const formData = ref<MesProMpsApi.Mps>();
const formType = ref<FormType>('detail');
const selectedProductId = ref<number>();
const isEditable = computed(() => ['create', 'edit'].includes(formType.value));
@@ -50,6 +52,9 @@
  schema: [],
  showDefaultActions: false,
  wrapperClass: 'grid-cols-3',
  handleValuesChange: (values, changedFields) => {
    if (changedFields.includes('productId')) selectedProductId.value = values.productId;
  },
});
/** é‡æ–°æŒ‚è½½ schema å¹¶æŒ‰è¡¨å•态控制底部确认按钮 */
@@ -86,6 +91,7 @@
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      selectedProductId.value = undefined;
      return;
    }
    // åŠ è½½æ•°æ®
@@ -96,6 +102,7 @@
      modalApi.lock();
      try {
        formData.value = await getMps(data.id);
        selectedProductId.value = formData.value.productId;
        await formApi.setValues(formData.value);
      } finally {
        modalApi.unlock();
@@ -108,6 +115,10 @@
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
    <div class="mx-4 mb-4 rounded border p-4">
      <div class="mb-3 font-medium">技术文件</div>
      <TechnicalFiles :work-order-id="formData?.workOrderId" :item-id="selectedProductId" />
    </div>
    <!-- è¯¦æƒ…模式额外展示只读信息 -->
    <div v-if="formType === 'detail' && formData" class="mx-4">
      <Descriptions bordered :column="2">
src/views/mes/pro/workorder/index.vue
@@ -26,6 +26,7 @@
  import { useGridColumns, useGridFormSchema } from "./data";
  import Form from "./modules/form.vue";
  import ScrapModal from "./modules/scrap-modal.vue";
  const [FormModal, formModalApi] = useVbenModal({
    connectedComponent: Form,
@@ -34,6 +35,7 @@
  const archiveVisible = ref(false);
  const archiveProductCode = ref('');
  const scrapModalRef = ref<InstanceType<typeof ScrapModal>>();
// const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // æ¡ç è¯¦æƒ…弹窗
  /** åˆ·æ–°è¡¨æ ¼ */
@@ -279,6 +281,13 @@
            //   onClick: handleBarcode.bind(null, row),
            // },
            {
              label: '废料',
              type: 'link',
              auth: ['mes:wm-scrap-receipt:create'],
              ifShow: row.status === MesProWorkOrderStatusEnum.FINISHED,
              onClick: () => scrapModalRef?.open(row.id, row.code),
            },
            {
              label: '归档',
              type: 'link',
              onClick: handleArchive.bind(null, row),
@@ -288,6 +297,7 @@
    </Grid>
    <!-- <BarcodeDetail ref="barcodeDetailRef" /> -->
    <ArchiveDetail :visible="archiveVisible" :product-code="archiveProductCode" @update:visible="archiveVisible = $event" />
    <ScrapModal ref="scrapModalRef" @success="handleRefresh" />
  </Page>
</template>
src/views/mes/pro/workorder/modules/form.vue
@@ -33,6 +33,7 @@
  import PredictRisk from "./ai/predict-risk.vue";
  import ItemList from "./item-list.vue";
  import ProcessList from "./process-list.vue";
  import TechnicalFiles from "./technical-files.vue";
  const emit = defineEmits(["success"]);
  const formType = ref<FormType>("create");
@@ -367,6 +368,10 @@
            @process-list-change="handleProcessListChange"
          />
        </Tabs.TabPane>
        <Tabs.TabPane key="technical-files" tab="技术文件">
          <TechnicalFiles v-if="formData?.id" :work-order-id="formData.id" :key="formData.id" />
          <div v-else class="text-gray-400 text-center py-8">保存工单时将留存所选产品的技术文件</div>
        </Tabs.TabPane>
        <Tabs.TabPane key="item"
                      tab="物料需求">
          <p class="mb-3 text-xs text-gray-400 leading-relaxed">
src/views/mes/pro/workorder/modules/scrap-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Button, Form, FormItem, Input, InputNumber, Modal, Select, message } from 'ant-design-vue';
import { requestClient } from '#/api/request';
import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
import { WmWarehouseAreaSelect, WmWarehouseLocationSelect } from '#/views/wls/warehouse/components';
const emit = defineEmits<{ success: [] }>();
const visible = ref(false);
const busy = ref(false);
const orderId = ref<number>();
const orderCode = ref('');
const requestId = ref('');
const outputs = ref<Array<{ itemId: number; itemCode?: string; itemName?: string; quantity: number; batchId?: number; batchCode?: string }>>([]);
const warehouses = ref<Array<{ id?: number; name?: string; warehouseType?: string; frozen?: boolean }>>([]);
const itemId = ref<number>();
const selectedOutput = ref<number>();
const batchId = ref<number>();
const quantity = ref<number>();
const warehouseId = ref<number>();
const locationId = ref<number>();
const areaId = ref<number>();
const batchCode = ref('');
const options = computed(() => outputs.value.map((item, index) => ({ value: index, label: `${item.itemName || item.itemCode || item.itemId}${item.batchCode ? ` Â· æ‰¹æ¬¡ ${item.batchCode}` : ''}(产出 ${item.quantity})` })));
function selectOutput(index: number) {
  const output = outputs.value[index];
  itemId.value = output?.itemId; batchId.value = output?.batchId; batchCode.value = output?.batchCode || '';
}
async function open(id: number, code?: string) {
  orderId.value = id; orderCode.value = code || ''; itemId.value = undefined; selectedOutput.value = undefined; batchId.value = undefined; quantity.value = undefined;
  warehouseId.value = undefined; locationId.value = undefined; areaId.value = undefined; batchCode.value = '';
  requestId.value = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
  outputs.value = []; visible.value = true; busy.value = true;
  try {
    const [items, list] = await Promise.all([
      requestClient.get<typeof outputs.value>('/mes/wm/scrap-receipt/order-outputs', { params: { workOrderId: id } }),
      getWarehouseSimpleList(),
    ]);
    outputs.value = items || [];
    warehouses.value = (list || []).filter(w => ['废料仓', '废料'].includes(w.warehouseType || '') && !w.frozen);
    if (!outputs.value.length) message.warning('该订单没有已完成的产出物料');
    if (!warehouses.value.length) message.warning('请先在仓库设置中将废料仓的仓库类型设为“废料仓”');
  } finally { busy.value = false; }
}
async function submit() {
  if (!orderId.value || !itemId.value || !quantity.value || quantity.value <= 0 || !warehouseId.value || !locationId.value || !areaId.value) {
    message.warning('请选择产出物料、数量以及废料仓库和库位'); return;
  }
  busy.value = true;
  try {
    await requestClient.post('/mes/wm/scrap-receipt/create-from-output', {
      requestId: requestId.value, workOrderId: orderId.value, itemId: itemId.value,
      quantity: quantity.value, warehouseId: warehouseId.value, locationId: locationId.value,
      areaId: areaId.value, batchId: batchId.value, batchCode: batchCode.value || undefined,
    });
    message.success('废料已入库'); visible.value = false; emit('success');
  } finally { busy.value = false; }
}
defineExpose({ open });
</script>
<template>
  <Modal v-model:open="visible" title="废料入库" :confirm-loading="busy" :mask-closable="false" @ok="submit">
    <Form layout="vertical">
      <FormItem label="生产订单"><Input :value="orderCode" disabled /></FormItem>
      <FormItem label="产出物料" required><Select v-model:value="selectedOutput" :options="options" placeholder="请选择订单产出物料" @change="selectOutput" /></FormItem>
      <FormItem label="废料数量" required><InputNumber v-model:value="quantity" :min="0.0001" :precision="4" class="w-full" /></FormItem>
      <FormItem label="废料仓库" required><Select v-model:value="warehouseId" :options="warehouses.map(w => ({value: w.id, label: w.name}))" placeholder="请选择废料仓库" @change="locationId = undefined; areaId = undefined" /></FormItem>
      <FormItem label="库区" required><WmWarehouseLocationSelect v-model="locationId" :warehouse-id="warehouseId" @change="areaId = undefined" /></FormItem>
      <FormItem label="库位" required><WmWarehouseAreaSelect v-model="areaId" :location-id="locationId" /></FormItem>
      <FormItem label="批次号"><Input :value="batchCode || '无'" disabled /></FormItem>
    </Form>
    <template #footer><Button @click="visible = false">取消</Button><Button type="primary" :loading="busy" :disabled="busy" @click="submit">确认入库</Button></template>
  </Modal>
</template>
src/views/mes/pro/workorder/modules/technical-files.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { Button, Empty, Space, Table } from 'ant-design-vue';
import { requestClient } from '#/api/request';
const props = defineProps<{ workOrderId?: number; itemId?: number }>();
interface TechnicalFile {
  id: number;
  fileName?: string;
  versionNo?: number;
  previewURL?: string;
  downloadURL?: string;
}
const files = ref<TechnicalFile[]>([]);
const loading = ref(false);
watch(() => [props.workOrderId, props.itemId] as const, async ([workOrderId, itemId], _old, onCleanup) => {
  let active = true;
  onCleanup(() => { active = false; });
  files.value = [];
  if (!workOrderId && !itemId) {
    loading.value = false;
    return;
  }
  loading.value = true;
  try {
    const data: TechnicalFile[] = workOrderId
      ? await requestClient.get('/mes/pro/work-order/technical-files', { params: { id: workOrderId } })
      : await requestClient.get('/mdm/item/technical-file/list', { params: { itemId } });
    if (active) files.value = data || [];
  } finally {
    if (active) loading.value = false;
  }
}, { immediate: true });
</script>
<template>
  <p class="mb-3 text-xs text-gray-500">{{ workOrderId ? '显示生产订单创建时留存的技术文件。' : '显示物料当前技术文件,下发时保存到生产订单。' }}预览后可使用浏览器打印,其他格式可下载后打印。</p>
  <Table v-if="files.length || loading" :data-source="files" :loading="loading" :pagination="false" row-key="id" size="small">
    <Table.Column title="文件名称" data-index="fileName" />
    <Table.Column title="文件版本" data-index="versionNo" />
    <Table.Column title="操作" key="actions">
      <template #default="{ record }">
        <Space>
          <Button v-if="record.previewURL" type="link" :href="record.previewURL" target="_blank" rel="noopener noreferrer">预览 / æ‰“印</Button>
          <Button v-if="record.downloadURL" type="link" :href="record.downloadURL" target="_blank" rel="noopener noreferrer">下载</Button>
        </Space>
      </template>
    </Table.Column>
  </Table>
  <Empty v-else :description="workOrderId ? '该工单没有留存技术文件' : '当前物料没有技术文件'" />
</template>
src/views/mes/qc/components/qc-attachments.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { useAccess } from '@vben/access';
import { Button, Empty, message, Popconfirm, Space, Table, Upload } from 'ant-design-vue';
import { requestClient } from '#/api/request';
import { uploadFile, type SystemStorageApi } from '#/api/system/storage';
const props = defineProps<{ qcType: 'iqc' | 'ipqc' | 'oqc' | 'rqc'; qcId: number; readonly?: boolean }>();
const { hasAccessByCodes, hasAccessByRoles } = useAccess();
const canEdit = computed(() => !props.readonly && (hasAccessByRoles(['super_admin']) || hasAccessByCodes([`mes:qc-${props.qcType}:update`])));
const files = ref<SystemStorageApi.StorageBlob[]>([]);
const loading = ref(false);
async function refresh() {
  files.value = await requestClient.get('/mes/qc/attachment/list', { params: { type: props.qcType, id: props.qcId } });
}
onMounted(refresh);
async function beforeUpload(file: File) {
  if (loading.value) return Upload.LIST_IGNORE;
  loading.value = true;
  try {
    const uploaded = await uploadFile([file]);
    const blobId = uploaded?.[0]?.id;
    if (!blobId) throw new Error('上传结果缺少文件编号');
    await requestClient.post('/mes/qc/attachment/append', {
      type: props.qcType, id: props.qcId, blobId,
    });
    await refresh();
    message.success('附件已上传');
  } finally {
    loading.value = false;
  }
  return Upload.LIST_IGNORE;
}
async function removeFile(blobId: number) {
  loading.value = true;
  try {
    await requestClient.delete('/mes/qc/attachment/remove', { params: { type: props.qcType, id: props.qcId, blobId } });
    await refresh();
    message.success('附件已移除');
  } finally {
    loading.value = false;
  }
}
</script>
<template>
  <Space direction="vertical" class="w-full">
    <Upload v-if="canEdit" :show-upload-list="false" :before-upload="beforeUpload" :disabled="loading">
      <Button :loading="loading">上传检验附件</Button>
    </Upload>
    <Table v-if="files.length" :data-source="files" :pagination="false" row-key="id" size="small">
      <Table.Column title="文件名称" data-index="name" />
      <Table.Column title="操作" key="actions">
        <template #default="{ record }">
          <a v-if="record.previewURL || record.downloadURL" :href="record.previewURL || record.downloadURL" target="_blank" rel="noopener noreferrer">查看 / ä¸‹è½½</a>
          <Popconfirm v-if="canEdit" title="确认从质检单移除此附件?" ok-text="移除" cancel-text="取消" @confirm="removeFile(record.id)">
            <a class="ml-3 text-red-500">移除</a>
          </Popconfirm>
        </template>
      </Table.Column>
    </Table>
    <Empty v-else description="暂无检验附件" />
  </Space>
</template>
src/views/mes/qc/components/qc-source-link.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,116 @@
<script lang="ts" setup>
import type { MesQcSourceLinkApi } from '#/api/mes/qc/source-link';
import { computed, ref } from 'vue';
import { useAccess } from '@vben/access';
import { Button, message, Modal, Table } from 'ant-design-vue';
import {
  getSourceLinkCandidates,
  linkSource,
} from '#/api/mes/qc/source-link';
const props = defineProps<{
  type: MesQcSourceLinkApi.LinkType;
  qcId: number;
  reelNo?: string;
}>();
const emit = defineEmits<{ linked: [] }>();
const { hasAccessByCodes, hasAccessByRoles } = useAccess();
const allowed = computed(
  () =>
    hasAccessByRoles(['super_admin']) ||
    hasAccessByCodes([
      `mes:qc-${props.type === 'feedback' ? 'ipqc' : 'oqc'}:update`,
    ]),
);
const title = computed(() =>
  props.type === 'feedback' ? '关联生产报工' : '关联销售出库',
);
const targetName = computed(() =>
  props.type === 'feedback' ? '待检报工产出行' : '待检出库明细',
);
const open = ref(false);
const loading = ref(false);
const linkingLineId = ref<number>();
const rows = ref<MesQcSourceLinkApi.Candidate[]>([]);
const columns = [
  { title: '来源单据编号', dataIndex: 'documentCode', key: 'documentCode' },
  { title: '盘号', dataIndex: 'reelNo', key: 'reelNo' },
  { title: '待检数量', dataIndex: 'quantity', key: 'quantity' },
  { title: '操作', key: 'action', width: 100 },
];
async function show() {
  if (!props.reelNo?.trim()) {
    message.warning('该质检单缺少盘号,请先补充盘号后再关联');
    return;
  }
  open.value = true;
  loading.value = true;
  try {
    rows.value = await getSourceLinkCandidates(props.type, props.qcId);
  } finally {
    loading.value = false;
  }
}
async function link(row: MesQcSourceLinkApi.Candidate) {
  linkingLineId.value = row.lineId;
  try {
    await linkSource(props.type, props.qcId, row.lineId);
    message.success('关联成功,质检数量已回写来源单据');
    open.value = false;
    emit('linked');
  } catch {
    // åŽç«¯é”™è¯¯æç¤ºå·²ç”±è¯·æ±‚拦截器展示,保留弹窗便于重新选择候选
  } finally {
    linkingLineId.value = undefined;
  }
}
</script>
<template>
  <Button v-if="allowed" @click="show">{{ title }}</Button>
  <Modal v-model:open="open" :title="title" :footer="null" :width="760">
    <p class="mb-3">
      å½“前质检单盘号:{{ reelNo || '未填写' }}。仅显示物料、盘号和数量均一致且仍处于待检状态的{{
        targetName
      }}。
    </p>
    <Table
      :columns="columns"
      :data-source="rows"
      :loading="loading"
      :pagination="{ pageSize: 10 }"
      row-key="lineId"
      size="small"
    >
      <template #emptyText>
        <div class="py-4 text-gray-500">
          æš‚无可关联的{{ targetName }},请核对盘号、检测数量及来源单据状态后重试。
        </div>
      </template>
      <template #bodyCell="{ column, record }">
        <template v-if="column.key === 'action'">
          <Button
            type="link"
            :disabled="!!linkingLineId"
            :loading="linkingLineId === record.lineId"
            @click="link(record as MesQcSourceLinkApi.Candidate)"
          >
            å…³è”
          </Button>
        </template>
      </template>
    </Table>
  </Modal>
</template>
src/views/mes/qc/ipqc/data.ts
@@ -14,7 +14,6 @@
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getTemplateItemOptionList } from '#/api/mes/qc/template/item';
import { getSimpleUserList } from '#/api/system/user';
import { getDateRangePickerDefaultProps } from '#/utils';
import { MdItemSelect } from '#/views/mes/md/item/components';
@@ -202,39 +201,6 @@
      },
    },
    {
      fieldName: 'templateId',
      label: '质检方案',
      component: 'ApiSelect',
      componentProps: {
        api: getTemplateItemOptionList,
        autoSelect: 'first',
        labelField: 'templateName',
        valueField: 'templateId',
        placeholder: '请选择质检方案',
      },
      rules: 'selectRequired',
      dependencies: {
        triggerFields: ['workOrderId', 'taskId'],
        componentProps: (values) => ({
          api: getTemplateItemOptionList,
          autoSelect: 'first',
          labelField: 'templateName',
          valueField: 'templateId',
          disabled: formType !== 'create' || !values.workOrderId,
          params: values.workOrderId
            ? {
                qcType: 2,
                workOrderId: values.workOrderId,
                taskId: values.taskId,
              }
            : {},
          placeholder: values.workOrderId
            ? '请选择质检方案'
            : '请先选择生产订单',
        }),
      },
    },
    {
      fieldName: 'checkQuantity',
      label: '检测数量',
      component: 'InputNumber',
@@ -361,16 +327,6 @@
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
    {
      fieldName: 'checkResult',
      label: '检测结果',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_QC_CHECK_RESULT, 'number'),
        placeholder: '请选择检测结果',
      },
    },
    {
      fieldName: 'remark',
src/views/mes/qc/ipqc/modules/form.vue
@@ -6,24 +6,22 @@
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { MesQcCheckResultEnum, MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { MesQcStatusEnum } from '@vben/constants';
import { Button, Descriptions, message, Tabs } from 'ant-design-vue';
import { Button, message, Tabs } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createIpqc, finishIpqc, getIpqc, updateIpqc } from '#/api/mes/qc/ipqc';
import { $t } from '#/locales';
import { QcIndicatorResultList } from '../../indicatorresult/components';
import MesQcAiSuggest from '../../components/ai-suggest.vue';
import MesQcAiAssistant from '../../components/ai-assistant.vue';
import QcAttachments from '../../components/qc-attachments.vue';
import QcSourceLink from '../../components/qc-source-link.vue';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesQcIpqcApi.Ipqc>();
const subTabsName = ref('line');
const subTabsName = ref('attachments');
const originalSnapshot = ref(''); // è¡¨å•原始数据快照,用于 finish æ—¶è·³è¿‡æœªå˜æ›´çš„保存请求
const isDetail = computed(() => formType.value === 'detail');
const canFinish = computed(
@@ -54,14 +52,6 @@
  showDefaultActions: false,
});
/** å­è¡¨å˜æ›´åŽåˆ·æ–°ä¸»è¡¨å¤´æ•°æ®ï¼ˆç¼ºé™·ç»Ÿè®¡ç­‰ï¼‰ */
async function handleRefresh() {
  if (!formData.value?.id) {
    return;
  }
  formData.value = await getIpqc(formData.value.id);
}
/** æäº¤è¡¨å• */
async function handleSubmit(): Promise<boolean> {
  const { valid } = await formApi.validate();
@@ -69,13 +59,6 @@
    return false;
  }
  const data = (await formApi.getValues()) as MesQcIpqcApi.Ipqc;
  if (
    (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
    data.checkResult === MesQcCheckResultEnum.FAIL
  ) {
    message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
    return false;
  }
  if (formData.value?.id) {
    await updateIpqc({ ...data, id: formData.value.id });
    formData.value = { ...formData.value, ...data };
@@ -95,6 +78,18 @@
  return true;
}
/** å…³è”来源单据后刷新详情、表单显示与快照 */
async function handleLinked() {
  const id = formData.value?.id;
  if (!id) {
    return;
  }
  formData.value = await getIpqc(id);
  await formApi.setValues(formData.value);
  originalSnapshot.value = JSON.stringify(await formApi.getValues());
  emit('success');
}
/** å®Œæˆæ£€éªŒå•:表单有修改时先保存,再调用完成接口 */
async function handleFinish() {
  const id = formData.value?.id;
@@ -106,7 +101,7 @@
    return;
  }
  try {
    await confirm('是否完成过程检验单编制?【完成后将不能更改】');
    await confirm('是否完成过程检验单编制?【完成后仅可修改检验附件】');
  } catch {
    return;
  }
@@ -115,13 +110,6 @@
    const current = JSON.stringify(await formApi.getValues());
    if (current !== originalSnapshot.value) {
      const data = (await formApi.getValues()) as MesQcIpqcApi.Ipqc;
      if (
        (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
        data.checkResult === MesQcCheckResultEnum.FAIL
      ) {
        message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
        return;
      }
      await updateIpqc({ ...data, id });
      formData.value = { ...formData.value, ...data };
      originalSnapshot.value = current;
@@ -162,7 +150,7 @@
      originalSnapshot.value = '';
      return;
    }
    subTabsName.value = 'line';
    subTabsName.value = 'attachments';
    // åŠ è½½æ•°æ®
    const data = modalApi.getData<{
      formType: FormType;
@@ -196,60 +184,31 @@
<template>
  <Modal :title="getTitle" class="w-4/5">
    <Form class="mx-4" />
    <!-- ç¼ºé™·ç»Ÿè®¡ï¼ˆåªè¯»ï¼‰ -->
    <div v-if="formData?.id" class="mx-4 mt-4">
      <Descriptions title="缺陷情况" :column="3" bordered size="small">
        <Descriptions.Item label="致命缺陷数">
          {{ formData.criticalQuantity ?? 0 }}
        </Descriptions.Item>
        <Descriptions.Item label="严重缺陷数">
          {{ formData.majorQuantity ?? 0 }}
        </Descriptions.Item>
        <Descriptions.Item label="轻微缺陷数">
          {{ formData.minorQuantity ?? 0 }}
        </Descriptions.Item>
        <Descriptions.Item label="致命缺陷率">
          {{ formData.criticalRate ?? 0 }}%
        </Descriptions.Item>
        <Descriptions.Item label="严重缺陷率">
          {{ formData.majorRate ?? 0 }}%
        </Descriptions.Item>
        <Descriptions.Item label="轻微缺陷率">
          {{ formData.minorRate ?? 0 }}%
        </Descriptions.Item>
      </Descriptions>
    </div>
    <Tabs
      v-if="formData?.id"
      v-model:active-key="subTabsName"
      class="mx-4 mt-4"
    >
      <Tabs.TabPane key="line" tab="检验项">
        <LineList
          :form-type="formType"
          :ipqc-id="formData.id"
          @refresh="handleRefresh"
        />
      </Tabs.TabPane>
      <Tabs.TabPane key="result" tab="检测结果">
        <QcIndicatorResultList
          :qc-id="formData.id"
          :qc-type="MesQcTypeEnum.IPQC"
          :readonly="isDetail"
        />
      <Tabs.TabPane key="attachments" tab="检验附件">
        <QcAttachments qc-type="ipqc" :qc-id="formData!.id!" />
      </Tabs.TabPane>
    </Tabs>
    <template #prepend-footer>
      <div class="flex flex-auto items-center gap-2">
        <QcSourceLink
          v-if="
            formData?.id &&
            formData.status === MesQcStatusEnum.FINISHED &&
            !formData.sourceDocId
          "
          :qc-id="formData.id"
          :reel-no="formData.reelNo"
          type="feedback"
          @linked="handleLinked"
        />
        <Button v-if="canFinish" type="primary" @click="handleFinish">
          å®Œæˆ
        </Button>
        <MesQcAiSuggest
          v-if="canFinish && formData?.id"
          :qc-id="formData.id"
          :qc-type="MesQcTypeEnum.IPQC"
        />
        <MesQcAiAssistant />
      </div>
    </template>
  </Modal>
src/views/mes/qc/iqc/data.ts
@@ -11,7 +11,6 @@
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getTemplateItemOptionList } from '#/api/mes/qc/template/item';
import { getSimpleUserList } from '#/api/system/user';
import { getDateRangePickerDefaultProps } from '#/utils';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
@@ -135,33 +134,6 @@
      },
    },
    {
      fieldName: 'templateId',
      label: '质检方案',
      component: 'ApiSelect',
      componentProps: {
        api: getTemplateItemOptionList,
        autoSelect: 'first',
        labelField: 'templateName',
        valueField: 'templateId',
        placeholder: '请选择质检方案',
      },
      rules: 'selectRequired',
      dependencies: {
        triggerFields: ['itemId'],
        componentProps: (values) => ({
          api: getTemplateItemOptionList,
          autoSelect: 'first',
          labelField: 'templateName',
          valueField: 'templateId',
          disabled: formType !== 'create' || !values.itemId,
          params: values.itemId
            ? { itemId: values.itemId, qcType: 1 }
            : {},
          placeholder: values.itemId ? '请选择质检方案' : '请先选择产品物料',
        }),
      },
    },
    {
      fieldName: 'vendorName',
      label: '供应商',
      component: 'Input',
@@ -273,16 +245,6 @@
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
    {
      fieldName: 'checkResult',
      label: '检测结果',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_QC_CHECK_RESULT, 'number'),
        placeholder: '请选择检测结果',
      },
    },
    {
      fieldName: 'remark',
src/views/mes/qc/iqc/modules/form.vue
@@ -6,24 +6,21 @@
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { MesQcCheckResultEnum, MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { MesQcStatusEnum } from '@vben/constants';
import { Button, Descriptions, message, Tabs } from 'ant-design-vue';
import { Button, message, Tabs } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createIqc, finishIqc, getIqc, updateIqc } from '#/api/mes/qc/iqc';
import { $t } from '#/locales';
import { QcIndicatorResultList } from '../../indicatorresult/components';
import MesQcAiSuggest from '../../components/ai-suggest.vue';
import MesQcAiAssistant from '../../components/ai-assistant.vue';
import QcAttachments from '../../components/qc-attachments.vue';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesQcIqcApi.Iqc>();
const subTabsName = ref('line');
const subTabsName = ref('attachments');
const originalSnapshot = ref(''); // è¡¨å•原始数据快照,用于 finish æ—¶è·³è¿‡æœªå˜æ›´çš„保存请求
const isDetail = computed(() => formType.value === 'detail');
const canFinish = computed(
@@ -54,14 +51,6 @@
  showDefaultActions: false,
});
/** å­è¡¨å˜æ›´åŽåˆ·æ–°ä¸»è¡¨å¤´æ•°æ®ï¼ˆç¼ºé™·ç»Ÿè®¡ç­‰ï¼‰ */
async function handleRefresh() {
  if (!formData.value?.id) {
    return;
  }
  formData.value = await getIqc(formData.value.id);
}
/** æäº¤è¡¨å• */
async function handleSubmit(): Promise<boolean> {
  const { valid } = await formApi.validate();
@@ -69,13 +58,6 @@
    return false;
  }
  const data = (await formApi.getValues()) as MesQcIqcApi.Iqc;
  if (
    (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
    data.checkResult === MesQcCheckResultEnum.FAIL
  ) {
    message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
    return false;
  }
  if (formData.value?.id) {
    await updateIqc({ ...data, id: formData.value.id });
    formData.value = { ...formData.value, ...data };
@@ -106,7 +88,7 @@
    return;
  }
  try {
    await confirm('是否完成来料检验单编制?【完成后将不能更改】');
    await confirm('是否完成来料检验单编制?【完成后仅可修改检验附件】');
  } catch {
    return;
  }
@@ -115,13 +97,6 @@
    const current = JSON.stringify(await formApi.getValues());
    if (current !== originalSnapshot.value) {
      const data = (await formApi.getValues()) as MesQcIqcApi.Iqc;
      if (
        (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
        data.checkResult === MesQcCheckResultEnum.FAIL
      ) {
        message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
        return;
      }
      await updateIqc({ ...data, id });
      formData.value = { ...formData.value, ...data };
      originalSnapshot.value = current;
@@ -162,7 +137,7 @@
      originalSnapshot.value = '';
      return;
    }
    subTabsName.value = 'line';
    subTabsName.value = 'attachments';
    // åŠ è½½æ•°æ®
    const data = modalApi.getData<{
      formType: FormType;
@@ -197,47 +172,13 @@
<template>
  <Modal :title="getTitle" class="w-4/5">
    <Form class="mx-4" />
    <!-- ç¼ºé™·ç»Ÿè®¡ï¼ˆåªè¯»ï¼‰ -->
    <div v-if="formData?.id" class="mx-4 mt-4">
      <Descriptions title="缺陷情况" :column="3" bordered size="small">
        <Descriptions.Item label="致命缺陷数">
          {{ formData.criticalQuantity ?? 0 }}
        </Descriptions.Item>
        <Descriptions.Item label="严重缺陷数">
          {{ formData.majorQuantity ?? 0 }}
        </Descriptions.Item>
        <Descriptions.Item label="轻微缺陷数">
          {{ formData.minorQuantity ?? 0 }}
        </Descriptions.Item>
        <Descriptions.Item label="致命缺陷率">
          {{ formData.criticalRate ?? 0 }}%
        </Descriptions.Item>
        <Descriptions.Item label="严重缺陷率">
          {{ formData.majorRate ?? 0 }}%
        </Descriptions.Item>
        <Descriptions.Item label="轻微缺陷率">
          {{ formData.minorRate ?? 0 }}%
        </Descriptions.Item>
      </Descriptions>
    </div>
    <Tabs
      v-if="formData?.id"
      v-model:active-key="subTabsName"
      class="mx-4 mt-4"
    >
      <Tabs.TabPane key="line" tab="检验项">
        <LineList
          :form-type="formType"
          :iqc-id="formData.id"
          @refresh="handleRefresh"
        />
      </Tabs.TabPane>
      <Tabs.TabPane key="result" tab="检测结果">
        <QcIndicatorResultList
          :qc-id="formData.id"
          :qc-type="MesQcTypeEnum.IQC"
          :readonly="isDetail"
        />
      <Tabs.TabPane key="attachments" tab="检验附件">
        <QcAttachments qc-type="iqc" :qc-id="formData!.id!" />
      </Tabs.TabPane>
    </Tabs>
    <template #prepend-footer>
@@ -245,12 +186,6 @@
        <Button v-if="canFinish" type="primary" @click="handleFinish">
          å®Œæˆ
        </Button>
        <MesQcAiSuggest
          v-if="canFinish && formData?.id"
          :qc-id="formData.id"
          :qc-type="MesQcTypeEnum.IQC"
        />
        <MesQcAiAssistant />
      </div>
    </template>
  </Modal>
src/views/mes/qc/oqc/data.ts
@@ -11,7 +11,6 @@
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getTemplateItemOptionList } from '#/api/mes/qc/template/item';
import { getSimpleUserList } from '#/api/system/user';
import { getDateRangePickerDefaultProps } from '#/utils';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
@@ -132,33 +131,6 @@
          allCustomers: true,
          disabled: !!values.sourceDocId,
          placeholder: '请选择产品物料',
        }),
      },
    },
    {
      fieldName: 'templateId',
      label: '质检方案',
      component: 'ApiSelect',
      componentProps: {
        api: getTemplateItemOptionList,
        autoSelect: 'first',
        labelField: 'templateName',
        valueField: 'templateId',
        placeholder: '请选择质检方案',
      },
      rules: 'selectRequired',
      dependencies: {
        triggerFields: ['itemId'],
        componentProps: (values) => ({
          api: getTemplateItemOptionList,
          autoSelect: 'first',
          labelField: 'templateName',
          valueField: 'templateId',
          disabled: formType !== 'create' || !values.itemId,
          params: values.itemId
            ? { itemId: values.itemId, qcType: 3 }
            : {},
          placeholder: values.itemId ? '请选择质检方案' : '请先选择产品物料',
        }),
      },
    },
@@ -315,16 +287,6 @@
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
    {
      fieldName: 'checkResult',
      label: '检测结果',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_QC_CHECK_RESULT, 'number'),
        placeholder: '请选择检测结果',
      },
    },
    {
      fieldName: 'remark',
src/views/mes/qc/oqc/modules/form.vue
@@ -6,24 +6,22 @@
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { MesQcCheckResultEnum, MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { MesQcStatusEnum } from '@vben/constants';
import { Button, Descriptions, message, Tabs } from 'ant-design-vue';
import { Button, message, Tabs } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createOqc, finishOqc, getOqc, updateOqc } from '#/api/mes/qc/oqc';
import { $t } from '#/locales';
import { QcIndicatorResultList } from '../../indicatorresult/components';
import MesQcAiSuggest from '../../components/ai-suggest.vue';
import MesQcAiAssistant from '../../components/ai-assistant.vue';
import QcAttachments from '../../components/qc-attachments.vue';
import QcSourceLink from '../../components/qc-source-link.vue';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesQcOqcApi.Oqc>();
const subTabsName = ref('line');
const subTabsName = ref('attachments');
const originalSnapshot = ref(''); // è¡¨å•原始数据快照,用于 finish æ—¶è·³è¿‡æœªå˜æ›´çš„保存请求
const isDetail = computed(() => formType.value === 'detail');
const canFinish = computed(
@@ -54,14 +52,6 @@
  showDefaultActions: false,
});
/** å­è¡¨å˜æ›´åŽåˆ·æ–°ä¸»è¡¨å¤´æ•°æ®ï¼ˆç¼ºé™·ç»Ÿè®¡ç­‰ï¼‰ */
async function handleRefresh() {
  if (!formData.value?.id) {
    return;
  }
  formData.value = await getOqc(formData.value.id);
}
/** æäº¤è¡¨å• */
async function handleSubmit(): Promise<boolean> {
  const { valid } = await formApi.validate();
@@ -69,13 +59,6 @@
    return false;
  }
  const data = (await formApi.getValues()) as MesQcOqcApi.Oqc;
  if (
    (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
    data.checkResult === MesQcCheckResultEnum.FAIL
  ) {
    message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
    return false;
  }
  if (formData.value?.id) {
    await updateOqc({ ...data, id: formData.value.id });
    formData.value = { ...formData.value, ...data };
@@ -94,6 +77,18 @@
  return true;
}
/** å…³è”来源单据后刷新详情、表单显示与快照 */
async function handleLinked() {
  const id = formData.value?.id;
  if (!id) {
    return;
  }
  formData.value = await getOqc(id);
  await formApi.setValues(formData.value);
  originalSnapshot.value = JSON.stringify(await formApi.getValues());
  emit('success');
}
/** å®Œæˆæ£€éªŒå•:表单有修改时先保存,再调用完成接口 */
async function handleFinish() {
  const id = formData.value?.id;
@@ -105,7 +100,7 @@
    return;
  }
  try {
    await confirm('是否完成出货检验单编制?【完成后将不能更改】');
    await confirm('是否完成出货检验单编制?【完成后仅可修改检验附件】');
  } catch {
    return;
  }
@@ -114,13 +109,6 @@
    const current = JSON.stringify(await formApi.getValues());
    if (current !== originalSnapshot.value) {
      const data = (await formApi.getValues()) as MesQcOqcApi.Oqc;
      if (
        (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
        data.checkResult === MesQcCheckResultEnum.FAIL
      ) {
        message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
        return;
      }
      await updateOqc({ ...data, id });
      formData.value = { ...formData.value, ...data };
      originalSnapshot.value = current;
@@ -161,7 +149,7 @@
      originalSnapshot.value = '';
      return;
    }
    subTabsName.value = 'line';
    subTabsName.value = 'attachments';
    // åŠ è½½æ•°æ®
    const data = modalApi.getData<{
      formType: FormType;
@@ -200,60 +188,31 @@
<template>
  <Modal :title="getTitle" class="w-4/5">
    <Form class="mx-4" />
    <!-- ç¼ºé™·ç»Ÿè®¡ï¼ˆåªè¯»ï¼‰ -->
    <div v-if="formData?.id" class="mx-4 mt-4">
      <Descriptions title="缺陷情况" :column="3" bordered size="small">
        <Descriptions.Item label="致命缺陷数">
          {{ formData.criticalQuantity ?? 0 }}
        </Descriptions.Item>
        <Descriptions.Item label="严重缺陷数">
          {{ formData.majorQuantity ?? 0 }}
        </Descriptions.Item>
        <Descriptions.Item label="轻微缺陷数">
          {{ formData.minorQuantity ?? 0 }}
        </Descriptions.Item>
        <Descriptions.Item label="致命缺陷率">
          {{ formData.criticalRate ?? 0 }}%
        </Descriptions.Item>
        <Descriptions.Item label="严重缺陷率">
          {{ formData.majorRate ?? 0 }}%
        </Descriptions.Item>
        <Descriptions.Item label="轻微缺陷率">
          {{ formData.minorRate ?? 0 }}%
        </Descriptions.Item>
      </Descriptions>
    </div>
    <Tabs
      v-if="formData?.id"
      v-model:active-key="subTabsName"
      class="mx-4 mt-4"
    >
      <Tabs.TabPane key="line" tab="检验项">
        <LineList
          :form-type="formType"
          :oqc-id="formData.id"
          @refresh="handleRefresh"
        />
      </Tabs.TabPane>
      <Tabs.TabPane key="result" tab="检测结果">
        <QcIndicatorResultList
          :qc-id="formData.id"
          :qc-type="MesQcTypeEnum.OQC"
          :readonly="isDetail"
        />
      <Tabs.TabPane key="attachments" tab="检验附件">
        <QcAttachments qc-type="oqc" :qc-id="formData!.id!" />
      </Tabs.TabPane>
    </Tabs>
    <template #prepend-footer>
      <div class="flex flex-auto items-center gap-2">
        <QcSourceLink
          v-if="
            formData?.id &&
            formData.status === MesQcStatusEnum.FINISHED &&
            !formData.sourceDocId
          "
          :qc-id="formData.id"
          :reel-no="formData.reelNo"
          type="sales"
          @linked="handleLinked"
        />
        <Button v-if="canFinish" type="primary" @click="handleFinish">
          å®Œæˆ
        </Button>
        <MesQcAiSuggest
          v-if="canFinish && formData?.id"
          :qc-id="formData.id"
          :qc-type="MesQcTypeEnum.OQC"
        />
        <MesQcAiAssistant />
      </div>
    </template>
  </Modal>
src/views/mes/qc/rqc/data.ts
@@ -15,7 +15,6 @@
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getTemplateItemOptionList } from '#/api/mes/qc/template/item';
import { getSimpleUserList } from '#/api/system/user';
import { getDateRangePickerDefaultProps } from '#/utils';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
@@ -160,33 +159,6 @@
      },
    },
    {
      fieldName: 'templateId',
      label: '质检方案',
      component: 'ApiSelect',
      componentProps: {
        api: getTemplateItemOptionList,
        autoSelect: 'first',
        labelField: 'templateName',
        valueField: 'templateId',
        placeholder: '请选择质检方案',
      },
      rules: 'selectRequired',
      dependencies: {
        triggerFields: ['itemId'],
        componentProps: (values) => ({
          api: getTemplateItemOptionList,
          autoSelect: 'first',
          labelField: 'templateName',
          valueField: 'templateId',
          disabled: formType !== 'create' || !values.itemId,
          params: values.itemId
            ? { itemId: values.itemId, qcType: 4 }
            : {},
          placeholder: values.itemId ? '请选择质检方案' : '请先选择产品物料',
        }),
      },
    },
    {
      fieldName: 'batchCode',
      label: '批次号',
      component: 'Input',
@@ -265,16 +237,6 @@
        valueField: 'id',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'checkResult',
      label: '检测结果',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_QC_CHECK_RESULT, 'number'),
        placeholder: '请选择检测结果',
      },
    },
    {
      fieldName: 'remark',
src/views/mes/qc/rqc/modules/form.vue
@@ -6,24 +6,21 @@
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { MesQcCheckResultEnum, MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { MesQcStatusEnum } from '@vben/constants';
import { Button, Descriptions, message, Tabs } from 'ant-design-vue';
import { Button, message, Tabs } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createRqc, finishRqc, getRqc, updateRqc } from '#/api/mes/qc/rqc';
import { $t } from '#/locales';
import { QcIndicatorResultList } from '../../indicatorresult/components';
import MesQcAiSuggest from '../../components/ai-suggest.vue';
import MesQcAiAssistant from '../../components/ai-assistant.vue';
import QcAttachments from '../../components/qc-attachments.vue';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesQcRqcApi.Rqc>();
const subTabsName = ref('line');
const subTabsName = ref('attachments');
const originalSnapshot = ref(''); // è¡¨å•原始数据快照,用于 finish æ—¶è·³è¿‡æœªå˜æ›´çš„保存请求
const isDetail = computed(() => formType.value === 'detail');
const canFinish = computed(
@@ -54,14 +51,6 @@
  showDefaultActions: false,
});
/** å­è¡¨å˜æ›´åŽåˆ·æ–°ä¸»è¡¨å¤´æ•°æ®ï¼ˆç¼ºé™·ç»Ÿè®¡ç­‰ï¼‰ */
async function handleRefresh() {
  if (!formData.value?.id) {
    return;
  }
  formData.value = await getRqc(formData.value.id);
}
/** æäº¤è¡¨å• */
async function handleSubmit(): Promise<boolean> {
  const { valid } = await formApi.validate();
@@ -69,13 +58,6 @@
    return false;
  }
  const data = (await formApi.getValues()) as MesQcRqcApi.Rqc;
  if (
    (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
    data.checkResult === MesQcCheckResultEnum.FAIL
  ) {
    message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
    return false;
  }
  if (formData.value?.id) {
    await updateRqc({ ...data, id: formData.value.id });
    formData.value = { ...formData.value, ...data };
@@ -105,7 +87,7 @@
    return;
  }
  try {
    await confirm('是否完成退货检验单编制?【完成后将不能更改】');
    await confirm('是否完成退货检验单编制?【完成后仅可修改检验附件】');
  } catch {
    return;
  }
@@ -114,13 +96,6 @@
    const current = JSON.stringify(await formApi.getValues());
    if (current !== originalSnapshot.value) {
      const data = (await formApi.getValues()) as MesQcRqcApi.Rqc;
      if (
        (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
        data.checkResult === MesQcCheckResultEnum.FAIL
      ) {
        message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
        return;
      }
      await updateRqc({ ...data, id });
      formData.value = { ...formData.value, ...data };
      originalSnapshot.value = current;
@@ -159,7 +134,7 @@
      originalSnapshot.value = '';
      return;
    }
    subTabsName.value = 'line';
    subTabsName.value = 'attachments';
    // åŠ è½½æ•°æ®
    const data = modalApi.getData<{
      formType: FormType;
@@ -196,47 +171,13 @@
<template>
  <Modal :title="getTitle" class="w-4/5">
    <Form class="mx-4" />
    <!-- ç¼ºé™·ç»Ÿè®¡ï¼ˆåªè¯»ï¼‰ -->
    <div v-if="formData?.id" class="mx-4 mt-4">
      <Descriptions title="缺陷情况" :column="3" bordered size="small">
        <Descriptions.Item label="致命缺陷数">
          {{ formData.criticalQuantity ?? 0 }}
        </Descriptions.Item>
        <Descriptions.Item label="严重缺陷数">
          {{ formData.majorQuantity ?? 0 }}
        </Descriptions.Item>
        <Descriptions.Item label="轻微缺陷数">
          {{ formData.minorQuantity ?? 0 }}
        </Descriptions.Item>
        <Descriptions.Item label="致命缺陷率">
          {{ formData.criticalRate ?? 0 }}%
        </Descriptions.Item>
        <Descriptions.Item label="严重缺陷率">
          {{ formData.majorRate ?? 0 }}%
        </Descriptions.Item>
        <Descriptions.Item label="轻微缺陷率">
          {{ formData.minorRate ?? 0 }}%
        </Descriptions.Item>
      </Descriptions>
    </div>
    <Tabs
      v-if="formData?.id"
      v-model:active-key="subTabsName"
      class="mx-4 mt-4"
    >
      <Tabs.TabPane key="line" tab="检验项">
        <LineList
          :form-type="formType"
          :rqc-id="formData.id"
          @refresh="handleRefresh"
        />
      </Tabs.TabPane>
      <Tabs.TabPane key="result" tab="检测结果">
        <QcIndicatorResultList
          :qc-id="formData.id"
          :qc-type="MesQcTypeEnum.RQC"
          :readonly="isDetail"
        />
      <Tabs.TabPane key="attachments" tab="检验附件">
        <QcAttachments qc-type="rqc" :qc-id="formData!.id!" />
      </Tabs.TabPane>
    </Tabs>
    <template #prepend-footer>
@@ -244,12 +185,6 @@
        <Button v-if="canFinish" type="primary" @click="handleFinish">
          å®Œæˆ
        </Button>
        <MesQcAiSuggest
          v-if="canFinish && formData?.id"
          :qc-id="formData.id"
          :qc-type="MesQcTypeEnum.RQC"
        />
        <MesQcAiAssistant />
      </div>
    </template>
  </Modal>
src/views/mes/workbench/components/WorkstationSelectDialog.vue
@@ -5,9 +5,10 @@
import { IconifyIcon } from '@vben/icons';
import { Input, Modal, Spin } from 'ant-design-vue';
import { Input, message, Modal, Spin } from 'ant-design-vue';
import { getWorkstationPage } from '#/api/mes/md/workstation';
import { getUnfinishedWorkstationCounts } from '#/api/mes/pro/task';
defineOptions({ name: 'WorkbenchWorkstationSelectDialog' });
@@ -19,6 +20,8 @@
const loading = ref(false);
const workstations = ref<MesMdWorkstationApi.Workstation[]>([]);
const searchKeyword = ref('');
const pendingCounts = ref<Record<number, number>>({});
const countsAvailable = ref(false);
/** æ‰“开弹窗 */
async function open() {
@@ -31,8 +34,20 @@
async function loadWorkstations() {
  loading.value = true;
  try {
    const result = await getWorkstationPage({ pageNo: 1, pageSize: 200 });
    pendingCounts.value = {};
    countsAvailable.value = false;
    const [result, counts] = await Promise.all([
      getWorkstationPage({ pageNo: 1, pageSize: 200 }),
      getUnfinishedWorkstationCounts().catch(() => {
        message.warning('未能获取工作站任务数量');
        return null;
      }),
    ]);
    workstations.value = result.list ?? [];
    if (counts) {
      pendingCounts.value = Object.fromEntries(counts.map((item) => [item.workstationId, item.pendingCount]));
      countsAvailable.value = true;
    }
  } finally {
    loading.value = false;
  }
@@ -103,6 +118,7 @@
            </span>
          </div>
          <div class="ws-card__name">{{ ws.name }}</div>
          <div class="ws-card__info-row">未完成任务:{{ countsAvailable ? (pendingCounts[ws.id!] ?? 0) : '—' }} æ¡</div>
          <div class="ws-card__info">
            <div v-if="ws.workshopName" class="ws-card__info-row">
              <IconifyIcon icon="lucide:building-2" class="ws-card__info-icon" />
src/views/mes/workbench/components/tabs/ProductIssueTab.vue
@@ -22,10 +22,13 @@
  cancelProductIssue,
  createProductIssue,
  deleteProductIssue,
  getProductIssue,
  getProductIssuePage,
  submitProductIssue,
  updateProductIssue,
} from '#/api/mes/wm/productissue';
import { getProductIssueLinesByIssue } from '#/api/mes/wm/productissue/line';
import dayjs from 'dayjs';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
@@ -240,7 +243,7 @@
      props: { type: DICT_TYPE.MES_WM_PRODUCT_ISSUE_STATUS },
    },
  },
  { title: '操作', width: 260, fixed: 'right', slots: { default: 'actions' } },
  { title: '操作', width: 360, fixed: 'right', slots: { default: 'actions' } },
];
const gridHeight = ref(300);
@@ -304,6 +307,52 @@
/** æ˜¯å¦å·²é€‰ä¸­å·¥ä½œç«™å’Œä»»åŠ¡ï¼Œæœªé€‰ä¸­æ—¶ä¸å±•ç¤ºè¡¨æ ¼ */
const isReady = computed(() => !!props.taskId && !!props.workstationId);
/** å•据内容全部按文本转义,避免物料名称或备注影响打印页面结构。 */
function escapePrintText(value: unknown) {
  return String(value ?? '').replace(/[&<>"']/g, (char) => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
  })[char] || char);
}
async function handlePrintIssue(row: MesWmProductIssueApi.ProductIssue) {
  if (!row.id) return;
  // å…ˆæ‰“开窗口,避免异步请求结束后被浏览器作为弹窗拦截。
  const popup = window.open('', '_blank');
  if (!popup) { message.warning('浏览器拦截了打印窗口,请允许弹窗后重试'); return; }
  popup.document.title = '正在准备领料单';
  popup.document.body.textContent = '正在准备领料单…';
  try {
    const [issue, lines] = await Promise.all([getProductIssue(row.id), getProductIssueLinesByIssue(row.id)]);
    if (!issue) throw new Error('领料单不存在');
    const safe = escapePrintText;
    const date = (value: unknown) => value && dayjs(value as string | number | Date).isValid()
      ? dayjs(value as string | number | Date).format('YYYY-MM-DD HH:mm') : '';
    const statusNames: Record<number, string> = {
      [MesWmProductIssueStatusEnum.PREPARE]: '草稿',
      [MesWmProductIssueStatusEnum.APPROVING]: '待拣货',
      [MesWmProductIssueStatusEnum.APPROVED]: '待领出',
      [MesWmProductIssueStatusEnum.FINISHED]: '已完成',
      [MesWmProductIssueStatusEnum.CANCELED]: '已取消',
    };
    const rows = lines.map((item, index) => `<tr><td>${index + 1}</td><td>${safe(item.itemCode)}</td><td>${safe(item.itemName)}</td><td>${safe(item.specification)}</td><td>${safe(item.unitMeasureName)}</td><td>${safe(item.quantity)}</td><td>${safe(item.remark)}</td></tr>`).join('');
    popup.document.open();
    popup.document.write(`<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><title>领料单 ${safe(issue.code)}</title><style>
      @page { size: A4; margin: 15mm; } body { color: #111; font: 13px 'Microsoft YaHei', sans-serif; margin: 0 auto; max-width: 180mm; }
      h1 { text-align: center; font-size: 22px; margin: 12px 0 20px; } .meta { display: grid; grid-template-columns: 1fr 1fr; gap: 9px 16px; margin-bottom: 16px; overflow-wrap: anywhere; }
      table { border-collapse: collapse; width: 100%; table-layout: fixed; } th,td { border: 1px solid #555; padding: 7px 4px; word-break: break-word; text-align: left; } th:first-child,td:first-child { width: 32px; text-align: center; } th:nth-child(5),td:nth-child(5) { width: 42px; } th:nth-child(6),td:nth-child(6) { width: 56px; } thead { display: table-header-group; } tr { break-inside: avoid; }
      .sign { display: flex; justify-content: space-between; gap: 18px; margin-top: 32px; } .toolbar { text-align: right; padding: 12px 0; } button { padding: 7px 14px; cursor: pointer; } @media print { .toolbar { display: none; } }
    </style></head><body><div class="toolbar"><button onclick="window.print()">打印 / ä¿å­˜ä¸º PDF</button></div><h1>领料单</h1>
      <div class="meta"><div>领料单号:${safe(issue.code)}</div><div>领料单名称:${safe(issue.name)}</div><div>生产任务:${safe(issue.taskCode)}</div><div>生产订单:${safe(issue.workOrderCode)}</div><div>工作站:${safe(issue.workstationName || issue.workstationCode)}</div><div>需求时间:${safe(date(issue.requiredTime))}</div><div>状态:${safe(statusNames[issue.status ?? -1] ?? '未知')}</div><div>备注:${safe(issue.remark)}</div></div>
      <table><thead><tr><th>序号</th><th>物料编码</th><th>物料名称</th><th>规格型号</th><th>单位</th><th>申请数量</th><th>备注</th></tr></thead><tbody>${rows || '<tr><td colspan="7">暂无领料明细</td></tr>'}</tbody></table>
      <div class="sign"><span>申请人:____________</span><span>发料人:____________</span><span>领料人:____________</span><span>日期:____________</span></div></body></html>`);
    popup.document.close();
  } catch (error) {
    popup.close();
    message.error('无法生成领料单,请稍后重试');
    console.error(error);
  }
}
/** ç¼–辑弹窗 */
const [EditFormModal, editFormModalApi] = useVbenModal({
  connectedComponent: ProductIssueFormModal,
@@ -361,6 +410,7 @@
      <div ref="listRef" class="issue-tab__list">
        <Grid table-title="领料记录">
          <template #actions="{ row }">
            <Button type="link" size="small" @click="handlePrintIssue(row)">打印 / å¯¼å‡º PDF</Button>
            <Button
              v-if="row.status === MesWmProductIssueStatusEnum.PREPARE"
              type="link"
src/views/wls/productsales/data.ts
@@ -132,7 +132,7 @@
            contactAddress: notice?.recipientAddress,
            contactName: notice?.recipientName,
            contactTelephone: notice?.recipientTelephone,
            salesOrderCode: notice?.salesOrderCode,
            salesOrderCode: notice?.saleOrderCode,
          });
        },
        status: MesWmSalesNoticeStatusEnum.APPROVED,
@@ -444,11 +444,20 @@
    },
    {
      field: 'oqcCheckFlag',
      title: '是否校验',
      width: 100,
      title: '需要出货检验',
      width: 120,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
      },
    },
    {
      field: 'qualityStatus',
      title: '质量状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_WM_QUALITY_STATUS },
      },
    },
    {
@@ -563,7 +572,7 @@
    },
    {
      fieldName: 'oqcCheckFlag',
      label: '是否校验',
      label: '需要出货检验',
      component: 'RadioGroup',
      componentProps: {
        buttonStyle: 'solid',
src/views/wls/productsales/modules/form.vue
@@ -242,7 +242,7 @@
      <div class="flex flex-auto items-center gap-2">
        <Popconfirm
          v-if="canSubmit"
          title="确认提交该销售出库单?【提交后将不能修改】"
          title="确认提交该销售出库单?【提交后将不能修改;需要出货检验的明细将进入待检测,需质检员创建并关联出货检验单】"
          @confirm="handleSubmit"
        >
          <Button type="primary">提交</Button>