可列印 A4 報表的 CSS 全攻略:頁碼、跨頁表頭、列印隔離

問題現場:想做一份「能印」的網頁報表 需求很常見:後台要一頁會計報表(明細列表 + 合計列),會計同事要能直接從瀏覽器按列印、出 A4。沒人想為這種小需求引入 PDF library 或產 PDF 的後端服務 —— 瀏覽器列印就好。 寫下去才發現坑滿地:頁碼從哪來?跨頁時表頭會不會消失?頁面外面那層 admin sidebar、左側選單會不會也跟著被印出來?螢幕上預覽是黑背景白字,印到紙上會變什麼樣? 這些問題的答案幾乎都不在「常見 CSS 教學」裡,而是藏在一份很少人讀的 W3C 規範:CSS Paged Media Module。本文用一份實際做出來的會計報表為例,把這些技巧串起來。 為什麼不選 PDF library? 開發者第一個反射動作往往是「裝個 jsPDF / pdfmake 直接產 PDF」。在跳進那條路之前,先看清各方案的代價: 方案 中文字型 排版精度 Bundle 適用情境 CSS @page(本文) ✅ 系統字型 高(向量字) 0 KB 後台報表、現場列印、人工流程 html2canvas + jsPDF ⚠️ 需嵌字型 中(光柵化) ~300 KB 簡單表單下載 pdfmake / React-PDF(向量 PDF) ⚠️ 需嵌字型 高 ~500 KB+ 給外部讀者的下載檔 後端 Puppeteer / WeasyPrint ✅ 系統字型 最高 0(前端) 批次寄信、加密簽章 前端 PDF library 最大的痛點是中文字型。系統列印免費借用作業系統字型;jsPDF 預設只內建 Helvetica,要中文不亂碼就得把整套思源黑體(壓縮後仍有 5-10 MB)打包進 bundle,否則出現方塊或缺字。 ...

May 6, 2026 ·  · 3 分鐘 · Peter

後端修好了,前端卻沒跟上:Vue 網頁版的 JWT Refresh 機制從零補上

結帳頁突然報錯:Missing or invalid credentials 使用者在 App 內開啟網頁版結帳頁,輸入折扣碼後收到一個刺眼的錯誤訊息:「Missing or invalid credentials」。 不是每個人都遇到,也不是每次都發生。但只要在頁面上停留超過一段時間再操作,就一定會觸發。 聽起來很熟悉?沒錯,這跟前一篇文章的問題同源——JWT 15 分鐘過期。差別在於,上次是後端的 refresh API 被權限系統擋掉,這次是前端根本沒有呼叫 refresh 的能力。 架構背景:App 開網頁的 SSO 流程 我們的 Flutter App 會透過 in-app browser 開啟 Vue 網頁。token 的傳遞方式很直接: https://web.example.com/checkout/xxx?token=eyJhbG... Vue 從 URL query param 取得 JWT,存進 sessionStorage,之後的 API 呼叫都帶這個 token。 問題在於:只傳了 JWT,沒有傳 refresh token。 JWT 有 15 分鐘的壽命。使用者開啟頁面、慢慢填寫發票資料、比較方案——15 分鐘一晃就過了。接下來任何 API 呼叫都會收到 401,但 Vue 手上沒有 refresh token,無法換新的 JWT。只能眼睜睜看著使用者被擋在門外。 為什麼 Flutter 不需要擔心,但 Vue 必須處理 Flutter App 本身有完整的 token refresh 機制:interceptor 攔截 401、用 refresh token 換新 JWT、重送失敗的 request。整個流程無縫,使用者無感。 ...

February 24, 2026 ·  · 3 分鐘 · Peter

解決 CKEditor 圖片水平排版在前端顯示為垂直排列的問題

前言:編輯器與前端的排版不一致之謎 在開發多平台醫療健康應用時,我們採用了現代化的技術棧: 後端 CMS:Strapi v5.15.1(Headless CMS) 前端框架:Vue.js 3 富文本編輯器:CKEditor 5 資料傳輸:GraphQL API 這個組合在大多數情況下運作良好,編輯者可以在 Strapi 後台使用 CKEditor 輕鬆編輯富文本內容,前端 Vue.js 應用透過 GraphQL 獲取並渲染這些內容。 然而,我們遇到了一個令人困惑的問題: 在 Strapi 後台使用 CKEditor 精心排版的水平並排圖片,到了前端網頁卻變成了垂直排列。 這個問題不僅影響了內容的視覺呈現,也破壞了編輯者的排版意圖。更重要的是,這讓非技術背景的內容編輯者感到困惑:「為什麼我在後台看到的排版,到了網站上就變了?」 這篇文章將深入探討這個問題的根本原因,並提供系統性的解決方案。 問題現象與環境說明 內容流程架構 我們的內容從編輯到展示的完整流程如下: 問題的具體表現 預期行為: 在 Strapi CKEditor 後台,編輯者將兩張圖片設定為水平並排: <!-- CKEditor 生成的 HTML 結構 --> <figure class="image" style="float:left"> <img src="/uploads/image1.webp" alt="圖片1"> <figcaption>圖片1說明</figcaption> </figure> <figure class="image" style="float:left"> <img src="/uploads/image2.webp" alt="圖片2"> <figcaption>圖片2說明</figcaption> </figure> 實際現象: 前端 Vue.js 渲染後,圖片變成垂直排列: 兩張圖片沒有並排顯示,而是一張接著一張垂直堆疊。 技術環境詳情 Strapi CKEditor 配置(config/schema.json): { "kind": "collectionType", "collectionName": "articles", "info": { "singularName": "article", "pluralName": "articles", "displayName": "文章" }, "attributes": { "title": { "type": "string" }, "content": { "type": "customField", "options": { "preset": "defaultHtml" }, "customField": "plugin::ckeditor5.CKEditor" } } } Vue.js 前端渲染組件(ServiceDetailView.vue): ...

August 1, 2025 ·  · 9 分鐘 · Peter

Vue.js SPA 社交分享完整指南:解決 Facebook/LINE OpenGraph 爬蟲問題

引言:當精美的網站變成分享時的「無名氏」 在現代 Web 開發中,社交媒體分享功能是不可或缺的一部分。當用戶在 Facebook、LINE 或其他社交平台分享你的網站連結時,你希望顯示的是精美的預覽卡片,而不是空白或錯誤的資訊。 然而,對於使用 Vue.js、React、Angular 等前端框架開發的單頁應用程式(SPA),這個看似簡單的需求卻隱藏著技術挑戰。 本文將完整記錄從問題發現、原因分析、到解決方案實作的全過程。 問題發現:為什麼分享連結總是顯示預設值? 場景描述 我的平台 www.abc.com 是一個基於 Vue.js 3 + Strapi CMS 的網站。某天,我發現一個嚴重問題: 當用戶分享服務頁面(如 https://www.abc.com/service-us/6)到 Facebook 或 LINE 時,顯示的預覽資訊總是預設值,而非該服務的實際標題和描述。 實際情況對比: 情境 期望結果 實際結果 分享服務頁面 顯示「專業網站開發服務」 顯示「ABCDEFG(預設標題)」 分享部落格文章 顯示文章標題與摘要 顯示網站預設描述 分享產品頁面 顯示產品圖片與名稱 顯示網站 Logo 診斷工具測試 使用 Facebook Open Graph Debugger 測試後發現: 爬蟲抓取到的 HTML: <!DOCTYPE html> <html lang="zh-TW"> <head> <meta charset="UTF-8"> <title>ABCDEFG</title> <meta property="og:title" content="ABCDEFG"> <meta property="og:description" content="預設網站描述"> <!-- 沒有任何動態內容! --> </head> <body> <div id="app"></div> <script src="/assets/index.js"></script> </body> </html> ⚠️ 關鍵發現: 爬蟲只看到靜態的 HTML 模板,完全沒有 JavaScript 執行後動態生成的 meta 標籤。 ...

July 31, 2025 ·  · 11 分鐘 · Peter

前端登入失敗的真兇:深入理解 CORS 問題與實戰解法

前言:那個令人抓狂的錯誤訊息 在開發前後端分離的 Web 應用時,幾乎每位工程師都曾遇過這個令人頭痛的錯誤: Access to fetch at 'http://localhost:1337/api/auth/local' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. 這個錯誤通常發生在最關鍵的時刻: 前端登入功能即將完成,卻無法呼叫後端 API 串接第三方服務時,資料無法正常取得 部署到測試環境後,原本運作正常的功能突然失效 CORS(Cross-Origin Resource Sharing,跨來源資源共用)是現代 Web 開發中的核心安全機制,但也是許多開發者的痛點。這篇文章將從基礎原理到實戰應用,帶你完整理解 CORS 的運作方式,並提供實際可用的解決方案。 為什麼需要 CORS?從同源政策說起 同源政策的誕生 在理解 CORS 之前,我們需要先認識「同源政策」(Same-Origin Policy, SOP)。這是瀏覽器最基本的安全機制,在 1995 年 Netscape Navigator 2.0 引入 JavaScript 時就已經存在。 同源政策的目的:防止惡意網站讀取另一個網站的敏感資料。 想像一個情境:你登入了網路銀行(https://bank.com),此時你的瀏覽器保存了銀行的登入 Cookie。如果沒有同源政策,當你不小心訪問了一個惡意網站(https://evil.com),該網站的 JavaScript 就能透過你的瀏覽器向 https://bank.com 發送請求,並讀取你的帳戶資料。 同源政策阻止了這種攻擊:https://evil.com 的 JavaScript 無法讀取 https://bank.com 的回應內容。 什麼是「同源」? 兩個 URL 被視為同源,必須滿足以下三個條件: ...

July 1, 2025 ·  · 9 分鐘 · Peter

解決 macOS 上 Rollup rollup.darwin-arm64.node 模組錯誤與 Gatekeeper 阻擋問題

前言 使用 Apple Silicon Mac 開發前端專案時,你有沒有遇過這種情況:npm install 明明成功了,但執行 npm run dev 時 Vite 卻突然翻臉不認人,噴出一堆關於 rollup.darwin-arm64.node 的錯誤? 更慘的是,macOS 還會彈出一個視窗說「無法打開 rollup.darwin-arm64.node,因為無法驗證開發者」——彷彿你的電腦在暗示你下載了什麼奇怪的東西。 別擔心,這不是你的問題(好吧,也不完全是 Apple 的問題),而是 npm + Gatekeeper 的經典組合技。本文將帶你徹底理解並解決這個惱人的問題。 問題現象 當你在 Apple Silicon Mac 上執行 Vite/Rollup 專案時,可能會遇到以下錯誤: 錯誤 1:模組找不到 Error: Cannot find module '@rollup/rollup-darwin-arm64' Require stack: - /project/node_modules/rollup/dist/native.js 錯誤 2:系統策略拒絕載入 Error: dlopen(/project/node_modules/@rollup/rollup-darwin-arm64/rollup.darwin-arm64.node, 0x0001): code signature in (...) not valid for use in process: library load disallowed by system policy 錯誤 3:Gatekeeper 彈窗警告 macOS 直接彈出視窗:「無法打開 rollup.darwin-arm64.node,因為無法驗證開發者」 ...

January 1, 2024 ·  · 2 分鐘 · Peter