昨天完成 LocalStorage 後,AI 產生的學習卡已經可以保存在瀏覽器中。即使重新整理網頁,先前產生與人工編輯過的主題也不會消失。
不過,目前的學習卡仍然只能在網站上使用。
如果家長想把教材帶到沒有網路的地方,或老師想在課堂上發給學生練習,就需要一個更適合實際使用的輸出方式。
因此,今天除了整理「從 Prompt 到畫面」的完整資料流程,也要替 Learning Card App 加入:
將目前選取的學習卡主題列印,或另存為 PDF。
這次不使用額外的 PDF 套件,而是運用瀏覽器原生的列印功能,搭配專用的列印元件與 CSS,將畫面中的學習卡轉換成適合 A4 紙張的版面。
經過前幾天的調整後,App 的資料流程已經逐漸完整。
使用者先在產生表單中設定:
接著將這些資料送給 Gemini API。
Gemini 回傳結構化的 Learning Card 資料後,App 會把卡片整理成一個 Generated Theme,加入統一的 AppTheme[] 狀態中。
使用者輸入產生條件
↓
建立 Gemini Prompt
↓
Gemini Structured Output
↓
轉換成 LearningCard[]
↓
建立 Generated AppTheme
↓
加入 AppTheme[]
↓
ThemeSelector 顯示新主題
↓
React 畫面呈現學習卡
↓
LocalStorage 自動保存
↓
列印或另存為 PDF
目前不論是內建主題或 AI 產生主題,都使用相同的 Theme 與 Card 結構。
因此,新增列印功能時,不需要重新整理 Gemini 回傳資料,也不需要建立另一套卡片格式。只要把目前選取的 Theme 傳給列印元件,就能產生紙本版面。
今天的列印功能需要符合以下條件:
這次仍然請 Gemini 採用最小必要修改,不更動現有資料結構,也不另外安裝 PDF 套件。
請檢查目前 React + TypeScript Learning Card App,
新增「列印/另存為 PDF」功能。
請沿用現有的 AppTheme[]、selectedThemeId 與 LearningCard 資料結構,
以最小必要修改完成需求。
【功能目標】
讓家長或老師可以將目前選取的學習卡主題列印,
或使用瀏覽器列印功能另存為 PDF,
方便在離線環境或實體課堂中使用。
【功能需求】
1. 在目前卡片上方的操作區加入「列印/PDF」按鈕。
2. 點擊按鈕時使用瀏覽器原生 window.print()。
3. 列印內容只包含目前選取的 Theme:
- Theme emoji
- Theme name
- source
- generationParams.ageGroup
- generationParams.topic
- 卡片總數
- 該 Theme 的全部 Learning Cards
4. 每張列印卡片需顯示:
- emoji
- title
- pinyin(如果存在)
- content
- question
- answer
- parentTip(如果存在)
- soundName(如果存在)
5. 請新增獨立元件:
src/components/PrintableLearningCards.tsx
6. PrintableLearningCards 只負責接收目前 Theme,
並產生列印用版面,
不直接修改 themes、selectedThemeId 或其他 App State。
7. 使用 CSS @media print:
- A4 直向
- 適當頁面邊界
- 每頁雙欄排列
- 隱藏原本 App 操作介面
- 只顯示列印內容
- 避免單張卡片跨頁切斷
- 保持中文字體與內容可讀性
8. 題目與答案在紙本中必須直接顯示,
不使用互動式「查看答案」按鈕。
9. 若 Theme 有 5 張以上卡片,
應允許瀏覽器自然分頁,
不可因固定高度導致內容被裁切。
10. 列印前停止語音播放與自動朗讀,
避免列印期間仍然切換卡片。
【限制】
- 不安裝 jsPDF 或其他 PDF 套件
- 不修改 Gemini API
- 不修改 Structured Output Schema
- 不修改 LearningCard 與 AppTheme 資料結構
- 不影響 Human Edit
- 不影響 Generate 與 Regenerate
- 不影響 ThemeSelector
- 不影響 Explore 與 Challenge Mode
- 不修改 LocalStorage Key
- 保留現有畫面與樣式
- 以最小必要修改完成
【完成後請回報】
1. 修改及新增的檔案
2. 列印資料如何取得
3. 如何只列印目前選取的 Theme
4. 如何隱藏原本 App UI
5. 如何避免卡片跨頁切斷
6. TypeScript 與 Build 結果
Gemini 完成後,主要修改了三個檔案。

新增:
src/components/PrintableLearningCards.tsx
這個元件專門負責產生紙本版面。
它接收目前選取的 Theme,並將主題資訊與所有卡片排列成適合列印的格式。
概念上可以整理為:
<PrintableLearningCards theme={currentTheme} />
這個元件不會修改 themes,也不處理主題切換或卡片編輯。
它只負責:
接收 Theme
↓
讀取 Theme 資訊
↓
讀取 Theme.cards
↓
產生列印版卡片
將列印內容抽成獨立元件,可以避免把紙本排版全部塞進 LearningCardApp.tsx,也讓互動畫面與列印畫面保持各自清楚的責任。
在原本的卡片操作區加入「列印/PDF」按鈕。
【圖 1:App 加入「列印/PDF」按鈕】

使用者點擊按鈕後,App 會:
window.print()。PrintableLearningCards 產生的內容。列印資料直接來自目前選取的 Theme,不需要再次呼叫 Gemini。
selectedThemeId
↓
從 AppTheme[] 找到 currentTheme
↓
傳入 PrintableLearningCards
↓
輸出該主題全部卡片
因此,畫面上目前選擇「太陽系」,列印內容就會是太陽系的全部卡片;切換到其他主題後再次列印,也會自動取得新主題的內容。
這次列印功能的關鍵之一,是使用 @media print 建立列印專用樣式。
一般瀏覽畫面中,列印內容會先被隱藏:
.print-only {
display: none;
}
進入列印模式後,再隱藏原本 App 畫面並顯示紙本版面:
@media print {
.no-print {
display: none !important;
}
.print-only {
display: block !important;
}
}
同時設定 A4 直向與紙張邊界:
@page {
size: A4 portrait;
margin: 12mm;
}
每張卡片則加入避免跨頁切斷的設定:
.print-card-item {
break-inside: avoid;
page-break-inside: avoid;
}
如此一來,瀏覽器在需要換頁時,會盡量把一張完整卡片移到下一個位置,而不是將卡片上半部留在前一頁、下半部放到下一頁。
這次沒有直接列印目前畫面上的大卡片。
原因是網站中的學習卡屬於互動介面,包含:
如果直接把整個 App 畫面送去列印,不只會出現許多紙本不需要的按鈕,也只會看到目前正在瀏覽的單張卡片。
因此,這次另外建立紙本專用元件:
互動版 Learning Card
適合在螢幕上操作
一次瀏覽一張卡片
保留按鈕與互動功能
↓ 使用同一份 Theme 資料
列印版 Learning Card
適合 A4 紙張與 PDF
一次排列全部卡片
直接顯示題目與答案
雖然兩種畫面的用途不同,但資料來源相同,所以不需要複製或重新維護卡片內容。
在 Google AI Studio 中,App 是顯示在嵌入式預覽環境裡。這類環境可能會限制網站直接開啟瀏覽器列印視窗。
因此,正式測試時,我先用新分頁開啟 App 的網站連結,再點擊「列印/PDF」。
獨立分頁中的列印功能可以正常執行。
如果要輸出 PDF,只要在列印視窗的「目的地」選擇:
另存為 PDF
接著按下儲存即可,不需要安裝額外套件。
這次使用 AI 產生的「太陽系」主題進行測試。
測試資料包含:
列印預覽結果中,可以看到:
【圖 2:太陽系主題的 A4 列印預覽】

實際結果符合預期:
| 測試項目 | 結果 |
|---|---|
| 列印按鈕正常顯示 | 成功 |
| 獨立分頁可開啟列印視窗 | 成功 |
| 只顯示學習卡列印內容 | 成功 |
| 沒有列印 App 操作按鈕 | 成功 |
| 正確取得目前選取的主題 | 成功 |
| 五張卡片全部輸出 | 成功 |
| 題目與答案直接顯示 | 成功 |
| A4 直向版面 | 成功 |
| 雙欄卡片排列 | 成功 |
| 卡片沒有跨頁切斷 | 成功 |
| 中文內容正常顯示 | 成功 |
| 可選擇另存為 PDF | 成功 |
前端產生 PDF 常見的方式包括:
不過,這些方法可能增加:
目前的需求只是將網站上的學習卡輸出成紙本或 PDF,因此瀏覽器原生列印功能已經足夠。
它的優點是:
對目前這個專案而言,原生列印是較符合需求的方案。
完成 Day 28 後,Learning Card App 已經形成一條更完整的資料使用流程:
Generate Form
↓
Gemini Prompt
↓
Structured Output
↓
LearningCard[]
↓
AppTheme[]
├── ThemeSelector
├── Explore Mode
├── Challenge Mode
├── Human Edit
├── Regenerate
├── LocalStorage
└── PrintableLearningCards
同一份 AppTheme[] 資料現在可以同時支援:
這也是統一資料結構帶來的好處。
如果每個功能都使用不同的資料格式,加入列印功能時就必須再次轉換資料;現在只要把目前 Theme 傳給列印元件,就能完成輸出。
程式本身完成後,在 Google AI Studio 的預覽區測試時,列印按鈕沒有直接開啟系統列印視窗。
後來將網站連結改用獨立分頁開啟,再測試同一個按鈕,列印功能就能正常運作。
這表示問題不是卡片資料或列印 CSS,而是開發平台的嵌入式預覽環境對瀏覽器功能有所限制。
這次也讓我更清楚區分:
功能實作是否正確
以及:
功能是否受到測試環境限制
開發工具中的 Preview 適合快速確認畫面,但遇到列印、下載、彈窗或權限相關功能時,仍然應該在獨立分頁或實際部署環境中再次驗證。
今天最大的收穫不只是增加一個列印按鈕,而是把前幾天完成的資料流程真正串接到新的使用情境。
同一份卡片資料可以同時產生互動畫面與紙本畫面,不需要把目前看到的 UI 原封不動拿去列印。
PrintableLearningCards.tsx 只負責紙本輸出,讓主要 App 不需要承擔所有版面邏輯。
透過 @media print,同一個網站也能針對 A4 紙張建立不同的呈現方式。
不是每個功能都需要安裝新的套件。只要需求清楚,window.print() 搭配列印 CSS 就能完成實用的 PDF 輸出。
嵌入式預覽與獨立分頁的瀏覽器權限不同。遇到功能沒有反應時,除了檢查程式,也要確認是否受到執行環境限制。
今天完成了:
現在,AI 產生的內容已經不只停留在網站畫面中。
家長可以將卡片保存成 PDF,老師也能把教材列印後帶進實體課堂,讓同一份 AI 學習內容同時支援線上與離線使用。
明天將進入整合測試:
Day 29|不只會動,還要穩定:完成智慧學習卡整合測試
接下來會把 Generate、Regenerate、Human Edit、ThemeSelector、LocalStorage、列印與各種操作流程串在一起測試,確認這些功能不只各自成功,也能在完整使用流程中穩定合作。