该方法适合复杂界面分成多个组件,组件里面又嵌套子组件,且要触发的方法组参复杂,比如我这个就涉及到canvas转html转pdf的报告导出功能。
方案1:使用window挂载目标方法(简单快捷)
父组件:
javascript
const handlePowerSwitch = async (on) => {
// 单机版停止调度时记录仿真报告
if (appStore.features === 'emulator' && !on) {
// 直接调用孙子组件的方法,等待完成
if (window.exportSimulationReport) {
await window.exportSimulationReport();
}
}
const res = await appStore.changePowerAction(on);
if (!res.success) {
appStore.power_action = !on;
}
};
孙子组件:
javascript
// 提供全局方法供父组件调用
const exportSimulationReport = async () => {
if (exporting.value) return;
try {
exporting.value = true;
// 准备地图信息数据
const blocks = Models.allBlocks();
const mapStats = {
total: Object.keys(blocks).length,
charger: 0,
entry: 0,
exit: 0,
wait: 0,
queue: 0,
sorter: 0,
};
// 使用更高效的方式统计
Object.values(blocks).forEach(block => {
const statsMap = {
'chargerPort': 'charger',
'loadPort': 'entry',
'unloadPort': 'exit',
'waitPort': 'wait',
'queuePort': 'queue',
'sorterPort': 'sorter'
};
if (statsMap[block.tag]) {
mapStats[statsMap[block.tag]]++;
}
});
// 更新地图信息数据
mapInfoData.value = [
{ label: 'scene.report.total_blocks', value: mapStats.total },
{ label: 'scene.report.charger_count', value: mapStats.charger },
{ label: 'scene.report.entry_count', value: mapStats.entry },
{ label: 'scene.report.exit_count', value: mapStats.exit },
{ label: 'scene.report.wait_count', value: mapStats.wait },
{ label: 'scene.report.queue_count', value: mapStats.queue },
{ label: 'scene.report.sorter_count', value: mapStats.sorter },
];
// 生成地图 SVG 数据
const svgData = generateMapSVG(blocks, t);
mapSvgData.value = svgData;
// 使用微任务等待渲染更新
await nextTick();
// 添加延迟确保渲染完成
await new Promise(resolve => requestAnimationFrame(() =>
setTimeout(resolve, 100)
));
// 生成报告
await generatePDFReport(mapStats);
} catch (error) {
console.error('导出报告失败:', error);
throw error; // 向上传递错误
} finally {
exporting.value = false;
}
};
// 将PDF生成分离为独立函数
const generatePDFReport = async (mapStats) => {
return new Promise(async (resolve, reject) => {
try {
const template = document.getElementById('report-template');
const canvas = await html2canvas(template, {
scale: 1.5, // 降低缩放比例提高性能
useCORS: true,
logging: false,
backgroundColor: '#ffffff',
async: true, // 启用异步渲染
removeContainer: true, // 清理临时容器
onclone: (clonedDoc) => {
const style = clonedDoc.createElement('style');
style.innerHTML = `
@font-face {
font-family: 'Microsoft YaHei';
src: local('Microsoft YaHei'), local('PingFang SC'), local('SimHei');
}
* {
font-family: 'Microsoft YaHei', 'PingFang SC', 'SimHei', Arial, sans-serif !important;
}
svg {
width: 100% !important;
height: auto !important;
max-height: 400px !important;
}
`;
clonedDoc.head.appendChild(style);
},
});
const imgData = canvas.toDataURL('image/jpeg', 0.9); // 降低图片质量
// 创建 PDF
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4',
});
const pageWidth = pdf.internal.pageSize.width;
const pageHeight = pdf.internal.pageSize.height;
const imgWidth = pageWidth;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
// 分页处理
let heightLeft = imgHeight;
let position = 0;
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
while (heightLeft > 0) {
position = heightLeft - imgHeight;
pdf.addPage();
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
// 保存到IndexDB
const fileName = `simulation_report_${Date.now()}.pdf`;
const dataUriString = pdf.output('datauristring');
const base64 = dataUriString.split(',')[1];
await localDB.addReport({
title: fileName,
base64: base64,
duration: consoleStore.info.duration,
throughput_per_hour: consoleStore.info.throughput_per_hour,
robots: consoleStore.info.robots,
total: mapStats.total,
});
resolve();
} catch (error) {
reject(error);
}
});
};
// 在组件挂载时注册方法
onMounted(() => {
window.exportSimulationReport = exportSimulationReport;
Scene_.getRatio().then(res => {
const data = res.data || [];
envRatio.value = data.map(item => ({
value: item,
label: 'x' + item,
}));
});
});
onUnmounted(() => {
// 清理全局方法
delete window.exportSimulationReport;
sceneTimer.value && clearInterval(sceneTimer.value);
});
方案2:使用Vue的Provide/Inject(更优雅)
父组件:
javascript
// 提供方法给子孙组件
const exportMethods = {
exportSimulationReport: null
};
provide('exportMethods', exportMethods);
const handlePowerSwitch = async (on) => {
if (appStore.features === 'emulator' && !on) {
if (exportMethods.exportSimulationReport) {
await exportMethods.exportSimulationReport();
}
}
const res = await appStore.changePowerAction(on);
if (!res.success) {
appStore.power_action = !on;
}
};
孙子组件:
javascript
const exportMethods = inject('exportMethods');
onMounted(() => {
// 注册导出方法
exportMethods.exportSimulationReport = exportSimulationReport;
});
onUnmounted(() => {
// 清理方法
exportMethods.exportSimulationReport = null;
});
方案3:addEventListener+listenerComplete
1、若目标方法无异步函数或不需要等待
在主组件中添加dispatchEvent,用来触发目标事件
html
// index.js
<script setup>
const handlePowerSwitch = async on => {
if( appStore.features === 'emulator' && !on) {
// 触发孙子组件导出仿真报告事件
window.dispatchEvent(new CustomEvent('exportSimulationReport'));
}
// ......rest
};
</script>
在目标组件中添加addEventListener
html
// SceneInfoPanel.vue
<script setup>
import { onMounted, onUnmounted } from 'vue';
// 监听导出报告事件
const handleExportReportEvent = () => {
handleExportReport();
};
onMounted(() => {
// 添加事件监听器
window.addEventListener('exportSimulationReport', handleExportReportEvent);
// ......rest
});
onUnmounted(() => {
// 清理事件监听器
window.removeEventListener('exportSimulationReport', handleExportReportEvent);
});
</script>
2、若目标方法有异步函数,下一步操作需等待目标方法结束
html
<script setup>
// ... 其他导入 ...
const handlePowerSwitch = async on => {
// 单机版停止调度时记录仿真报告
if (appStore.features === 'emulator' && !on) {
// 等待导出仿真报告完成
await new Promise((resolve) => {
// 创建一次性事件监听器
const handleExport = () => {
window.removeEventListener('exportSimulationReportComplete', handleExport);
resolve();
};
window.addEventListener('exportSimulationReportComplete', handleExport);
// 触发导出仿真报告事件
window.dispatchEvent(new CustomEvent('exportSimulationReport'));
});
}
const res = await appStore.changePowerAction(on);
// 若开关机失败 开关要复原
if (!res.success) {
appStore.power_action = !on;
}
};
// ... 其他代码 ...
</script>
在目标组件中:
html
<script setup>
// ... 其他导入和代码 ...
// 修改 dispatchExport 函数,在完成后发送完成事件
const dispatchExport = async() => {
if (exporting.value) return; // 防止重复导出
try {
exporting.value = true;
// 准备地图信息数据
const blocks = Models.allBlocks();
const mapStats = {
total: Object.keys(blocks).length,
charger: 0,
entry: 0,
exit: 0,
wait: 0,
queue: 0,
sorter: 0,
};
for (let block of Object.values(blocks)) {
if (block.tag === 'chargerPort') mapStats.charger++;
else if (block.tag === 'loadPort') mapStats.entry++;
else if (block.tag === 'unloadPort') mapStats.exit++;
else if (block.tag === 'waitPort') mapStats.wait++;
else if (block.tag === 'queuePort') mapStats.queue++;
else if (block.tag === 'sorterPort') mapStats.sorter++;
}
// 更新地图信息数据
mapInfoData.value = [
{ label: 'scene.report.total_blocks', value: mapStats.total },
{ label: 'scene.report.charger_count', value: mapStats.charger },
{ label: 'scene.report.entry_count', value: mapStats.entry },
{ label: 'scene.report.exit_count', value: mapStats.exit },
{ label: 'scene.report.wait_count', value: mapStats.wait },
{ label: 'scene.report.queue_count', value: mapStats.queue },
{ label: 'scene.report.sorter_count', value: mapStats.sorter },
];
// 生成地图 SVG 数据
const svgData = generateMapSVG(blocks, t);
mapSvgData.value = svgData;
try {
// 等待一下确保内容都已渲染
await new Promise(resolve => setTimeout(resolve, 500));
// 使用 html2canvas 将地图转换为图片
const template = document.getElementById('report-template');
const canvas = await html2canvas(template, {
scale: 2,
useCORS: true,
logging: false,
backgroundColor: '#ffffff',
onclone: clonedDoc => {
// 添加字体样式
const style = clonedDoc.createElement('style');
style.innerHTML = `
@font-face {
font-family: 'Microsoft YaHei';
src: local('Microsoft YaHei'), local('PingFang SC'), local('SimHei');
}
* {
font-family: 'Microsoft YaHei', 'PingFang SC', 'SimHei', Arial, sans-serif !important;
}
svg {
width: 100% !important;
height: auto !important;
max-height: 400px !important;
}
`;
clonedDoc.head.appendChild(style);
},
});
const imgData = canvas.toDataURL('image/jpeg', 1.0);
// 创建 PDF
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4',
// 直接设置页边距
marginTop: 50,
marginBottom: 40,
marginLeft: 30,
marginRight: 30,
});
// 获取页面尺寸
const pageWidth = pdf.internal.pageSize.width;
const pageHeight = pdf.internal.pageSize.height;
// 计算图片尺寸以适应 A4 页面
const imgWidth = pageWidth;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
// 如果内容超过一页,需要分页处理
let heightLeft = imgHeight;
let position = 0;
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
while (heightLeft > 0) {
position = heightLeft - imgHeight;
pdf.addPage();
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
// 生成文件名
const fileName = `simulation_report_${new Date()
.toISOString()
.replace(/[^0-9]/g, '')
.slice(0, 14)}.pdf`;
const dataUriString = pdf.output('datauristring');
const base64 = dataUriString.split(',')[1];
// 保存至indexDB
await localDB.addReport({
title: fileName,
base64: base64,
duration: consoleStore.info.duration,
throughput_per_hour: consoleStore.info.throughput_per_hour,
robots: consoleStore.info.robots,
total: mapStats.total,
});
} finally {
}
} catch (error) {
console.error('导出报告失败:', error);
} finally {
exporting.value = false;
// 发送导出完成事件
window.dispatchEvent(new CustomEvent('exportSimulationReportComplete'));
}
}
// ... 其他代码 ...
</script>