<script lang="ts" setup>
|
import type { ManagementCockpitApi } from '#/api/mes/management-cockpit';
|
|
import { ref } from 'vue';
|
|
import { Page } from '@vben/common-ui';
|
import { IconifyIcon } from '@vben/icons';
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
|
import { Button, message, Space } from 'ant-design-vue';
|
// 变量名 vPrint(v 开头)在 <script setup> 中会被 Vue 自动注册为局部指令 v-print
|
// @ts-expect-error - vue3-print-nb 未提供类型声明
|
import vPrint from 'vue3-print-nb';
|
|
import {
|
exportManagementCockpitExcel,
|
getManagementCockpitSummary,
|
} from '#/api/mes/management-cockpit';
|
|
import KpiCards from './modules/kpi-cards.vue';
|
import InventoryChart from './modules/inventory-chart.vue';
|
import OutboundChart from './modules/outbound-chart.vue';
|
import ProductionTrend from './modules/production-trend.vue';
|
import QualityChart from './modules/quality-chart.vue';
|
import TraceChart from './modules/trace-chart.vue';
|
|
defineOptions({ name: 'ManagementCockpit' });
|
|
// 打印配置(打印报表容器)
|
const printObj = ref({
|
id: 'management-cockpit-report',
|
popTitle: ' ',
|
extraCss: '/print.css',
|
zIndex: 20_003,
|
});
|
|
const summary = ref<ManagementCockpitApi.Summary>({
|
batchTotal: 0,
|
batchDraft: 0,
|
batchProducing: 0,
|
batchFinished: 0,
|
batchCanceled: 0,
|
bagCodeTotal: 0,
|
bagCodeGenerated: 0,
|
bagCodePrinted: 0,
|
bagCodeCanceled: 0,
|
palletTotal: 0,
|
todayInboundBagCount: 0,
|
qualityTotal: 0,
|
qualityPending: 0,
|
qualityPass: 0,
|
qualityFail: 0,
|
qualityPassRate: 0,
|
stockTotal: 0,
|
stockTemporary: 0,
|
stockQualified: 0,
|
stockOutbound: 0,
|
lowStockCount: 0,
|
outboundOrderTotal: 0,
|
outboundQuantityTotal: 0,
|
todayOutboundQuantity: 0,
|
scanTotal: 0,
|
todayScanCount: 0,
|
scanSuccessCount: 0,
|
crossRegionWarningTotal: 0,
|
crossRegionPendingCount: 0,
|
});
|
|
// 通过递增 key 触发子组件重新挂载实现整体刷新
|
const reloadKey = ref(0);
|
|
const loading = ref(false);
|
|
/** 加载汇总统计 */
|
async function loadSummary() {
|
try {
|
summary.value = await getManagementCockpitSummary();
|
} catch {
|
// 接口异常时保留默认值,避免页面崩溃
|
}
|
}
|
|
/** 刷新全部数据 */
|
async function handleRefresh() {
|
loading.value = true;
|
try {
|
await Promise.all([loadSummary()]);
|
reloadKey.value += 1;
|
message.success('已刷新');
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
/** 导出 Excel 报表(后端生成多 sheet 工作簿) */
|
async function handleExportExcel() {
|
try {
|
const data = await exportManagementCockpitExcel();
|
downloadFileFromBlobPart({
|
fileName: `管理驾驶舱统计报表-${new Date().toLocaleDateString()}.xls`,
|
source: data,
|
});
|
message.success('Excel 报表已导出');
|
} catch {
|
// 错误已由 requestClient 统一提示
|
}
|
}
|
|
/** 导出 PDF 报表(前端 html2canvas + jspdf 生成可下载文件) */
|
async function handleExportPdf() {
|
const target = document.getElementById('management-cockpit-report');
|
if (!target) {
|
message.warning('报表区域未就绪');
|
return;
|
}
|
const hide = message.loading('正在生成 PDF,请稍候…', 0);
|
try {
|
// 动态加载,避免拖慢首屏(html-to-image 通过 SVG foreignObject 让浏览器原生渲染,
|
// 规避 html2canvas 不支持 oklch 等现代 CSS 颜色函数的问题)
|
const [{ toPng }, { jsPDF }] = await Promise.all([
|
import('html-to-image'),
|
import('jspdf'),
|
]);
|
const dataUrl = await toPng(target, {
|
backgroundColor: '#ffffff',
|
cacheBust: true,
|
pixelRatio: 2,
|
});
|
const pdf = new jsPDF({ compress: true, format: 'a4', orientation: 'p', unit: 'mm' });
|
const pageWidth = pdf.internal.pageSize.getWidth();
|
const pageHeight = pdf.internal.pageSize.getHeight();
|
const imgWidth = pageWidth;
|
const imgHeight = (target.offsetHeight * imgWidth) / target.offsetWidth;
|
|
let heightLeft = imgHeight;
|
let position = 0;
|
pdf.addImage(dataUrl, 'PNG', 0, position, imgWidth, imgHeight);
|
heightLeft -= pageHeight;
|
while (heightLeft > 0) {
|
position -= pageHeight;
|
pdf.addPage();
|
pdf.addImage(dataUrl, 'PNG', 0, position, imgWidth, imgHeight);
|
heightLeft -= pageHeight;
|
}
|
pdf.save(`管理驾驶舱统计报表-${new Date().toLocaleDateString()}.pdf`);
|
message.success('PDF 报表已导出');
|
} catch (error) {
|
console.error('[management-cockpit] PDF 导出失败:', error);
|
message.error(
|
`PDF 导出失败:${error instanceof Error ? error.message : '未知错误'},请打开浏览器控制台(F12)查看详情`,
|
);
|
} finally {
|
hide();
|
}
|
}
|
|
/** 加载汇总统计 */
|
async function loadInitial() {
|
await loadSummary();
|
}
|
loadInitial();
|
</script>
|
|
<template>
|
<Page>
|
<!-- 工具栏 -->
|
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
|
<div class="flex items-center gap-2">
|
<IconifyIcon class="text-xl text-blue-500" icon="lucide:gauge" />
|
<span class="text-base font-semibold">管理驾驶舱与统计分析</span>
|
</div>
|
<Space>
|
<Button :loading="loading" @click="handleRefresh">
|
<template #icon><IconifyIcon icon="lucide:refresh-cw" /></template>
|
刷新
|
</Button>
|
<Button v-access:code="['mes:dashboard:export']" type="primary" @click="handleExportExcel">
|
<template #icon><IconifyIcon icon="lucide:file-spreadsheet" /></template>
|
导出 Excel
|
</Button>
|
<Button v-access:code="['mes:dashboard:export']" type="primary" ghost @click="handleExportPdf">
|
<template #icon><IconifyIcon icon="lucide:file-down" /></template>
|
导出 PDF
|
</Button>
|
<Button v-print="printObj">
|
<template #icon><IconifyIcon icon="lucide:printer" /></template>
|
打印
|
</Button>
|
</Space>
|
</div>
|
|
<!-- 报表内容(Excel/PDF/打印 均基于此区域) -->
|
<div id="management-cockpit-report" class="rounded-lg bg-white p-4 dark:bg-gray-800">
|
<!-- 五维度 KPI -->
|
<KpiCards :key="`kpi-${reloadKey}`" :summary="summary" class="mb-2" />
|
|
<!-- 生产趋势 -->
|
<ProductionTrend :key="`pt-${reloadKey}`" class="mb-4" />
|
|
<QualityChart :key="`qc-${reloadKey}`" class="mb-4" />
|
|
<InventoryChart :key="`iv-${reloadKey}`" class="mb-4" />
|
<OutboundChart :key="`ob-${reloadKey}`" class="mb-4" />
|
<TraceChart :key="`tr-${reloadKey}`" class="mb-4" />
|
</div>
|
</Page>
|
</template>
|