列印與匯出
問題
大多數團隊只設計淺色與深色兩種主題。後台系統還有第三種:印出來的那一份。
而列印是後台系統真正的交付物之一——報表要印出來給主管簽名、 要存成 PDF 附在流程裡、要寄給不用系統的人。
沒有處理列印樣式的結果:側邊欄佔掉半頁、狀態徽章變成白色方塊、 表格被切成兩半而且第二頁沒有表頭。
做法:列印規則
1. 底色是內容,不是裝飾
@media print {
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
}
後台介面的徽章、提示框、佔位塊全靠底色辨義。瀏覽器預設會剝掉背景色 (除非使用者勾「列印背景圖形」,而沒有人會記得去勾),剝掉之後整塊語意就消失了。
補一層保底:佔位塊之類純靠底色的元素,在列印時補上淡邊框—— 有些印表機驅動仍會剝背景,至少留下形狀。
2. 控制項一律隱藏
搜尋框、篩選鈕、分頁器、匯出鈕、側邊欄、導覽——印出來要是一份乾淨文件。
元件內建 print-hidden class;容器層另外隱藏站台外殼。
3. 不跨頁截斷
@media print {
thead { display: table-header-group; } /* 每頁重複表頭 */
tfoot { display: table-footer-group; }
tr, img, svg, figure, .print-block { break-inside: avoid; }
h1, h2, h3, h4 { break-after: avoid; } /* 標題不孤懸頁底 */
p { orphans: 3; widows: 3; } /* 段落至少留 3 行 */
}
break-inside: avoid 是盡力而為——整塊超過一頁時瀏覽器仍會就地分頁。
所以大區塊要能自己再細分(例如逐步驟各自 avoid)。
4. 黏性元素要解除
@media print { .sticky-header { position: static !important; } }
黏性表頭在列印時會壓在第一頁的內容上。表頭重複交給 table-header-group 處理就好。
5. 動畫改印靜態版本
@media print {
.spotlight-ring {
animation: none !important;
box-shadow: 0 0 0 3px hsl(var(--primary)), 0 0 0 6px hsl(var(--primary) / 0.3) !important;
}
}
注意是換成靜態版本,不是直接拿掉。直接 animation: none 會讓聚光完全消失。
6. 頁面邊界與頁首頁尾
@media print {
@page { margin: 14mm 12mm 16mm; }
}
瀏覽器預設邊界各家不同,而且使用者可以在列印對話框改。 不自己定的話,同一份文件在不同人手上會印出不同的版面—— 需要簽名的文件被要求重印,有一半是因為「邊界不夠寫字」。
下緣要留得比上緣多,因為那裡要放頁尾。
頁首(文件名稱、期間、產生時間)與頁尾(頁碼、系統版本)只在列印時出現。
做法是先無條件隱藏,再在 @media print 內開啟:
/* 基礎規則:螢幕上一律不顯示 */
.print-header,
.print-footer { display: none; }
/* 列印時才打開 */
@media print {
.print-header,
.print-footer { display: block; }
}
基礎規則(隱藏)與 media 區塊(顯示)要分開寫, 而且隱藏那條要在前面。
不要只在 @media print 裡寫顯示規則、卻忘了寫螢幕上的隱藏規則。
結果是每一頁的最上面多一行版本戳記——而且因為它看起來像刻意設計的頁首,
可以存活很久都沒有人回報。
頁尾用固定定位貼在頁底,並且要寫得出「這張紙是哪一版印的」—— 放系統版本 + commit + 產生時間:
月度彙總表 v1.4.2 · a3f9c21 2026-07-29 14:20 產生
理由很具體:使用者回報「這份文件的數字怪怪的」時, 手上那張紙是唯一還能對到版本的線索。 沒有這一行,你只能問「你什麼時候印的」,而答案通常是「不記得了」。
版本字串要與畫面上其他地方同一個來源,做法見 版本策略。
7. 版面高度要回歸內容
@media print {
html, body, #root, .app-shell {
height: auto !important;
min-height: 0 !important;
}
}
應用外殼常寫 min-height: 100vh(讓短頁面的頁尾不要浮在中間)。
列印時 100vh 會被換算成一整頁的高度——
於是每一份文件最後都多印一張幾乎空白的紙。
這條之所以值得單獨列出,是因為症狀與原因看起來毫無關係: 使用者回報的是「最後多一頁空白」,而沒有人會聯想到那是首頁排版用的高度設定。
8. 印不出來的內容要誠實標示
有些區塊在紙上本質上不存在:嵌入的互動範例、影片、地圖、需要展開才有內容的元件。
錯誤做法有兩種,而且都很常見:印出一個空白框(讀者以為壞了), 或直接隱藏(讀者不知道自己少看了什麼)。
正確做法是換成替代說明框:
@media print {
.embed-frame { display: none !important; }
.embed-print-note { display: block !important; border: 1px dashed; }
}
替代框要說出那裡原本是什麼,以及去哪裡看得到。
不要印一個空的容器。讀者看到的是一塊莫名其妙的留白, 合理的推論是「這份文件印壞了」——然後他會再印一次。
9. 摺疊與輪播,列印時全部展開
會自動輪播的步驟示意、預設收合的明細、分頁的內容—— 螢幕上一次只顯示一份,紙上必須全部都在。
/* 螢幕:只顯示當前步 */
.step { display: none; }
.step.is-active { display: block; }
@media print {
.step { display: block !important; break-inside: avoid; }
.step .step-caption { display: block !important; } /* 各步字幕一起印 */
.carousel-controls,
.carousel-counter { display: none !important; } /* 控制項與「3 / 5」計數失義 */
}
三個細節:
- 全部步驟要在 DOM 裡,只用 CSS 控制顯隱。用條件渲染只掛當前步的話, 列印時拿不到其他步——這決定了元件一開始就要那樣寫。
- 各步的字幕要一起印出來。螢幕上字幕在控制列,一次只顯示當前步的; 紙上每一步都要有自己的說明,否則印出來是五張沒有註解的圖。
- 計數器(「3 / 5」)要隱藏。全部展開之後它是錯的。
把螢幕上的「展開全部」按鈕做成與列印共用同一套展開樣式。 一份規則兩個用途,而且使用者按下展開時看到的,就是他印出來會拿到的。
不要為列印另寫一套展開邏輯。兩套一定會分歧, 而分歧的那一天你不會發現——因為沒有人會把兩者並排比對。
10. 外連箭頭要拿掉
站台常在外連連結後面用 ::after 加一個箭頭圖示。列印時要拿掉:
@media print { a[target="_blank"]::after { content: ""; } }
紙上沒有「開新分頁」這件事,那個箭頭只是一個看起來像錯字的符號。
匯出(CSV)
| 規則 | 為什麼 |
|---|---|
| 加 UTF-8 BOM | 沒有 BOM,中文在 Excel 開起來就是亂碼 |
| 匯出=目前畫面(篩選+排序後) | 「我明明篩好了,匯出卻是全部」 |
| 跳脫引號、逗號、換行 | 品名裡有逗號是常態 |
檔名帶語意(orders-2024-02.csv) | 使用者的下載資料夾裡有 40 個 export.csv |
取捨
代價:每個新頁面都要想一次列印。
減災:把列印規則做進 token 層的 CSS(.print-hidden、.print-block、.print-only),
讓它預設就對,而不是每頁各寫一份 @media print。
跟隨主題 vs 強制白底
第 1 條(print-color-adjust: exact)確保底色會被印出來。
但還有一個上層決定沒有回答:深色模式的使用者按下列印,該印出深色的紙嗎?
| 做法 | 得到 | 失去 |
|---|---|---|
| 跟隨當下主題 | 所見即所得。使用者不會困惑「我看到的跟印出來的不一樣」 | 深色主題會吃掉大量碳粉/墨水,而且大面積深色底在多數印表機上會糊成一片 |
強制白底(@media print { body { background: #fff; } }) | 省墨、永遠可讀 | 深色使用者印出來是另一份文件;且若只改 body 而元件底色仍是深色的,會得到最糟的組合 |
沒有普世正解,但有一條不能兩者都做一半的紀律:
選一種,並且整層一致。要強制白底,就把表面 token
在列印時整組換成淺色;要跟隨主題,就讓 print-color-adjust: exact 涵蓋全部元素。
不要只把 body 改成白色而不動元件底色。
深色主題下印出來會是白紙上一堆深色方塊配深色文字——那是唯一一種真的讀不出來的結果。
無論選哪一種,靠底色辨義的元素都要補一層保底邊框:
@media print { .placeholder, .badge { border: 1px solid; } }
因為 print-color-adjust: exact 是請求不是保證——
部分印表機驅動仍會剝掉背景。有邊框的話,至少形狀與標籤留得下來。
反例
提供頁面層級的「🖨 列印此頁」按鈕,讓使用者知道這件事被支援。
不要另外做一個「列印版頁面」。兩份版面一定會不同步, 而且使用者會問「為什麼列印版少了那一欄」。