🌐如何在 Vue 3 + Pinia 記住「執行時間」資訊? — 從零開始也看得懂的前端狀態管理實戰

一、前言:為什麼「執行時間」總是消失?

假設你做了一個「訂單排程結果」頁面,上面會顯示:

執行時間:2025-11-01 01:00:00 ~ 2025-11-01 01:03:21

共計:00:03:21


這個時間是後端夜間批次排程跑完後,寫到資料庫,再由前端透過 API 抓回來顯示的。

一切看起來都很正常,直到你遇到這幾個情況:

  • 換到另一個頁面再切回來,執行時間不見了

  • F5 重新整理頁面,列表還能透過 store 還原,但執行時間變成空白

  • 同一個「版本」查詢結果明明一樣,但執行時間又要再打一次 API 才抓得到

於是你會問:

為什麼表格資料可以記住,執行時間卻不能一起記?


其實原因很簡單:你有用 Pinia 記住表格資料,但沒有把「執行時間」也存進去。

這篇文章會用「完全不懂程式的人也看得懂」的方式,帶你一步一步理解:

  • 什麼是「全域狀態」?

  • 為什麼要把「執行時間」一起存?

  • Vue 3 + Pinia 怎麼設計,才能跨頁 / 重新整理都保留這些資訊?

所有程式碼範例我都會用新的名字與結構來示範,不會使用任何真實專案的內容。


二、先用生活例子理解:什麼是「全域狀態」?

想像你在一個大商場裡逛街:

  • 每一間店 = 一個 Vue 頁面

  • 客人手上的購物籃 = 當前頁面的狀態(例如查詢結果、篩選條件…)

  • 購物中心的服務台 = 一個「全域狀態管理」工具(在 Vue 裡就是 Pinia)

如果你只把東西放在店裡的推車(單一頁面),一走出那間店就沒了。
但如果你把重要的東西先寄放在「服務台」,你就可以:

  • 去別的店逛一圈

  • 再回到同一間店

  • 跟服務台說:「我要取回剛剛那籃東西」

這就是 全域狀態的概念

在這篇文章裡:

  • 列表資料、篩選條件、選取的版本

  • 以及「執行時間」

全部都會存到「服務台」(Pinia)裡,讓你怎麼切頁、怎麼返回,都能恢復現場。


三、需求拆解:我們想做到什麼?

用一句話描述這個功能:

「當使用者選擇某個報表版本查詢時,不只要顯示對應的表格資料,也要顯示這個版本對應的批次執行時間,並且在頁面切換或重新整理後仍能保留。」


拆開來有 4 個重點:

  1. 前端有一個查詢頁面(例如:排程結果)

  2. 使用者選擇一個「版本」後送出查詢

  3. 後端提供兩種資料:

    • 報表內容(表格)

    • 排程執行資訊(開始時間、結束時間、花費時間)

  4. 這些資料應該存到 Pinia 中,讓:

    • 返回此頁面時可以還原

    • 同一個版本不用重複打 API 也能看到執行時間

接下來我們用一個重新命名、簡化過的範例來示範整個流程。


四、定義 Pinia Store:集中管理各頁面狀態與執行時間

先建立一個 useReportStore.ts(名稱可以自訂)的 store,用來管理:

  • 全域選取的版本

  • 各頁面的:

    • 版本

    • 表格欄位

    • 查詢結果

    • 篩選條件

    • 執行時間(重點!)

4-1. 型別與 state 結構

⚠️ 以下程式碼是全新示範用範例,與你真實專案無關。


// src/stores/useReportStore.ts
import { defineStore } from 'pinia' interface PageState { selectedVersion: string headers: string[] rows: any[] filters: Record<string, any> runInfo: { startTime: string endTime: string duration: string } | null } interface GlobalFilters { factory: any[] customer: any[] product: any[] [key: string]: any[] } export const useReportStore = defineStore('reportStore', { state: () => ({ // 全域選取版本(例如下拉選單的值) globalVersion: '', // 全域共享篩選條件(例如廠別、客戶、品名) globalFilters: { factory: [], customer: [], product: [], } as GlobalFilters, // 各頁面獨立狀態(這裡示範三頁) pages: { summary: { selectedVersion: '', headers: [], rows: [], filters: {}, runInfo: null, // ✅ 關鍵:執行時間資訊 }, detail: { selectedVersion: '', headers: [], rows: [], filters: {}, runInfo: null, }, material: { selectedVersion: '', headers: [], rows: [], filters: {}, runInfo: null, }, } as Record<string, PageState>, }), actions: { /** 設定全域版本(例如由下拉框選到新版本時呼叫) */ setGlobalVersion(version: string) { this.globalVersion = version }, /** 更新全域共用篩選條件(例如 SITE、客戶等) */ setGlobalFilter(key: string, value: any[]) { this.globalFilters[key] = value }, /** 儲存指定頁面的完整狀態(含執行時間) */ setPageState( pageKey: string, payload: { selectedVersion: string headers: string[] rows: any[] filters: Record<string, any> runInfo: PageState['runInfo'] } ) { if (!this.pages[pageKey]) return this.pages[pageKey].selectedVersion = payload.selectedVersion this.pages[pageKey].headers = payload.headers this.pages[pageKey].rows = payload.rows this.pages[pageKey].filters = payload.filters this.pages[pageKey].runInfo = payload.runInfo // ✅ 把執行時間也記起來 }, /** 只更新某一頁的執行時間,不動其它狀態 */ setPageRunInfo(pageKey: string, info: PageState['runInfo']) { if (!this.pages[pageKey]) return this.pages[pageKey].runInfo = info }, /** 清除指定頁面的資料(例如按下「清空」按鈕時) */ clearPage(pageKey: string) { if (!this.pages[pageKey]) return this.pages[pageKey].selectedVersion = '' this.pages[pageKey].headers = [] this.pages[pageKey].rows = [] this.pages[pageKey].filters = {} this.pages[pageKey].runInfo = null }, }, })

關鍵有兩個:

  1. runInfo 放在 PageState 裡,代表 每個頁面都有自己的執行時間資訊

  2. setPageState()setPageRunInfo() 都會把 runInfo 一併維護


五、在頁面組件裡:查詢資料 + 取得執行時間

接下來看一個簡化版的 Vue 3 組件,假設檔名叫 ReportSummary.vue

5-1. 匯入 store 與基本狀態

<script setup lang="ts"> import { ref, computed, onMounted, watch } from 'vue' import apiClient from '@/services/apiClient' import { useReportStore } from '@/stores/useReportStore' const pageKey = 'summary' const store = useReportStore() // 綁定全域選取版本 const selectedVersion = computed({ get: () => store.globalVersion, set: (val) => store.setGlobalVersion(val), }) const loading = ref(false) const versions = ref<string[]>([]) const headers = ref<string[]>([]) const rows = ref<any[]>([]) const filters = ref<Record<string, any>>({}) const runInfo = ref<{ startTime: string; endTime: string; duration: string } | null>(null)

5-2. 取得版本清單

async function fetchVersions() { try { const res = await apiClient.get('/report/versions') versions.value = res.data.sort().reverse() } catch (err) { console.error('取得版本清單失敗', err) } }

5-3. 查詢報表資料(表格)

async function fetchReportData() { if (!selectedVersion.value) { alert('請先選擇版本') return } loading.value = true try { const res = await apiClient.get(`/report/summary?version=${selectedVersion.value}`) if (Array.isArray(res.data) && res.data.length > 0) { // 這裡用簡單方式取欄位名稱 headers.value = Object.keys(res.data[0]) rows.value = res.data // 每次查詢後先把目前頁面狀態存進 store(runInfo 稍後補上) store.setPageState(pageKey, { selectedVersion: selectedVersion.value, headers: headers.value, rows: rows.value, filters: filters.value, runInfo: runInfo.value, }) // 查詢完主資料後,再查執行時間 await fetchRunInfo() } else { headers.value = [] rows.value = [] runInfo.value = null } } catch (err) { console.error('取得報表資料失敗', err) } finally { loading.value = false } }

5-4. 取得執行時間資訊(重點函式)

這裡對應你原本的 fetchJobInfo 概念,但用完全新的命名與欄位示例。

async function fetchRunInfo() { try { const res = await apiClient.get(`/report/run-info?version=${selectedVersion.value}`) if (Array.isArray(res.data) && res.data.length > 0) { const info = res.data[0] runInfo.value = { startTime: info.start_time, // 依照你 API 回傳欄位修改 endTime: info.end_time, duration: info.duration_text, } } else { runInfo.value = null } // ✅ 同步寫回 Pinia,讓狀態可跨頁 / 重整保留 store.setPageRunInfo(pageKey, runInfo.value) } catch (err) { console.error('取得執行時間失敗', err) } }

六、還原狀態:回到頁面時自動帶出執行時間

使用者「切到別頁 → 再回來」時,我們希望:

  • 如果版本跟之前一樣

  • 就直接用 store 裡的資料還原,而不是全部重查一次

onMounted(async () => { await fetchVersions() const saved = store.pages[pageKey] if (saved && saved.rows.length > 0 && saved.selectedVersion === store.globalVersion) { // ✅ 還原之前的狀態(包括執行時間) selectedVersion.value = saved.selectedVersion headers.value = saved.headers rows.value = saved.rows filters.value = saved.filters runInfo.value = saved.runInfo } else if (selectedVersion.value) { // 如果全域版本已經有值,但沒有頁面資料,就重新查一次 await fetchReportData() } }) // 如果版本改變,自動重新查 watch(selectedVersion, (val) => { if (val) { fetchReportData() } })

七、在畫面上顯示「執行時間」

前端畫面使用非常簡單的方式:

<template> <div class="toolbar"> <div class="left"> <label>報表版本:</label> <select v-model="selectedVersion"> <option value="" disabled>請選擇版本</option> <option v-for="v in versions" :key="v" :value="v">{{ v }}</option> </select> <button @click="fetchReportData">查詢</button> <!-- ✅ 執行時間顯示區塊 --> <span v-if="runInfo" class="run-info"> 執行時間:{{ runInfo.startTime }} ~ {{ runInfo.endTime }} (共計:{{ runInfo.duration }}) </span> </div> </div> <div class="table-wrapper"> <table v-if="rows.length"> <thead> <tr> <th v-for="h in headers" :key="h">{{ h }}</th> </tr> </thead> <tbody> <tr v-for="(r, i) in rows" :key="i"> <td v-for="h in headers" :key="h">{{ r[h] }}</td> </tr> </tbody> </table> <div v-else-if="loading">載入中...</div> <div v-else>尚無資料</div> </div> </template>

樣式略寫一個簡單範例:

<style scoped> .toolbar { display: flex; align-items: center; gap: 12px; padding: 10px; border-bottom: 1px solid #ddd; } .run-info { margin-left: 16px; font-weight: bold; color: #333; } .table-wrapper { margin-top: 12px; max-height: 500px; overflow: auto; border: 1px solid #ccc; } table { border-collapse: collapse; min-width: 600px; } th, td { border: 1px solid #ddd; padding: 6px 10px; white-space: nowrap; } </style>

八、常見問題&除錯方向

1. 為什麼我明明呼叫了 setPageRunInfo,重新整理後還是會消失?

Pinia 預設只存 記憶體,重新整理就會清空。
如果你希望「重新整理也保留」,就需要搭配:

  • Pinia plugin + localStorage

  • pinia-plugin-persistedstate 之類的工具

這是另一個主題,可以另外寫一篇。


2. 要不要把「執行時間」直接塞進每筆資料裡?

通常不建議。

執行時間是屬於「整個版本的 metadata(附加資訊)」,不是每一筆資料本身的屬性。
把它獨立在 runInfo 會比較清楚,也比較容易維護。


3. 為什麼不要每次 render 都重新打 /run-info API?

  • 對後端是負擔

  • 不容易做快取

  • 使用者切頁很頻繁時,體驗也不好

直接把執行時間存進 Pinia,將來要再取,就變成單純的讀取動作。


九、結語:把「小細節」當成「產品體驗」的一部分

很多人做前端頁面時,會先把「主資料」做好,例如:

  • 表格有顯示

  • 查詢有反應

  • 匯出功能可以按

但像「執行時間」、「執行人員」、「最後更新原因」這種看似小細節,其實對使用者來說非常重要:

  • 他可以快速知道資料是不是最新的

  • 批次如果掛掉,對照時間可以幫後端排查

  • 開會時,看到「報表最後執行時間」,不會被問到突然語塞

透過 Vue 3 + Pinia 這種全域狀態管理方式,我們可以很優雅地把這些資訊整合進去,做到:

  • 查一次,跨頁共享

  • 狀態集中管理,維護容易

  • 使用者體驗更完整

如果你目前專案裡已經有使用 Pinia 管列表資料,不妨也把「執行時間」類的 metadata 一起納入設計,同樣存進 store,下次切頁回來,全部都還在。

留言

這個網誌中的熱門文章

🔍Vue.js 專案錯誤排查:解決 numericFields is not defined 與合併儲存格邏輯最佳化

🔎EF Core 連 Oracle 出現 ORA-00600 [kpp_concatq:2] 的完整排錯指南(含 EF Core ToString/CultureInfo 錯誤)

🛠【ASP.NET Core + Oracle】解決 ORA-00904 "FALSE": 無效的 ID 錯誤與資料欄位動態插入顯示問題