You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

393 lines
12 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import ExcelJS from "exceljs";
interface TrainReportData {
train_id: string;
arrive_at: string;
leave_at: string | null;
train_data: { carriage_number: string }[];
appearance_data: {
carriage_number: string;
carriage_type: string;
occurrence_time: string;
warning_type: string;
fault_type: string;
defect_image: string;
}[];
}
const formatDateTime = (dateTimeStr: string) => {
const date = new Date(dateTimeStr);
return date.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
};
// 使用您指定的图片URL
const DEFAULT_IMAGE_URL =
"https://img1.baidu.com/it/u=967792105,51180745&fm=253&fmt=auto?w=1200&h=800";
const convertImageToBase64 = async (url: string) => {
try {
const response = await fetch(url);
const blob = await response.blob();
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (error) {
console.error("Error loading image:", error);
return null;
}
};
const getImageDimensions = (
url: string
): Promise<{ width: number; height: number }> => {
return new Promise((resolve) => {
const img = new Image();
const timer = setTimeout(() => {
resolve({ width: 80, height: 60 }); // 默认尺寸
}, 3000);
img.crossOrigin = "Anonymous";
img.onload = () => {
clearTimeout(timer);
resolve({ width: img.width, height: img.height });
};
img.onerror = () => {
clearTimeout(timer);
resolve({ width: 80, height: 60 });
};
img.src = url;
});
};
// 下载图片并转换为ArrayBuffer
const downloadImage = async (url: string): Promise<ExcelJS.Image | null> => {
try {
const finalUrl = url || DEFAULT_IMAGE_URL;
let response: Response;
try {
response = await fetch(finalUrl);
if (!response.ok) throw new Error("直接下载失败");
} catch (directError) {
// 尝试使用CORS代理
try {
const proxyUrl = `https://corsproxy.io/?${encodeURIComponent(
finalUrl
)}`;
response = await fetch(proxyUrl);
if (!response.ok) throw new Error("代理下载失败");
} catch (proxyError) {
console.error("图片下载失败:", proxyError);
return null;
}
}
const arrayBuffer = await response.arrayBuffer();
const extension = finalUrl.split(".").pop()?.toLowerCase() || "jpeg";
const { width, height } = await getImageDimensions(finalUrl);
// 计算缩放比例(保持宽高比)
const maxWidth = 80;
const maxHeight = 60;
const scale = Math.min(maxWidth / width, maxHeight / height, 1);
return {
buffer: arrayBuffer, // 直接使用ArrayBuffer
extension: extension === "png" ? "png" : "jpeg",
width: width * scale,
height: height * scale,
};
} catch (error) {
console.error("图片处理失败:", error);
return null;
}
};
// 像素转换为Excel的EMU单位 (Excel使用EMU作为度量单位)
const pixelsToEMU = (pixels: number) => Math.round(pixels * 9525);
export const exportTrainReport = async (data: TrainReportData) => {
const workbook = new ExcelJS.Workbook();
workbook.creator = "Railway Inspection System";
workbook.created = new Date();
// 1. 创建"整车信息"工作表 (无表头行)
const trainSheet = workbook.addWorksheet("整车信息");
trainSheet.getColumn(1).width = 20;
trainSheet.getColumn(2).width = 30;
// 直接添加数据行
const trainInfoData = [
{ key: "车头编号", value: data.train_id },
{ key: "进场时间", value: formatDateTime(data.arrive_at) },
{
key: "出场时间",
value: data.leave_at ? formatDateTime(data.leave_at) : "未出场",
},
{ key: "车厢列表", value: "" },
];
trainInfoData.forEach((info, index) => {
const row = index + 1;
trainSheet.getCell(`A${row}`).value = info.key;
trainSheet.getCell(`B${row}`).value = info.value;
// 设置第一列加粗
trainSheet.getCell(`A${row}`).font = { bold: true };
// 设置第二列加粗(除车厢列表行)
if (row < 4) {
trainSheet.getCell(`B${row}`).font = { bold: true };
}
});
// 添加车厢列表
const startRow = trainInfoData.length + 1;
data.train_data.forEach((carriage, index) => {
const row = startRow + index;
trainSheet.getCell(`A${row}`).value = `车厢 ${index + 1}`;
trainSheet.getCell(`B${row}`).value = carriage.carriage_number;
});
// 2. 创建"车身缺陷记录"工作表
const defectSheet = workbook.addWorksheet("车身缺陷记录");
// 修改列定义
defectSheet.columns = [
{ header: "车厢号", key: "carriage_number", width: 15 },
// { header: "车型", key: "carriage_type", width: 20 },
{ header: "发生时间", key: "occurrence_time", width: 20 },
{ header: "告警类型", key: "warning_type", width: 15 },
{ header: "缺陷类型", key: "fault_type", width: 15 },
{ header: "缺陷图片", key: "thumbnail", width: 15 }, // 缩略图列
// { header: "查看原图", key: "defect_image", width: 15 }, // 原图链接列
];
// 设置表头样式
defectSheet.getRow(1).eachCell((cell) => {
cell.font = { bold: true };
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFDDEBF7" }, // 浅蓝色背景
};
cell.border = {
top: { style: "thin" },
left: { style: "thin" },
bottom: { style: "thin" },
right: { style: "thin" },
};
});
// 设置所有行高60像素≈45磅
defectSheet.properties.defaultRowHeight = 45;
// 添加所有缺陷记录
for (let i = 0; i < data.appearance_data.length; i++) {
const item = data.appearance_data[i];
const rowIndex = i + 2; // 从第二行开始(第一行是表头)
// 添加文本数据
defectSheet.addRow({
carriage_number: item.carriage_number,
carriage_type: item.carriage_type,
occurrence_time: formatDateTime(item.occurrence_time),
warning_type: item.warning_type,
fault_type: item.fault_type,
});
// 使用指定图片URL
const imageUrl = item.defect_image || DEFAULT_IMAGE_URL;
try {
// 下载图片
// const image = await downloadImage(imageUrl);
// console.log("图片下载成功", image);
const base64Image = await convertImageToBase64(imageUrl);
if (base64Image) {
const imageId = workbook.addImage({
base64: base64Image,
extension: "png", // Adjust extension if needed (jpeg, etc.)
});
// TODO : 调整图片大小和位置 (根据需要调整)
// // 计算缩放比例和显示尺寸
// const maxSize = 30; // 最大边长30像素
// const scale = Math.min(maxSize / image.width, maxSize / image.height);
// const displayWidth = image.width * scale;
// const displayHeight = image.height * scale;
// // 列宽(像素)和行高(像素)
// const colWidthPixels = 105; // 15字符 * 7
// const rowHeightPixels = 60; // 45磅 * (96/72) = 60像素
// // 计算偏移量(居中)
// const offsetXPixels = (colWidthPixels - displayWidth) / 2;
// const offsetYPixels = (rowHeightPixels - displayHeight) / 2;
// // 转换为EMU
// const offsetXEMU = pixelsToEMU(offsetXPixels);
// const offsetYEMU = pixelsToEMU(offsetYPixels);
// const widthEMU = pixelsToEMU(displayWidth);
// const heightEMU = pixelsToEMU(displayHeight);
defectSheet.addImage(imageId, {
tl: { col: defectSheet.columns.length - 1, row: rowIndex - 1 }, // F列是第5列0-based
// br: { col: 6, row: rowIndex },
ext: {
width: 80,
height: 45,
},
editAs: "oneCell",
});
}
} catch (error) {
console.error(`图片添加失败 (行 ${rowIndex}):`, error);
const errorCell = defectSheet.getCell(`E${rowIndex}`);
errorCell.value = "图片加载失败";
errorCell.font = {
color: { argb: "FFFF0000" }, // 红色
italic: true,
};
}
}
// 3. 生成Excel文件
const buffer = await workbook.xlsx.writeBuffer();
try {
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 = "车辆信息.xlsx";
link.click();
URL.revokeObjectURL(url);
} catch (error) {
console.error("导出 Excel 文件时出错:", error);
}
};
// ... formatDateTime 函数保持不变 ...
export const useTrainSaveToExcel = (data) => {
// 模拟数据 (实际应用中应通过API获取)
const reportData = {
train_id: "DF4-1234",
arrive_at: "2023-06-15 08:30:00",
leave_at: "2023-06-15 16:45:22",
train_data: [
{ carriage_number: "CK-1001" },
{ carriage_number: "CK-1002" },
{ carriage_number: "CK-1003" },
],
appearance_data: [
{
carriage_number: "CK-1001",
carriage_type: "C80型运煤敞车",
occurrence_time: "2023-06-15 09:15:34",
warning_type: "AI智能识别告警",
fault_type: "车体变形",
defect_image:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1002",
carriage_type: "P70型棚车",
occurrence_time: "2023-06-15 10:22:18",
warning_type: "人工巡检报告",
fault_type: "油漆剥落",
defect_image:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1001",
carriage_type: "C80型运煤敞车",
occurrence_time: "2023-06-15 09:15:34",
warning_type: "AI智能识别告警",
fault_type: "车体变形",
defect_image:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1002",
carriage_type: "P70型棚车",
occurrence_time: "2023-06-15 10:22:18",
warning_type: "人工巡检报告",
fault_type: "油漆剥落",
defect_image:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1001",
carriage_type: "C80型运煤敞车",
occurrence_time: "2023-06-15 09:15:34",
warning_type: "AI智能识别告警",
fault_type: "车体变形",
defect_image:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1002",
carriage_type: "P70型棚车",
occurrence_time: "2023-06-15 10:22:18",
warning_type: "人工巡检报告",
fault_type: "油漆剥落",
defect_image:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1001",
carriage_type: "C80型运煤敞车",
occurrence_time: "2023-06-15 09:15:34",
warning_type: "AI智能识别告警",
fault_type: "车体变形",
defect_image:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1002",
carriage_type: "P70型棚车",
occurrence_time: "2023-06-15 10:22:18",
warning_type: "人工巡检报告",
fault_type: "油漆剥落",
defect_image:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1001",
carriage_type: "C80型运煤敞车",
occurrence_time: "2023-06-15 09:15:34",
warning_type: "AI智能识别告警",
fault_type: "车体变形",
defect_image:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1002",
carriage_type: "P70型棚车",
occurrence_time: "2023-06-15 10:22:18",
warning_type: "人工巡检报告",
fault_type: "油漆剥落",
defect_image:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
],
};
return {
saveToExcel: () => exportTrainReport(reportData),
};
};