From 3471290659516cf21db3211a9053daff5f283e03 Mon Sep 17 00:00:00 2001
From: zhuguifei <312353457@qq.com>
Date: 星期五, 20 三月 2026 15:50:18 +0800
Subject: [PATCH] feat: 基础数据仪器管理、判定依据、判定依据明细

---
 ruoyi-plus-soybean/src/views/report/silk-storage-output/index.vue |  866 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 866 insertions(+), 0 deletions(-)

diff --git a/ruoyi-plus-soybean/src/views/report/silk-storage-output/index.vue b/ruoyi-plus-soybean/src/views/report/silk-storage-output/index.vue
new file mode 100755
index 0000000..a0585d7
--- /dev/null
+++ b/ruoyi-plus-soybean/src/views/report/silk-storage-output/index.vue
@@ -0,0 +1,866 @@
+<script setup lang="ts">
+import { computed, nextTick, onMounted, ref } from 'vue';
+import { jsonClone } from '@sa/utils';
+import dompdf from 'dompdf.js';
+import ExcelJS from 'exceljs';
+import '@/assets/fonts/SourceHanSansSC-Normal-Min-normal.js';
+import { fetchGetStoreSilkList } from '@/service/api/analy/store-silk';
+import { useNaiveForm } from '@/hooks/common/form';
+import { $t } from '@/locales';
+
+defineOptions({
+  name: 'BatchProductionReport'
+});
+
+const { formRef, validate, restoreValidation } = useNaiveForm();
+const zoom = ref(1);
+const exportLoading = ref(false);
+
+async function beginExportLoading() {
+  exportLoading.value = true;
+  await nextTick();
+  await new Promise<void>(resolve => {
+    requestAnimationFrame(() => {
+      requestAnimationFrame(() => resolve());
+    });
+  });
+}
+
+function formatNow() {
+  const now = new Date();
+  const y = now.getFullYear();
+  const m = String(now.getMonth() + 1).padStart(2, '0');
+  const d = String(now.getDate()).padStart(2, '0');
+  const hh = String(now.getHours()).padStart(2, '0');
+  const mm = String(now.getMinutes()).padStart(2, '0');
+  const ss = String(now.getSeconds()).padStart(2, '0');
+  return `${y}${m}${d}${hh}${mm}${ss}`;
+}
+
+// 鎼滅储鍙傛暟
+const searchParams = ref<Api.Analy.StoreSilkSearchParams>({
+  pageNum: 1,
+  pageSize: 1000, // 鎶ヨ〃閫氬父鏄剧ず杈冨鏁版嵁
+  materialname: null,
+  batchcode: null,
+  actualstarttime: null,
+  distimebegin: null,
+  distimeend: null,
+  siloid: null,
+  params: {
+    beginTime: undefined,
+    endTime: undefined
+  }
+});
+
+const defaultSearchParams = jsonClone(searchParams.value);
+
+// 鏃ユ湡鑼冨洿 (鐢ㄤ簬鎼滅储琛ㄥ崟)
+const searchDateRange = ref<[string, string] | null>(null);
+
+function onDateRangeUpdate(value: [string, string] | null) {
+  if (!searchParams.value.params) {
+    searchParams.value.params = {};
+  }
+  if (value?.[0] && value?.[1]) {
+    searchParams.value.params.beginTime = value[0];
+    searchParams.value.params.endTime = value[1];
+  } else {
+    searchParams.value.params.beginTime = undefined;
+    searchParams.value.params.endTime = undefined;
+  }
+}
+
+// 鏌滃瓙鍙烽�夐」
+const siloOptions = Array.from({ length: 12 }, (_, i) => ({
+  label: `${i + 1}鍙峰偍涓濇煖`,
+  value: String(i + 1)
+}));
+
+const selectedSilos = ref<string[]>([]);
+
+function handleSiloChange(val: string[]) {
+  selectedSilos.value = val;
+  searchParams.value.siloid = val.join(',');
+}
+
+// 琛ㄦ牸鏁版嵁
+const loading = ref(false);
+const tableRows = ref<any[]>([]);
+
+// 璁$畻鏄剧ず鏂囨湰鐢ㄧ殑鏃ユ湡
+const displayDateRange = ref<[string, string] | null>(null);
+
+// 鏃ユ湡鑼冨洿鏄剧ず鏂囨湰
+const dateRangeText = computed(() => {
+  if (displayDateRange.value && displayDateRange.value[0] && displayDateRange.value[1]) {
+    return `鏃ユ湡:${displayDateRange.value[0]}鈥�${displayDateRange.value[1]}`;
+  }
+  return '';
+});
+
+// 璁$畻浜ч噺杈呭姪鍑芥暟
+function calcRollerBox(val: unknown) {
+  if (val === null || val === undefined) return null;
+  const v = Number(val) / 50;
+  if (!Number.isFinite(v)) return null;
+  return v.toFixed(1);
+}
+
+function calcPackerBox(val: unknown) {
+  if (val === null || val === undefined) return null;
+  const v = Number(val) / 10 / 250;
+  if (!Number.isFinite(v)) return null;
+  return v.toFixed(1);
+}
+
+function setZoom(next: number) {
+  const v = Math.max(0.6, Math.min(1.2, Number(next)));
+  zoom.value = Number.isFinite(v) ? v : 1;
+}
+
+// 鏍煎紡鍖栨煖鍙� (鍙栨渶鍚庝竴涓嬩笅鍒掔嚎鍚庨潰鐨勬暟瀛�)
+function formatSiloId(val: string) {
+  if (!val) return '';
+  const parts = val.split('_');
+  return parts[parts.length - 1];
+}
+
+// 璁$畻鐑熶笣閲嶉噺 (jobinput - weight)
+function calculateSilkWeight(jobinput: any, weight: any) {
+  const res = Number(jobinput || 0) - Number(weight || 0);
+  return res.toFixed(2);
+}
+
+// 鏍煎紡鍖栫敓浜ф満缁� (鍙� rollerDetailList 閲岄潰鐨� equNo, 骞跺幓閲�)
+function formatProductionUnits(list: any[]) {
+  if (!Array.isArray(list)) return '';
+  const equNos = list.map(item => item.equNo).filter(Boolean);
+  const unique = Array.from(new Set(equNos));
+  unique.sort((a, b) => {
+    const aStr = String(a);
+    const bStr = String(b);
+    const aNum = Number(aStr);
+    const bNum = Number(bStr);
+    const aIsNum = Number.isFinite(aNum);
+    const bIsNum = Number.isFinite(bNum);
+    if (aIsNum && bIsNum) return aNum - bNum;
+    if (aIsNum) return -1;
+    if (bIsNum) return 1;
+    return aStr.localeCompare(bStr, 'zh-CN', { numeric: true });
+  });
+  return unique.join(',');
+}
+
+async function handleSearch() {
+  await validate();
+  loading.value = true;
+  try {
+    const { data, error } = await fetchGetStoreSilkList(searchParams.value);
+    if (!error && data) {
+      tableRows.value = data.rows || [];
+      // 鏇存柊鎶ヨ〃椤堕儴鐨勬樉绀烘棩鏈�
+      if (searchParams.value.params?.beginTime && searchParams.value.params?.endTime) {
+        displayDateRange.value = [searchParams.value.params.beginTime, searchParams.value.params.endTime];
+      } else {
+        displayDateRange.value = null;
+      }
+    }
+  } finally {
+    loading.value = false;
+  }
+}
+
+function setDefaultDateRange() {
+  const now = new Date();
+  const daysAgo = new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000);
+  const beginTime = `${daysAgo.getFullYear()}-${String(daysAgo.getMonth() + 1).padStart(2, '0')}-${String(daysAgo.getDate()).padStart(2, '0')} 00:00:00`;
+  const endTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} 23:59:59`;
+
+  searchDateRange.value = [beginTime, endTime];
+  onDateRangeUpdate([beginTime, endTime]);
+}
+
+function toNumberOrNull(val: unknown) {
+  const num = Number(val);
+  return Number.isFinite(num) ? num : null;
+}
+
+async function handleExport() {
+  await beginExportLoading();
+
+  const workbook = new ExcelJS.Workbook();
+  const worksheet = workbook.addWorksheet('鎵规浜ч噺缁熻鎶ヨ〃');
+
+  worksheet.columns = [
+    { key: 'batchcode', width: 16 },
+    { key: 'materialname', width: 22 },
+    { key: 'distimebegin', width: 20 },
+    { key: 'distimeend', width: 20 },
+    { key: 'siloid', width: 8 },
+    { key: 'silkWeight', width: 12 },
+    { key: 'units', width: 14 },
+    { key: 'roller', width: 12 },
+    { key: 'packer', width: 12 },
+    { key: 'elevator', width: 12 }
+  ];
+
+  const totalColumns = 10;
+  const titleRow = worksheet.addRow(['鎵规浜ч噺缁熻鎶ヨ〃']);
+  worksheet.mergeCells(1, 1, 1, totalColumns);
+  titleRow.height = 26;
+  titleRow.getCell(1).font = { size: 16, bold: true, color: { argb: 'FF111827' } };
+  titleRow.getCell(1).alignment = { horizontal: 'center', vertical: 'middle' };
+
+  const dateText = dateRangeText.value || '';
+  const dateRow = worksheet.addRow([dateText]);
+  worksheet.mergeCells(2, 1, 2, totalColumns);
+  dateRow.height = 20;
+  dateRow.getCell(1).font = { size: 12, bold: true, color: { argb: 'FF0066CC' } };
+  dateRow.getCell(1).alignment = { horizontal: 'right', vertical: 'middle' };
+
+  const headerRow1 = worksheet.addRow([
+    '鎵规鍙�',
+    '鐗屽彿',
+    '鎵规寮�濮嬫椂闂�',
+    '鎵规缁撴潫鏃堕棿',
+    '鏌滃彿',
+    '鐑熶笣閲嶉噺',
+    '鐢熶骇鏈虹粍',
+    '鍗风儫鏈烘�讳骇',
+    '鍖呰鏈烘�讳骇',
+    '鎻愬崌鏈烘�讳骇'
+  ]);
+  const headerRow2 = worksheet.addRow(['', '', '', '', '', '', '', '閲忥紙绠憋級', '閲忥紙绠憋級', '閲忥紙绠憋級']);
+
+  for (let i = 1; i <= 7; i += 1) {
+    worksheet.mergeCells(3, i, 4, i);
+  }
+
+  const headerFill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF337AB7' } } as const;
+  const headerFont = { bold: true, color: { argb: 'FFFFFFFF' }, size: 12 } as const;
+  const headerBorder = {
+    top: { style: 'thin', color: { argb: 'FF4A90D9' } },
+    left: { style: 'thin', color: { argb: 'FF4A90D9' } },
+    bottom: { style: 'thin', color: { argb: 'FF4A90D9' } },
+    right: { style: 'thin', color: { argb: 'FF4A90D9' } }
+  } as const;
+
+  [headerRow1, headerRow2].forEach(row => {
+    row.height = 22;
+    row.eachCell({ includeEmpty: true }, cell => {
+      cell.fill = headerFill;
+      cell.font = headerFont;
+      cell.border = headerBorder;
+      cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
+    });
+  });
+
+  const dataBorder = {
+    top: { style: 'thin', color: { argb: 'FFCCCCCC' } },
+    left: { style: 'thin', color: { argb: 'FFCCCCCC' } },
+    bottom: { style: 'thin', color: { argb: 'FFCCCCCC' } },
+    right: { style: 'thin', color: { argb: 'FFCCCCCC' } }
+  } as const;
+
+  tableRows.value.forEach((row, index) => {
+    const dataRow = worksheet.addRow([
+      row.batchcode ?? '',
+      row.materialname ?? '',
+      row.distimebegin ?? '',
+      row.distimeend ?? '',
+      formatSiloId(row.siloid),
+      toNumberOrNull(calculateSilkWeight(row.jobinput, row.weight)),
+      formatProductionUnits(row.rollerDetailList),
+      toNumberOrNull(calcRollerBox(row.rollerOutput)),
+      toNumberOrNull(calcPackerBox(row.packerOutput)),
+      toNumberOrNull(calcPackerBox(row.packerOutput))
+    ]);
+
+    dataRow.height = 20;
+
+    dataRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {
+      cell.border = dataBorder;
+      cell.alignment = { horizontal: 'center', vertical: 'middle' };
+
+      if (colNumber === 2) {
+        cell.alignment = { horizontal: 'left', vertical: 'middle' };
+      }
+
+      if (colNumber === 6) {
+        cell.numFmt = '0.00';
+      }
+
+      if (colNumber >= 8 && colNumber <= 10) {
+        cell.numFmt = '0.0';
+      }
+
+      if (index % 2 === 1) {
+        cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF9F9F9' } };
+      }
+    });
+  });
+
+  worksheet.views = [{ state: 'frozen', ySplit: 4 }];
+
+  try {
+    const buffer = await workbook.xlsx.writeBuffer();
+    const blob = new Blob([buffer], {
+      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+    });
+    const url = URL.createObjectURL(blob);
+    const link = document.createElement('a');
+    link.href = url;
+    link.download = `鍌ㄤ笣鏌滃嚭鏂欎骇閲忔姤琛╛${formatNow()}.xlsx`;
+    link.click();
+    URL.revokeObjectURL(url);
+  } finally {
+    exportLoading.value = false;
+  }
+}
+
+async function handleExportCsv() {
+  await beginExportLoading();
+
+  const headers = [
+    '鎵规鍙�',
+    '鐗屽彿',
+    '鎵规寮�濮嬫椂闂�',
+    '鎵规缁撴潫鏃堕棿',
+    '鏌滃彿',
+    '鐑熶笣閲嶉噺',
+    '鐢熶骇鏈虹粍',
+    '鍗风儫鏈烘�讳骇閲忥紙绠憋級',
+    '鍖呰鏈烘�讳骇閲忥紙绠憋級',
+    '鎻愬崌鏈烘�讳骇閲忥紙绠憋級'
+  ];
+
+  const rows = tableRows.value.map(row => [
+    row.batchcode ?? '',
+    row.materialname ?? '',
+    row.distimebegin ?? '',
+    row.distimeend ?? '',
+    formatSiloId(row.siloid),
+    calculateSilkWeight(row.jobinput, row.weight),
+    formatProductionUnits(row.rollerDetailList),
+    calcRollerBox(row.rollerOutput),
+    calcPackerBox(row.packerOutput),
+    calcPackerBox(row.packerOutput)
+  ]);
+
+  const escapeCsv = (value: unknown) => {
+    const str = value === null || value === undefined ? '' : String(value);
+    if (/["\n,]/.test(str)) {
+      return `"${str.replace(/"/g, '""')}"`;
+    }
+    return str;
+  };
+
+  const csvContent = [headers, ...rows].map(row => row.map(escapeCsv).join(',')).join('\n');
+  const blob = new Blob([`\uFEFF${csvContent}`], { type: 'text/csv;charset=utf-8;' });
+  try {
+    const url = URL.createObjectURL(blob);
+    const link = document.createElement('a');
+    link.href = url;
+      link.download = `鍌ㄤ笣鏌滃嚭鏂欎骇閲忔姤琛╛${formatNow()}.csv`;
+    link.click();
+    URL.revokeObjectURL(url);
+  } finally {
+    exportLoading.value = false;
+  }
+}
+
+function printReportSheet() {
+  const sheet = document.querySelector('.report-sheet') as HTMLElement | null;
+  if (!sheet) return;
+
+  const styles = Array.from(document.querySelectorAll('style, link[rel="stylesheet"]'))
+    .map(node => node.outerHTML)
+    .join('\n');
+
+  const printWindow = window.open('', '_blank');
+  if (!printWindow) return;
+
+  printWindow.document.open();
+  printWindow.document.write(`
+    <!DOCTYPE html>
+    <html>
+      <head>
+        <meta charset="utf-8" />
+        <title>鎵规浜ч噺缁熻鎶ヨ〃</title>
+        ${styles}
+        <style>
+          @page {
+            size: A4 landscape;
+            margin: 10mm;
+          }
+          body {
+            margin: 0;
+            padding: 16px;
+            background: #fff;
+          }
+          .print-container {
+            display: flex;
+            justify-content: center;
+            width: 100%;
+          }
+          .report-sheet {
+            transform: none !important;
+            box-shadow: none !important;
+            width: 100% !important;
+            margin: 0 !important;
+            padding: 0 !important;
+          }
+          .report-table-scroll {
+            overflow: visible !important;
+          }
+          .report-table {
+            width: 100% !important;
+            min-width: 0 !important;
+            table-layout: fixed;
+          }
+        </style>
+      </head>
+      <body>
+        <div class="print-container">${sheet.outerHTML}</div>
+      </body>
+    </html>
+  `);
+  printWindow.document.close();
+  printWindow.focus();
+  printWindow.addEventListener('load', () => {
+    setTimeout(() => {
+      printWindow.print();
+    }, 100);
+  });
+  printWindow.onafterprint = () => {
+    printWindow.close();
+  };
+}
+
+function handlePrint() {
+  printReportSheet();
+}
+
+async function handleExportPdf() {
+  await beginExportLoading();
+
+  setTimeout(async () => {
+    const sheet = document.querySelector('#report-capture') as HTMLElement | null;
+    if (!sheet) {
+      exportLoading.value = false;
+      return;
+    }
+
+    const sheetWidth = sheet.scrollWidth || sheet.getBoundingClientRect().width || 1200;
+    const scale = Math.min(1, 794 / sheetWidth);
+
+    const clone = sheet.cloneNode(true) as HTMLElement;
+    clone.style.transform = `scale(${scale})`;
+    clone.style.transformOrigin = 'top left';
+    clone.style.width = `${sheetWidth}px`;
+    clone.style.minWidth = '0';
+    clone.style.margin = '0';
+    clone.style.boxShadow = 'none';
+    clone.style.position = 'fixed';
+    clone.style.left = '-10000px';
+    clone.style.top = '0';
+    clone.style.background = '#ffffff';
+    clone.style.fontFamily = 'SourceHanSansSC-Normal-Min, sans-serif';
+
+    document.body.appendChild(clone);
+
+    try {
+      const blob = (await dompdf(clone, {
+        pagination: true,
+        format: 'a4',
+        useCORS: true,
+        backgroundColor: '#ffffff',
+        fontConfig: {
+          fontFamily: 'SourceHanSansSC-Normal-Min',
+          fontBase64: (window as any).fontBase64,
+          fontStyle: 'normal',
+          fontWeight: 400
+        }
+      })) as Blob;
+
+      const url = URL.createObjectURL(blob);
+      const link = document.createElement('a');
+      link.href = url;
+      link.download = `鍌ㄤ笣鏌滃嚭鏂欎骇閲忔姤琛╛${formatNow()}.pdf`;
+      document.body.appendChild(link);
+      link.click();
+      document.body.removeChild(link);
+      URL.revokeObjectURL(url);
+    } catch (err) {
+      console.error(err);
+    } finally {
+      document.body.removeChild(clone);
+      exportLoading.value = false;
+    }
+  }, 0);
+}
+
+async function handleReset() {
+  await restoreValidation();
+  searchParams.value = jsonClone(defaultSearchParams);
+  selectedSilos.value = [];
+  setDefaultDateRange();
+  handleSearch();
+}
+
+onMounted(() => {
+  setDefaultDateRange();
+  handleSearch();
+});
+</script>
+
+<template>
+  <div class="h-full min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
+    <!-- 鎼滅储鍖哄煙 -->
+    <NCard :bordered="false" size="small" class="card-wrapper">
+      <NCollapse :default-expanded-names="['report-search']">
+        <NCollapseItem :title="$t('common.search')" name="report-search">
+          <NForm ref="formRef" :model="searchParams" label-placement="left" :label-width="80">
+            <NGrid responsive="screen" item-responsive>
+              <NFormItemGi span="24 s:12 m:8" label="鐗屽彿" label-width="auto" path="materialname" class="pr-24px">
+                <NInput v-model:value="searchParams.materialname" clearable placeholder="璇疯緭鍏ョ墝鍙�" />
+              </NFormItemGi>
+              <NFormItemGi span="24 s:12 m:8" label="鎵规鍙�" label-width="auto" path="batchcode" class="pr-24px">
+                <NInput v-model:value="searchParams.batchcode" clearable placeholder="璇疯緭鍏ユ壒娆″彿" />
+              </NFormItemGi>
+              <NFormItemGi span="24 s:12 m:8" label="鎶曟枡鏃ユ湡" label-width="auto" path="actualstarttime" class="pr-24px">
+                <NDatePicker
+                  v-model:formatted-value="searchParams.actualstarttime"
+                  type="date"
+                  value-format="yyyy-MM-dd"
+                  clearable
+                  class="w-full"
+                />
+              </NFormItemGi>
+              <NFormItemGi span="24 s:12 m:8" label="鍑烘枡鏃堕棿" label-width="auto" path="params.beginTime" class="pr-24px">
+                <NDatePicker
+                  v-model:formatted-value="searchDateRange"
+                  type="datetimerange"
+                  value-format="yyyy-MM-dd HH:mm:ss"
+                  clearable
+                  @update:formatted-value="onDateRangeUpdate"
+                />
+              </NFormItemGi>
+              <NFormItemGi span="24 s:12 m:8" label="鏌滃瓙鍙�" label-width="auto" path="siloid" class="pr-24px">
+                <NSelect
+                  v-model:value="selectedSilos"
+                  multiple
+                  clearable
+                  :options="siloOptions"
+                  placeholder="璇烽�夋嫨鏌滃瓙鍙�"
+                  @update:value="handleSiloChange"
+                />
+              </NFormItemGi>
+              <NFormItemGi span="24 s:12 m:8">
+                <NSpace>
+                  <NButton type="primary" :loading="loading" @click="handleSearch">
+                    <template #icon>
+                      <icon-ic-round-search class="text-icon" />
+                    </template>
+                    鏌ヨ
+                  </NButton>
+                  <NButton @click="handleReset">
+                    <template #icon>
+                      <icon-ic-round-refresh class="text-icon" />
+                    </template>
+                    閲嶇疆
+                  </NButton>
+                </NSpace>
+              </NFormItemGi>
+            </NGrid>
+          </NForm>
+        </NCollapseItem>
+      </NCollapse>
+    </NCard>
+
+    <!-- 鎶ヨ〃鏄剧ず鍖哄煙 -->
+    <NCard
+      :bordered="false"
+      size="small"
+      class="card-wrapper flex flex-col sm:flex-1-hidden"
+      content-style="padding: 0; flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden;"
+    >
+      <div class="report-root">
+        <div class="report-toolbar">
+          <div class="flex-y-center gap-8px">
+            <NButton size="small" type="warning" ghost @click="handleExport">
+              <template #icon>
+                <icon-mdi-export class="text-icon" />
+              </template>
+              瀵煎嚭 Excel
+            </NButton>
+            <NButton size="small" secondary @click="handleExportPdf">
+              <template #icon>
+                <icon-mdi-file-pdf-box class="text-icon" />
+              </template>
+              瀵煎嚭 PDF
+            </NButton>
+            <NButton size="small" secondary @click="handleExportCsv">
+              <template #icon>
+                <icon-mdi-file-delimited class="text-icon" />
+              </template>
+              瀵煎嚭 CSV
+            </NButton>
+            <NButton size="small" type="primary" @click="handlePrint">
+              <template #icon>
+                <icon-mdi-printer class="text-icon" />
+              </template>
+              鎵撳嵃
+            </NButton>
+          </div>
+          <div class="flex-y-center gap-8px">
+            <span class="report-toolbar__label">缂╂斁</span>
+            <NButton size="small" @click="setZoom(zoom - 0.1)">-</NButton>
+            <NInputNumber v-model:value="zoom" size="small" :min="0.6" :max="1.2" :step="0.1" class="w-90px" />
+            <NButton size="small" @click="setZoom(zoom + 0.1)">+</NButton>
+          </div>
+        </div>
+
+        <div class="report-stage">
+          <NSpin :show="exportLoading" size="large" description="瀵煎嚭涓�..." :delay="0">
+            <div id="report-capture" class="report-sheet" :style="{ transform: `scale(${zoom})` }">
+              <!-- 鎶ヨ〃鏍囬 -->
+              <div class="report-title">鎵规浜ч噺缁熻鎶ヨ〃</div>
+
+            <!-- 鏃ユ湡鑼冨洿鏄剧ず -->
+            <div class="report-date-range">
+              <span class="report-date-range__text">{{ dateRangeText }}</span>
+            </div>
+
+            <!-- 鏁版嵁琛ㄦ牸 -->
+            <div class="report-table-scroll">
+              <table id="report-table" class="report-table">
+                <thead>
+                  <tr>
+                    <th rowspan="2" class="col-batch-no">鎵规鍙�</th>
+                    <th rowspan="2" class="col-brand">鐗屽彿</th>
+                    <th rowspan="2" class="col-time">鎵规寮�濮嬫椂闂�</th>
+                    <th rowspan="2" class="col-time">鎵规缁撴潫鏃堕棿</th>
+                    <th rowspan="2" class="col-cabinet">鏌滃彿</th>
+                    <th rowspan="2" class="col-weight">鐑熶笣閲嶉噺</th>
+                    <th rowspan="2" class="col-unit">鐢熶骇鏈虹粍</th>
+                    <th class="col-output">鍗风儫鏈烘�讳骇</th>
+                    <th class="col-output">鍖呰鏈烘�讳骇</th>
+                    <th class="col-output">鎻愬崌鏈烘�讳骇</th>
+                  </tr>
+                  <tr>
+                    <th class="col-output">閲忥紙绠憋級</th>
+                    <th class="col-output">閲忥紙绠憋級</th>
+                    <th class="col-output">閲忥紙绠憋級</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr v-for="row in tableRows" :key="row.id">
+                    <td>{{ row.batchcode }}</td>
+                    <td class="report-left">{{ row.materialname }}</td>
+                    <td>{{ row.distimebegin }}</td>
+                    <td>{{ row.distimeend }}</td>
+                    <td>{{ formatSiloId(row.siloid) }}</td>
+                    <td>{{ calculateSilkWeight(row.jobinput, row.weight) }}</td>
+                    <td>{{ formatProductionUnits(row.rollerDetailList) }}</td>
+                    <td>{{ calcRollerBox(row.rollerOutput) }}</td>
+                    <td>{{ calcPackerBox(row.packerOutput) }}</td>
+                    <td>{{ calcPackerBox(row.packerOutput) }}</td>
+                  </tr>
+                </tbody>
+              </table>
+            </div>
+          </div>
+          </NSpin>
+        </div>
+      </div>
+    </NCard>
+  </div>
+</template>
+
+<style scoped>
+/* ==================== 鎶ヨ〃瀹瑰櫒 ==================== */
+.report-root {
+  background: #f3f4f6;
+  padding: 12px;
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  min-height: 0;
+}
+
+.report-toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 12px;
+  background: #fff;
+  border-bottom: 1px solid #e5e7eb;
+  flex: none;
+}
+
+.report-toolbar__label {
+  font-size: 12px;
+  color: #6b7280;
+}
+
+.report-stage {
+  padding: 12px;
+  overflow: auto;
+  flex: 1;
+  min-height: 0;
+}
+
+.report-sheet {
+  transform-origin: top center;
+  background: #fff;
+  width: 1200px;
+  margin: 0 auto;
+  padding: 18px 18px 16px;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
+}
+
+/* ==================== 鎶ヨ〃鏍囬 ==================== */
+.report-title {
+  text-align: center;
+  font-size: 18px;
+  font-weight: 600;
+  padding: 10px 0 4px;
+  color: #111827;
+}
+
+/* ==================== 鏃ユ湡鑼冨洿 ==================== */
+.report-date-range {
+  display: flex;
+  justify-content: flex-end;
+  padding: 4px 0 8px;
+}
+
+.report-date-range__text {
+  font-size: 13px;
+  font-weight: 600;
+  color: #0066cc;
+}
+
+/* ==================== 琛ㄦ牸 ==================== */
+.report-table-scroll {
+  width: 100%;
+  overflow-x: auto;
+  overflow-y: hidden;
+}
+
+.report-table {
+  width: 100%;
+  min-width: 1100px;
+  border-collapse: collapse;
+  table-layout: fixed;
+  font-size: 12px;
+  color: #111827;
+}
+
+/* 鍒楀瀹氫箟 */
+.col-batch-no {
+  width: 120px;
+}
+
+.col-brand {
+  width: 170px;
+}
+
+.col-time {
+  width: 150px;
+}
+
+.col-cabinet {
+  width: 50px;
+}
+
+.col-weight {
+  width: 80px;
+}
+
+.col-unit {
+  width: 95px;
+}
+
+.col-output {
+  width: 90px;
+}
+
+/* 琛ㄥご鏍峰紡 - 钃濊壊鑳屾櫙鐧借壊鏂囧瓧 */
+.report-table th {
+  border: 1px solid #4a90d9;
+  padding: 6px 6px;
+  text-align: center;
+  vertical-align: middle;
+  line-height: 1.3;
+  background: #337ab7;
+  color: #fff;
+  font-weight: 600;
+  font-size: 12px;
+}
+
+/* 鏁版嵁鍗曞厓鏍� */
+.report-table td {
+  border: 1px solid #ccc;
+  padding: 6px 6px;
+  text-align: center;
+  vertical-align: middle;
+  line-height: 1.3;
+}
+
+/* 闅旇鍙樿壊 */
+.report-table tbody tr:nth-child(even) {
+  background: #f9f9f9;
+}
+
+.report-table tbody tr:hover {
+  background: #e8f0fe;
+}
+
+/* 宸﹀榻� */
+.report-left {
+  text-align: left;
+}
+
+/* ==================== 鎵撳嵃鏍峰紡 ==================== */
+@media print {
+  @page {
+    size: A4 landscape;
+    margin: 10mm;
+  }
+
+  .report-root {
+    background: #fff;
+    padding: 0;
+  }
+
+  .report-toolbar {
+    display: none;
+  }
+
+  .report-stage {
+    padding: 0;
+    overflow: visible;
+  }
+
+  .report-sheet {
+    width: 100%;
+    margin: 0;
+    padding: 0;
+    box-shadow: none;
+    transform: none !important;
+  }
+
+  .report-table-scroll {
+    overflow: visible;
+  }
+
+  .report-table {
+    width: 100%;
+    min-width: 0;
+    table-layout: fixed;
+  }
+}
+</style>

--
Gitblit v1.9.3