Skip to content

Commit 8db50ac

Browse files
committed
Make schedule PDF export A4 portrait
1 parent 7430cb1 commit 8db50ac

3 files changed

Lines changed: 326 additions & 4 deletions

File tree

assets/site.js

Lines changed: 208 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ const REVIEW_OFFSETS = REVIEW_INTERVALS.reduce((offsets, interval) => {
88
const PLAN_DAY_COUNT = CHAPTER_COUNT + REVIEW_OFFSETS.at(-1);
99
const ANNOTATABLE_SELECTOR = "h2, h3, h4, p, li, blockquote, td, th";
1010
const DISCUSSION_NEW_URL = "https://github.com/Lling0000/Vibe_coding_guide/discussions/new?category=q-a";
11+
const PDF_EXPORT_SCRIPTS = [
12+
"https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js",
13+
"https://cdn.jsdelivr.net/npm/jspdf@2.5.2/dist/jspdf.umd.min.js",
14+
];
15+
const PDF_A4_PORTRAIT = {
16+
widthMm: 210,
17+
heightMm: 297,
18+
widthPx: 794,
19+
heightPx: 1122,
20+
rowsPerPage: 18,
21+
};
22+
const scriptPromises = new Map();
1123

1224
const docs = {
1325
zh: {
@@ -313,6 +325,7 @@ const copy = {
313325
matrixKicker: "Schedule Table",
314326
matrixTitle: "间隔学习总表",
315327
exportMatrixPdf: "导出 PDF",
328+
exportMatrixPdfBusy: "生成 A4 PDF...",
316329
exportMatrixPdfTitle: "Vibe Coding Guide · 36 天学习计划",
317330
resetMatrix: "清空表格勾选",
318331
resetMatrixConfirm: "确定要清空总表里的所有勾选吗?",
@@ -424,6 +437,7 @@ const copy = {
424437
matrixKicker: "Schedule Table",
425438
matrixTitle: "Spaced Learning Table",
426439
exportMatrixPdf: "Export PDF",
440+
exportMatrixPdfBusy: "Building A4 PDF...",
427441
exportMatrixPdfTitle: "Vibe Coding Guide · 36-Day Learning Plan",
428442
resetMatrix: "Clear table checks",
429443
resetMatrixConfirm: "Clear all table checks?",
@@ -2075,7 +2089,141 @@ function resetMatrixProgress() {
20752089
renderSchedulePlan();
20762090
}
20772091

2078-
function exportSchedulePdf() {
2092+
function loadScriptOnce(src) {
2093+
if (scriptPromises.has(src)) return scriptPromises.get(src);
2094+
2095+
const promise = new Promise((resolve, reject) => {
2096+
const existing = document.querySelector(`script[src="${src}"]`);
2097+
if (existing?.dataset.loaded === "true") {
2098+
resolve();
2099+
return;
2100+
}
2101+
2102+
const script = existing || document.createElement("script");
2103+
script.src = src;
2104+
script.async = true;
2105+
script.crossOrigin = "anonymous";
2106+
script.addEventListener("load", () => {
2107+
script.dataset.loaded = "true";
2108+
resolve();
2109+
}, { once: true });
2110+
script.addEventListener("error", () => reject(new Error(`Unable to load ${src}`)), { once: true });
2111+
2112+
if (!existing) {
2113+
document.head.append(script);
2114+
}
2115+
});
2116+
2117+
scriptPromises.set(src, promise);
2118+
return promise;
2119+
}
2120+
2121+
async function loadPdfExportLibraries() {
2122+
if (window.html2canvas && window.jspdf?.jsPDF) return;
2123+
await Promise.all(PDF_EXPORT_SCRIPTS.map(loadScriptOnce));
2124+
2125+
if (!window.html2canvas || !window.jspdf?.jsPDF) {
2126+
throw new Error("PDF export libraries are unavailable.");
2127+
}
2128+
}
2129+
2130+
function schedulePdfFilename() {
2131+
const lang = state.lang === "zh" ? "zh" : "en";
2132+
return `vibe-coding-guide-schedule-a4-portrait-${lang}.pdf`;
2133+
}
2134+
2135+
function createSchedulePdfCell(item, text) {
2136+
const cell = document.createElement("td");
2137+
if (!item) return cell;
2138+
2139+
const chapter = document.createElement("span");
2140+
chapter.className = "pdf-export-chapter";
2141+
chapter.textContent = text.matrixItem(item.chapterIndex);
2142+
cell.append(chapter);
2143+
2144+
if (isChecked(item.id)) {
2145+
const check = document.createElement("span");
2146+
check.className = "pdf-export-check";
2147+
check.textContent = "✓";
2148+
cell.append(check);
2149+
}
2150+
2151+
return cell;
2152+
}
2153+
2154+
function createSchedulePdfTable(startDay, endDay) {
2155+
const text = copy[state.lang];
2156+
const columns = [
2157+
text.matrixDay,
2158+
text.matrixLearn,
2159+
...REVIEW_INTERVALS.map((_, index) => text.matrixReview(index + 1)),
2160+
];
2161+
2162+
const table = document.createElement("table");
2163+
table.className = "pdf-export-table";
2164+
2165+
const thead = document.createElement("thead");
2166+
const headRow = document.createElement("tr");
2167+
columns.forEach((label) => {
2168+
const th = document.createElement("th");
2169+
th.scope = "col";
2170+
th.textContent = label;
2171+
headRow.append(th);
2172+
});
2173+
thead.append(headRow);
2174+
2175+
const tbody = document.createElement("tbody");
2176+
for (let day = startDay; day <= endDay; day += 1) {
2177+
const row = document.createElement("tr");
2178+
const dayHeader = document.createElement("th");
2179+
dayHeader.scope = "row";
2180+
dayHeader.textContent = text.dayLabel(day);
2181+
row.append(dayHeader);
2182+
2183+
const itemsByColumn = new Map(getScheduleMatrixItems(day).map((item) => [item.column, item]));
2184+
for (let column = 0; column <= REVIEW_INTERVALS.length; column += 1) {
2185+
row.append(createSchedulePdfCell(itemsByColumn.get(column), text));
2186+
}
2187+
2188+
tbody.append(row);
2189+
}
2190+
2191+
table.append(thead, tbody);
2192+
return table;
2193+
}
2194+
2195+
function createSchedulePdfRoot() {
2196+
const text = copy[state.lang];
2197+
const pageCount = Math.ceil(PLAN_DAY_COUNT / PDF_A4_PORTRAIT.rowsPerPage);
2198+
const pageSizeLabel = state.lang === "zh" ? "A4 纵向" : "A4 portrait";
2199+
const root = document.createElement("div");
2200+
root.className = "pdf-export-root";
2201+
root.setAttribute("aria-hidden", "true");
2202+
2203+
for (let pageIndex = 0; pageIndex < pageCount; pageIndex += 1) {
2204+
const startDay = pageIndex * PDF_A4_PORTRAIT.rowsPerPage + 1;
2205+
const endDay = Math.min(PLAN_DAY_COUNT, startDay + PDF_A4_PORTRAIT.rowsPerPage - 1);
2206+
const page = document.createElement("section");
2207+
page.className = "pdf-export-page";
2208+
2209+
const header = document.createElement("header");
2210+
header.className = "pdf-export-header";
2211+
const kicker = document.createElement("p");
2212+
kicker.textContent = text.matrixKicker;
2213+
const title = document.createElement("h1");
2214+
title.textContent = text.exportMatrixPdfTitle;
2215+
const meta = document.createElement("span");
2216+
meta.textContent = `${text.dayLabel(startDay)} - ${text.dayLabel(endDay)} / ${pageSizeLabel}`;
2217+
header.append(kicker, title, meta);
2218+
2219+
page.append(header, createSchedulePdfTable(startDay, endDay));
2220+
root.append(page);
2221+
}
2222+
2223+
return root;
2224+
}
2225+
2226+
function printSchedulePdfFallback() {
20792227
const previousTitle = document.title;
20802228
document.body.dataset.printMode = "schedule";
20812229
document.title = copy[state.lang].exportMatrixPdfTitle;
@@ -2092,6 +2240,65 @@ function exportSchedulePdf() {
20922240
window.requestAnimationFrame(() => window.print());
20932241
}
20942242

2243+
async function exportSchedulePdf() {
2244+
const text = copy[state.lang];
2245+
const previousLabel = els.exportMatrixPdfLabel.textContent;
2246+
els.exportMatrixPdf.disabled = true;
2247+
els.exportMatrixPdfLabel.textContent = text.exportMatrixPdfBusy;
2248+
2249+
let root = null;
2250+
try {
2251+
await loadPdfExportLibraries();
2252+
root = createSchedulePdfRoot();
2253+
document.body.append(root);
2254+
await document.fonts?.ready;
2255+
2256+
const { jsPDF } = window.jspdf;
2257+
const pdf = new jsPDF({
2258+
orientation: "portrait",
2259+
unit: "mm",
2260+
format: "a4",
2261+
compress: true,
2262+
});
2263+
const pages = [...root.querySelectorAll(".pdf-export-page")];
2264+
2265+
for (const [index, page] of pages.entries()) {
2266+
const canvas = await window.html2canvas(page, {
2267+
backgroundColor: "#ffffff",
2268+
logging: false,
2269+
scale: 2,
2270+
useCORS: true,
2271+
windowWidth: PDF_A4_PORTRAIT.widthPx,
2272+
windowHeight: PDF_A4_PORTRAIT.heightPx,
2273+
});
2274+
2275+
if (index > 0) {
2276+
pdf.addPage("a4", "portrait");
2277+
}
2278+
2279+
pdf.addImage(
2280+
canvas.toDataURL("image/png"),
2281+
"PNG",
2282+
0,
2283+
0,
2284+
PDF_A4_PORTRAIT.widthMm,
2285+
PDF_A4_PORTRAIT.heightMm,
2286+
undefined,
2287+
"FAST",
2288+
);
2289+
}
2290+
2291+
pdf.save(schedulePdfFilename());
2292+
} catch (error) {
2293+
console.warn("A4 PDF export failed, falling back to browser print.", error);
2294+
printSchedulePdfFallback();
2295+
} finally {
2296+
root?.remove();
2297+
els.exportMatrixPdf.disabled = false;
2298+
els.exportMatrixPdfLabel.textContent = previousLabel || text.exportMatrixPdf;
2299+
}
2300+
}
2301+
20952302
async function loadGuide(lang, shouldUpdateUrl = true) {
20962303
state.lang = lang;
20972304
state.headings = [];

assets/styles.css

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1726,8 +1726,123 @@ button:disabled {
17261726
}
17271727
}
17281728

1729+
.pdf-export-root {
1730+
position: fixed;
1731+
top: 0;
1732+
left: -200vw;
1733+
z-index: -1;
1734+
display: grid;
1735+
gap: 16px;
1736+
overflow: hidden;
1737+
background: #fff;
1738+
color: #111;
1739+
pointer-events: none;
1740+
}
1741+
1742+
.pdf-export-page {
1743+
display: grid;
1744+
grid-template-rows: auto 1fr;
1745+
width: 794px;
1746+
height: 1122px;
1747+
box-sizing: border-box;
1748+
padding: 34px;
1749+
background: #fff;
1750+
color: #111;
1751+
font-family:
1752+
Inter,
1753+
"PingFang SC",
1754+
"Microsoft YaHei",
1755+
Arial,
1756+
sans-serif;
1757+
}
1758+
1759+
.pdf-export-header {
1760+
display: grid;
1761+
grid-template-columns: minmax(0, 1fr) auto;
1762+
gap: 3px 12px;
1763+
align-items: end;
1764+
margin-bottom: 14px;
1765+
}
1766+
1767+
.pdf-export-header p,
1768+
.pdf-export-header h1 {
1769+
margin: 0;
1770+
}
1771+
1772+
.pdf-export-header p {
1773+
grid-column: 1 / -1;
1774+
color: #0e6d63;
1775+
font-size: 13px;
1776+
font-weight: 900;
1777+
letter-spacing: 0.06em;
1778+
text-transform: uppercase;
1779+
}
1780+
1781+
.pdf-export-header h1 {
1782+
font-size: 24px;
1783+
line-height: 1.1;
1784+
}
1785+
1786+
.pdf-export-header span {
1787+
color: #4d5b57;
1788+
font-size: 13px;
1789+
font-weight: 800;
1790+
}
1791+
1792+
.pdf-export-table {
1793+
width: 100%;
1794+
height: 100%;
1795+
border-collapse: collapse;
1796+
table-layout: fixed;
1797+
background: #fff;
1798+
}
1799+
1800+
.pdf-export-table th,
1801+
.pdf-export-table td {
1802+
overflow: hidden;
1803+
padding: 5px;
1804+
border: 1.6px solid #111;
1805+
color: #111;
1806+
font-size: 13px;
1807+
line-height: 1.16;
1808+
text-align: left;
1809+
vertical-align: middle;
1810+
white-space: nowrap;
1811+
}
1812+
1813+
.pdf-export-table thead th {
1814+
height: 32px;
1815+
background: #eef5f2;
1816+
font-size: 13px;
1817+
font-weight: 900;
1818+
text-align: center;
1819+
}
1820+
1821+
.pdf-export-table tbody th {
1822+
width: 68px;
1823+
font-weight: 900;
1824+
}
1825+
1826+
.pdf-export-chapter {
1827+
font-weight: 850;
1828+
}
1829+
1830+
.pdf-export-check {
1831+
display: inline-grid;
1832+
width: 16px;
1833+
height: 16px;
1834+
margin-left: 4px;
1835+
place-items: center;
1836+
border-radius: 4px;
1837+
background: #16a34a;
1838+
color: #fff;
1839+
font-size: 12px;
1840+
font-weight: 950;
1841+
line-height: 1;
1842+
}
1843+
17291844
@page {
1730-
size: A4 landscape;
1845+
size: A4 portrait;
17311846
margin: 10mm;
17321847
}
17331848

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919
<meta name="twitter:card" content="summary" />
2020
<link rel="canonical" href="https://lling0000.github.io/Vibe_coding_guide/" />
2121
<title>Vibe Coding Guide | AI Coding 工程工作流手册</title>
22-
<link rel="stylesheet" href="./assets/styles.css?v=20260617-reader-bottom-nav" />
22+
<link rel="stylesheet" href="./assets/styles.css?v=20260617-a4-pdf-export" />
2323
<script defer src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
2424
<script defer src="https://cdn.jsdelivr.net/npm/marked@15.0.12/marked.min.js"></script>
2525
<script defer src="https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.min.js"></script>
2626
<script defer src="https://cdn.jsdelivr.net/npm/lucide@0.475.0/dist/umd/lucide.min.js"></script>
27-
<script defer src="./assets/site.js?v=20260617-reader-bottom-nav"></script>
27+
<script defer src="./assets/site.js?v=20260617-a4-pdf-export"></script>
2828
</head>
2929
<body data-mode="planner">
3030
<div class="site-progress" aria-hidden="true">

0 commit comments

Comments
 (0)