fix: lỗi font Bold của xuất pdf

This commit is contained in:
2026-06-24 18:17:42 +07:00
parent 4b551ccc31
commit 957ba6c72c
7 changed files with 91 additions and 466 deletions
+88 -30
View File
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useMemo, useRef } from 'react';
import { io } from 'socket.io-client';
import { jsPDF } from 'jspdf';
import autoTable from 'jspdf-autotable';
import { robotoBase64 } from '../utils/pdfFont';
import { robotoBase64, robotoBoldBase64 } from '../utils/pdfFont';
import { ItineraryTimeline } from '../components/ItineraryTimeline';
import { ExpenseManager } from '../components/ExpenseManager';
import { useTourStore } from '@/store/useTourStore';
@@ -486,12 +486,12 @@ export const TourDetailPage = ({
const handleExportPDF = async () => {
const doc = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' });
// 1. Thêm font vào Virtual File System của jsPDF để hỗ trợ tiếng Việt
doc.addFileToVFS("Roboto-Regular.ttf", robotoBase64);
doc.addFont("Roboto-Regular.ttf", "Roboto", "normal");
doc.addFileToVFS("Roboto-Bold.ttf", robotoBoldBase64);
doc.addFont("Roboto-Bold.ttf", "Roboto", "bold");
doc.setFont("Roboto");
// 2. Vẽ Tiêu đề & Thông tin Tour ở đầu trang
const titleText = `LỊCH TRÌNH TOUR: ${currentTour?.title?.toUpperCase() || 'HÀNH TRÌNH TOUR'}`;
doc.setFontSize(16);
doc.text(titleText, 148.5, 15, { align: 'center' });
@@ -508,9 +508,9 @@ export const TourDetailPage = ({
doc.text(dateRangeStr, 148.5, startY, { align: 'center' });
startY += 8;
// 3. Chuẩn bị dữ liệu bảng
const tableColumn = ["STT", "Ngày giờ", "Chặng", "Địa điểm", "Ghi chú"];
const tableRows: any[] = [];
const legRowRanges: Record<string, { start: number; count: number }> = {};
const formatDateTime = (dateStr: string) => {
if (!dateStr) return '';
@@ -525,30 +525,43 @@ export const TourDetailPage = ({
};
let stt = 1;
let globalRowIndex = 0;
if (currentTour?.legs && currentTour.legs.length > 0) {
currentTour.legs.forEach((leg: any, legIdx: number) => {
const legName = leg.note || `Chặng ${legIdx + 1}`;
if (leg.locations && leg.locations.length > 0) {
leg.locations.forEach((loc: any) => {
const currentStt = stt++;
const arrivalStr = loc.arrivalTime ? `Đến: ${formatDateTime(loc.arrivalTime)}` : '';
const departureStr = loc.departureTime ? `Đi: ${formatDateTime(loc.departureTime)}` : '';
const timeText = [arrivalStr, departureStr].filter(Boolean).join('\n');
const locName = loc.name;
const addressStr = loc.address ? `\n📍 Địa chỉ: ${loc.address}` : '';
const locationText = `${locName}${addressStr}`;
const noteText = loc.notes || '';
const legLocations = (leg.locations || []).filter((loc: any) =>
loc && (loc.plannedStart || loc.plannedEnd || loc.name)
);
if (legLocations.length > 0) {
const startRow = globalRowIndex;
legRowRanges[leg.id] = { start: startRow, count: legLocations.length };
legLocations.forEach((loc: any) => {
const timeStr = loc.plannedStart
? formatDateTime(loc.plannedStart)
: loc.plannedEnd
? formatDateTime(loc.plannedEnd)
: '';
const locName = loc.name || '';
const addressStr = loc.address || '';
const coordStr = loc.latitude && loc.longitude
? `\n${loc.latitude}, ${loc.longitude}`
: '';
const locationText = [locName, addressStr, coordStr].filter(Boolean).join('\n');
const noteText = loc.note || '';
tableRows.push([
currentStt,
timeText,
stt++,
timeStr,
legName,
locationText,
noteText
]);
globalRowIndex++;
});
}
});
@@ -558,22 +571,67 @@ export const TourDetailPage = ({
tableRows.push(["-", "-", "-", "Chưa có chặng hoặc địa điểm nào trong hành trình.", "-"]);
}
// 4. Vẽ bảng dùng autoTable
autoTable(doc, {
head: [tableColumn],
headStyles: {
fillColor: [37, 99, 235],
textColor: [255, 255, 255],
fontStyle: 'bold',
halign: 'center',
valign: 'middle'
},
body: tableRows,
startY: startY,
theme: 'grid',
headStyles: { fillColor: [37, 99, 235], textColor: [255, 255, 255], fontStyle: 'normal' },
styles: { font: "Roboto", fontSize: 9, cellPadding: 3, overflow: 'linebreak' },
columnStyles: {
0: { cellWidth: 12, halign: 'center' }, // STT
1: { cellWidth: 50 }, // Ngày giờ
2: { cellWidth: 45 }, // Chặng
3: { cellWidth: 90 }, // Địa điểm
4: { cellWidth: 70 } // Ghi chú
styles: {
font: "Roboto",
fontSize: 9,
cellPadding: 3,
overflow: 'linebreak',
valign: 'top'
},
margin: { left: 15, right: 15 }
columnStyles: {
0: { cellWidth: 12, halign: 'center', valign: 'middle' },
1: { cellWidth: 50, halign: 'center', valign: 'middle' },
2: { cellWidth: 45, halign: 'center', valign: 'middle' },
3: { cellWidth: 90, valign: 'top' },
4: { cellWidth: 70, valign: 'top' }
},
margin: { left: 15, right: 15 },
didParseCell: (cell: any) => {
if (cell.section === 'body') {
if (cell.column.index === 2) {
let assigned = false;
for (const leg of currentTour?.legs || []) {
const range = legRowRanges[leg.id];
if (range && cell.row.index >= range.start && cell.row.index < range.start + range.count) {
if (cell.row.index === range.start) {
cell.rowSpan = range.count;
} else {
cell.rowSpan = 0;
}
assigned = true;
break;
}
}
if (!assigned) {
cell.rowSpan = 1;
}
}
if (cell.column.index === 3) {
const rowData = tableRows[cell.row.index];
if (rowData && rowData[3]) {
const coordMatch = rowData[3].match(/([\d.]+),\s*([\d.]+)/);
if (coordMatch) {
const lat = coordMatch[1];
const lng = coordMatch[2];
cell.cell.link = `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}`;
}
}
}
}
}
});
try {
File diff suppressed because one or more lines are too long