|
|
|
|
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),
|
|
|
|
|
};
|
|
|
|
|
};
|