2026年1月13日 星期二

連接並操作 Firestore 的 4 個標準步驟

 

第一步:在 Firebase 控制台取得「金鑰」

在你可以寫程式之前,必須先告訴程式你要連接哪一個專案:

  1. 前往 Firebase Console。

  2. 點擊「專案設定」(齒輪圖示)。

  3. 在「您的應用程式」區塊,點擊 </> (Web) 圖示並註冊一個名稱。

  4. 你會看到一段 firebaseConfig 程式碼,長得像這樣:

    JavaScript
    const firebaseConfig = {
      apiKey: "AIzaSy...",
      authDomain: "你的專案ID.firebaseapp.com",
      projectId: "你的專案ID",
      // ... 其他參數
    };

    請複製這段內容,等一下會用到。


第二步:安裝或引入 SDK

如果你是在本地開發(使用 VS Code),通常會用 npm 安裝:

Bash
npm install firebase

如果你只是想先開一個簡單的 HTML 檔案試試看,可以在 <script> 標籤中引入(見下方的完整範例)。


第三步:撰寫連接程式碼 (JavaScript)

這是最核心的部分。我們將程式碼拆解為:初始化、寫入資料、讀取資料。

JavaScript
// 1. 引入必要的 Firebase 功能
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js";
import { getFirestore, collection, addDoc, getDocs } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js";

// 2. 貼上你在第一步複製的設定值
const firebaseConfig = {
  apiKey: "你的API金鑰",
  authDomain: "...",
  projectId: "...",
  // ...
};

// 3. 初始化 Firebase 與 Firestore
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

// --- 操作範例 ---

// 【寫入資料】:在 "users" 抽屜新增一個人
async function addUser() {
  try {
    const docRef = await addDoc(collection(db, "users"), {
      name: "王小明",
      age: 25,
      city: "台北"
    });
    console.log("資料寫入成功,ID 為: ", docRef.id);
  } catch (e) {
    console.error("錯誤發生: ", e);
  }
}

// 【讀取資料】:把 "users" 抽屜裡的人全部列出來
async function getUsers() {
  const querySnapshot = await getDocs(collection(db, "users"));
  querySnapshot.forEach((doc) => {
    console.log(`${doc.id} => `, doc.data());
  });
}

第四步:CRUD 常用語法速查表

當你連接成功後,會頻繁用到以下指令:

功能語法 (簡寫)說明
新增 (自動 ID)addDoc(collection(db, "名稱"), {資料})讓系統幫你決定文件 ID。
指定 ID 新增/覆蓋setDoc(doc(db, "名稱", "ID"), {資料})你自己決定 ID (例如用身分證字號)。
讀取單一文件getDoc(doc(db, "名稱", "ID"))只拿特定一張紙的內容。
更新部分內容updateDoc(doc(db, "名稱", "ID"), {年齡: 26})只改其中一個欄位,其餘不變。
刪除文件deleteDoc(doc(db, "名稱", "ID"))把那份文件刪掉。

重要安全提醒:解決「權限拒絕」問題

剛開始寫程式時,你可能會遇到 FirebaseError: Missing or insufficient permissions。這是因為你的資料庫目前處於保護模式。

測試時的解決方法:

  1. 回到 Firebase Console -> Firestore Database。

  2. 點擊上方標籤 "Rules" (規則)。

  3. 把內容暫時修改成允許任何人讀寫(注意:僅限練習,正式上線不可這樣做):

    JavaScript
    service cloud.firestore {
      match /databases/{database}/documents {
        match /{document=**} {
          allow read, write: if true; 
        }
      }
    }
  4. 點擊「發佈」。

完整email認證範例.
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Firebase Email 登入範例</title>
    <style>
        body { font-family: sans-serif; padding: 20px; line-height: 1.6; background-color: #f9f9f9; }
        .card { border: 1px solid #ccc; padding: 20px; border-radius: 8px; max-width: 400px; background: white; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
        .status { color: blue; font-weight: bold; margin-bottom: 10px; }
        input { display: block; margin: 10px 0; padding: 10px; width: 100%; box-sizing: border-box; border: 1px solid #ddd; border-radius: 4px; }
        button { cursor: pointer; padding: 10px 15px; background: #007bff; color: white; border: none; border-radius: 4px; width: 100%; margin-top: 5px; }
        button:hover { background: #0056b3; }
        .secondary-btn { background: #28a745; } /* 綠色按鈕用於註冊 */
        .secondary-btn:hover { background: #218838; }
        .logout-btn { background: #dc3545; } /* 紅色按鈕用於登出 */
        .data-display { background: #eee; padding: 10px; margin-top: 10px; border-radius: 4px; font-size: 0.9em; }
        hr { margin: 20px 0; border: 0; border-top: 1px solid #eee; }
    </style>
</head>
<body>

    <h2>Firebase 使用者系統 (Email/Pass)</h2>

    <div class="card">
        <div id="auth-status" class="status">正在檢查狀態...</div>

        <!-- 登入與註冊區塊 (未登入時顯示) -->
        <div id="auth-form">
            <input type="email" id="email-input" placeholder="電子郵件">
            <input type="password" id="password-input" placeholder="密碼 (至少 6 位數)">
            <button id="btn-login">登入</button>
            <button id="btn-signup" class="secondary-btn">註冊新帳號</button>
        </div>

        <!-- 使用者功能區塊 (登入後顯示) -->
        <div id="app-content" style="display:none;">
            <p>歡迎,<span id="user-email" style="color:blue;"></span></p>
            <p>UID: <small id="user-uid"></small></p>
            
            <hr>
            <label>更新個人暱稱:</label>
            <input type="text" id="nickname-input" placeholder="輸入你想叫什麼">
            <button id="btn-save">儲存到 Firestore</button>
            <button id="btn-read">讀取我的資料</button>

            <div class="data-display" id="display-area">尚未讀取資料...</div>
            <hr>
            <button id="btn-logout" class="logout-btn">登出系統</button>
        </div>
    </div>

    <script type="module">
        import { initializeApp } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js";
        // 引入 Email 登入所需的函式
        import { 
            getAuth, 
            createUserWithEmailAndPassword, 
            signInWithEmailAndPassword, 
            onAuthStateChanged, 
            signOut 
        } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js";
        import { getFirestore, doc, setDoc, getDoc } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js";

        // 1. Firebase 設定值 (請替換成你的!)
        const firebaseConfig = {
            apiKey: "你的API金鑰",
            authDomain: "你的專案ID.firebaseapp.com",
            projectId: "你的專案ID",
            storageBucket: "...",
            messagingSenderId: "...",
            appId: "..."
        };

        const app = initializeApp(firebaseConfig);
        const auth = getAuth(app);
        const db = getFirestore(app);

        // --- DOM 元素 ---
        const authStatus = document.getElementById('auth-status');
        const authForm = document.getElementById('auth-form');
        const appContent = document.getElementById('app-content');
        const userEmailDisp = document.getElementById('user-email');
        const userUidDisp = document.getElementById('user-uid');
        const emailInput = document.getElementById('email-input');
        const passwordInput = document.getElementById('password-input');
        const nicknameInput = document.getElementById('nickname-input');
        const displayArea = document.getElementById('display-area');

        // --- 狀態監聽 ---
        onAuthStateChanged(auth, (user) => {
            if (user) {
                authStatus.innerText = "狀態:已登入";
                userEmailDisp.innerText = user.email;
                userUidDisp.innerText = user.uid;
                authForm.style.display = "none";
                appContent.style.display = "block";
            } else {
                authStatus.innerText = "狀態:請登入";
                authForm.style.display = "block";
                appContent.style.display = "none";
            }
        });

        // --- 功能:註冊 ---
        document.getElementById('btn-signup').onclick = async () => {
            const email = emailInput.value;
            const password = passwordInput.value;
            try {
                const userCredential = await createUserWithEmailAndPassword(auth, email, password);
                alert("註冊成功!歡迎 " + userCredential.user.email);
            } catch (e) {
                alert("註冊失敗: " + e.message);
            }
        };

        // --- 功能:登入 ---
        document.getElementById('btn-login').onclick = async () => {
            const email = emailInput.value;
            const password = passwordInput.value;
            try {
                await signInWithEmailAndPassword(auth, email, password);
                alert("登入成功!");
            } catch (e) {
                alert("登入失敗: " + e.message);
            }
        };

        // --- 功能:登出 ---
        document.getElementById('btn-logout').onclick = () => signOut(auth);

        // --- 功能:儲存資料 (Firestore) ---
        document.getElementById('btn-save').onclick = async () => {
            const user = auth.currentUser;
            const name = nicknameInput.value;
            try {
                await setDoc(doc(db, "users", user.uid), {
                    nickname: name,
                    email: user.email,
                    lastSeen: new Date()
                }, { merge: true }); // merge: true 表示只更新填寫的欄位,不覆蓋整份文件
                alert("存檔完成");
            } catch (e) {
                alert("存檔失敗:" + e.message);
            }
        };

        // --- 功能:讀取資料 (Firestore) ---
        document.getElementById('btn-read').onclick = async () => {
            const user = auth.currentUser;
            try {
                const docSnap = await getDoc(doc(db, "users", user.uid));
                if (docSnap.exists()) {
                    const data = docSnap.data();
                    displayArea.innerHTML = `暱稱: ${data.nickname}<br>帳號: ${data.email}`;
                } else {
                    displayArea.innerText = "尚未建立個人檔案資料。";
                }
            } catch (e) {
                alert("讀取失敗");
            }
        };
    </script>
</body>
</html>

啟用 GitHub Pages

 

Step 1|確認程式在 GitHub 上能正常顯示

你已完成這一步(Repo 已存在)


Step 2|啟用 GitHub Pages(最重要)

  1. 進入你的 Repo
    👉 https://github.com/tsaimh/PDFToJpg

  2. 點 Settings

  3. 左側選單 → Pages

  4. 設定如下:

    • Source:Deploy from a branch

    • Branch:main

    • Folder:/ (root)

  5. 點 Save


Step 3|取得正式使用網址

約 10–30 秒後,你會看到一個網址:

https://tsaimh.github.io/PDFToJpg/

這個網址具備以下特性:

  • 不用登入

  • 不用安裝

  • 手機、平板、電腦都能用

  • 學生只要點網址就能操作


四、給「學生/一般使用者」的使用說明(建議你放 README)

你可以直接放這段(我幫你寫好):

📄 PDF 轉 JPG 線上工具 使用方式: 1. 開啟工具網址 2. 點選「選擇 PDF 檔案」 3. 系統會自動轉換每一頁為 JPG 4. 下載圖片即可 特色: ✔ 不需安裝 ✔ 不需登入 ✔ 檔案不會上傳到伺服器 ✔ 適合學生與教學使用

2026年1月11日 星期日

我用一個真實的 Excel 問題測試 ChatGPT,它出錯了,但我也學到了三件意想不到的事

身為一個 Excel 使用者,你是否也曾遇過這種情況:面對一個複雜的公式難題,卡關許久後,腦中第一個閃過的念頭就是「不如去問問 ChatGPT 吧?」這個想法很自然,畢竟 AI 彷彿無所不知。
但 AI 真的能完全理解我們的需求,並給出完美無瑕的解答嗎?為了找出答案,我進行了一次真實測試。我拿了一個網友提出的「勞保眷屬保費計算」Excel 問題去挑戰 ChatGPT。接下來的互動,簡直像一場雲霄飛車,充滿了低級錯誤、驚訝的誤解,以及一次意想不到的靈光乍現。這次經驗,讓我學到了三件關於如何「真正」與 AI 協作的寶貴課題。
1. AI 連「欄位」都會看錯:盲目複製貼上,結果就是一場空
這次的 Excel 問題,目標是加總每位員工(依「人事編號」區分)的健保費。但網友有兩種具體的呈現需求:一種是簡單版,在每個員工的每一筆資料列旁都顯示加總金額;另一種是進階版,只在每個員工資料群組的最後一筆才顯示總額。正是這個更細緻的第二點,真正考驗了 AI 的理解能力。
我將 Excel 的畫面截圖直接丟給 ChatGPT,請它提供公式。很快地,它給了我一個 SUMIF 公式。然而,一個經驗豐富的使用者一眼就能看出問題:健保費的來源資料在 H 欄,而我希望放置總計的目標是 J 欄,但 AI 提供的公式卻參考了一個完全不相關的 I 欄。
這是個典型的「新手錯誤」——無論對人類或 AI 而言。它提醒了我們,AI「讀取」圖片的方式與人眼不同,很容易誤判表格結構。如果我當時沒有檢查,直接複製貼上這個錯誤的公式,會發生什麼事?結果就是所有儲存格都顯示「0」。這個簡單的錯誤直接證明了盲目信任 AI 的風險。
根據你提供的畫面來判斷...所以健保合計I欄...他寫I欄所以這是錯的...如果照他的貼上去會怎樣 都是0所以你要自己判斷AI寫的東西對不對。
這個經驗給我們的第一個教訓非常明確:AI 提供的任何程式碼或公式,都絕對不能直接使用,必須經過我們親自的校正與驗證。
2. 它解決了問題,但不是「我」的問題:AI 的理解與你的需求有落差
在修正了 AI 的低級欄位錯誤後,我以為接下來就順利了。但我得到的下一個答案,卻揭示了一個更深層、也更微妙的問題。
當我進一步向 ChatGPT 提出「只在每個群組的最後一筆資料列顯示總計金額」這個進階需求時,它提供的解決方案是「只在第一筆資料列顯示總額」。這個方法確實能達成「只顯示一筆總額」的目標,但它並不符合我「在最後一筆顯示」的具體要求。
AI跟你講的做法是第一筆出現,那我是最後一筆出現是不一樣的做法。
這件事點出了一個關鍵區別:AI 解決了「一個」問題,但不是「我」的問題。對於任何使用 AI 的專業人士來說,這是一個重要的啟示。我們在提問時,經常會不自覺地代入隱含的背景知識或行業慣例(例如「常規的做法」、「讓版面乾淨點」)。但目前的 AI 會逐字解釋你的指令,它能理解你請求的「語法」(只顯示一筆),卻不一定能洞悉背後的「語意」(顯示在群組末尾才符合閱讀習慣)。提供完整、精確脈絡的責任,完全在我們身上。
3. 殊途同歸的兩種解法:AI 的邏輯,擴展了我的解題思路
經過幾輪溝通,在我明確指出「我希望在最後一筆顯示總額」之後,ChatGPT 終於提供了一個完全正確的公式。然而,最令人驚喜的部分來了:它達成目標的邏輯,和我自己的解法截然不同。
• 我的做法: 使用 COUNTIF 函數,從資料範圍的開頭算到目前位置,計算這個人事編號出現了幾次。當這個次數等於該編號的總出現次數時(代表這是最後一筆),才顯示加總金額。
• AI 的做法: 同樣使用 COUNTIF 函數,但它的計算範圍是從目前位置算到資料範圍的結尾。當計算結果等於 1 時(代表這個編號在此之後只出現這一次,也就是它自己),就顯示加總金額。
簡單來說,我的方法是向後看,問的是:「從頭算到現在,這是不是這個編號的最後一次出現?」 而 AI 的方法是向前看,問的是:「從現在這個位置往後看,這個編號是不是只剩下一次了?」 兩者都完美地找到了最後一列,但思考的邏輯方向完全相反。
這就是整個實驗的轉捩點。我不再將 ChatGPT 視為一個單純的公式產生器,而是把它看作一個「陪練夥伴」。它不一定總是對的,但它迥異的思考方式,迫使我重新檢視自己的邏輯,也再次印證了在 Excel 的世界裡,通往羅馬的路從來不只一條。
他就會跟我這個做法是一樣,然後他的做法是用Count If等於1的方式,那我是用等於他的最大值...實際上做法是都一樣,只是他的思考方式跟我思考方式不一樣。
總結與反思
這次與 ChatGPT 的交手,讓我深刻體會到三件事:
1. AI 會犯基本錯誤,我們必須扮演最終的驗證者。
2. AI 可能會誤解你的真實意圖,精準的提問至關重要。
3. AI 也能提供新的解題思路,成為我們擴展思維的催化劑。
總結來說,AI 目前是一個極其強大的「輔助工具」,而非可以完全取代人類思考的「全能專家」。我們終究必須是駕駛艙裡的飛行員,而不是被動的乘客。AI 可以是個能力超凡的副駕,協助我們規劃複雜的航線、提供替代方案,但最終,緊握方向舵、確認儀表數據、並對目的地做出最終決策的人,必須是我們自己。
在你使用 AI 解決專業問題時,你是否也曾有過類似的「意外收穫」?歡迎分享你的經驗。
--------------------------------------------------------------------------------
資源分享
如果你想親手練習這個案例,可以參考以下資源:
• 練習檔案下載: 點此前往下載頁面 (需 Email 註冊會員後即可免費下載)

• 本次 ChatGPT 對話紀錄: 點此查看 (提醒:對話連結中的圖片可能無法顯示,但公式仍然可見,可用於參考) 



【免寫程式的自動化革命】用 AI Studio 打造專屬 LINE 行事曆祕書:5 個極具啟發性的開發與防坑亮點

在步調緊湊的現代工作中,Google 日曆(Google Calendar)雖然具備內建提醒,卻需要使用者針對每個行程手動設定;一旦繁忙起來漏看系統通知,重要約會或待辦事項依舊面臨「被漏掉」的風險。試想一下:若能每天清晨打開 LINE,自動收到一份由專屬 AI 祕書梳理完畢的「今...