🌐Vue 3 下拉選單一改變就自動打 API?用 watch 與查詢按鈕控制資料重新載入
在 Vue 3 專案中,我們常會做一個「版本下拉選單 + 查詢按鈕 + 表格資料」的頁面。
畫面大概長這樣:
使用者先選擇版本,然後按下「查詢」按鈕,系統才去後端 API 撈資料並重新產生表格。
但有時候會遇到一個很常見的問題:
明明使用者只是更換下拉選單版本,還沒有按「查詢」,表格卻已經自動重新查詢了。
這種情況通常不是 API 壞掉,也不是按鈕被誤觸,而是 Vue 的
watch
在背景自動監聽到了資料變化。
問題情境
假設畫面上有一個版本下拉選單:
<select v-model="selectedBatch">
<option value="">請選擇批次</option>
<option v-for="item in batchList" :key="item" :value="item">
{{ item }}
</option>
</select>
<button @click="loadTableData">查詢</button>
直覺上我們會以為:
選版本 → 不查詢
按查詢 → 才查詢
但如果程式裡面有這段:
watch(selectedBatch, (newValue) => {
if (newValue) {
loadTableData()
}
})
那實際流程就會變成:
選版本 → selectedBatch 改變 → watch 觸發 → loadTableData() 執行 → API 被呼叫
所以即使使用者沒有按「查詢」按鈕,系統也會自動重跑資料。
為什麼會發生?
在 Vue 3 中,v-model
是雙向綁定。
當使用者更改下拉選單時:
selectedBatch.value = '2026-04'
這個值會立刻改變。
如果你又寫了
watch(selectedBatch),Vue
就會偵測到這個變化,然後自動執行你寫在 watch 裡面的邏輯。
例如:
watch(selectedBatch, () => {
loadTableData()
})
這段意思就是:
只要版本有變,我就重新查詢。
所以問題不在按鈕,而是在
watch
已經幫你自動呼叫查詢方法。
常見錯誤寫法
下面是一個常見範例:
const selectedBatch = ref('')
const tableRows = ref([])
const loading = ref(false)
const executeInfo = ref(null)
async function loadTableData() {
if (!selectedBatch.value) {
alert('請先選擇批次')
return
}
loading.value = true
try {
const result = await api.get(`/report/data?batch=${selectedBatch.value}`)
tableRows.value = result.data
await loadExecuteInfo()
} finally {
loading.value = false
}
}
async function loadExecuteInfo() {
const result = await api.get(`/report/job?batch=${selectedBatch.value}`)
executeInfo.value = result.data?.[0] || null
}
watch(selectedBatch, (newValue) => {
if (newValue) {
loadTableData()
}
})
這段程式的問題是:
watch(selectedBatch, ...)
會讓下拉選單一變更就直接查詢。
而
loadTableData() 裡面又呼叫了:
await loadExecuteInfo()
所以就會造成:
更換版本
→ 自動查詢表格
→ 自動查詢執行時間
解法一:只按查詢才打 API
如果你的需求是「使用者更換版本後,不要馬上查詢,必須按查詢才查」,那就應該移除自動查詢的
watch。
修改前:
watch(selectedBatch, (newValue) => {
if (newValue) {
loadTableData()
}
})
修改後:
// 不再監聽 selectedBatch 自動查詢
// 使用者必須按下查詢按鈕才會執行 loadTableData()
按鈕保留:
<button @click="loadTableData">
查詢
</button>
這樣流程會變成:
選版本 → 只改畫面上的 selectedBatch
按查詢 → 才呼叫 API
解法二:分離「選取版本」與「已查詢版本」
比較穩定的寫法,是準備兩個變數:
const selectedBatch = ref('')
const queriedBatch = ref('')
意思是:
selectedBatch:目前下拉選單選到的版本
queriedBatch:目前表格實際查詢出來的版本
查詢時才同步:
async function searchReport() {
if (!selectedBatch.value) {
alert('請先選擇批次')
return
}
queriedBatch.value = selectedBatch.value
await loadTableData(queriedBatch.value)
await loadExecuteInfo(queriedBatch.value)
}
完整範例:
const selectedBatch = ref('')
const queriedBatch = ref('')
const tableRows = ref([])
const loading = ref(false)
const executeInfo = ref(null)
async function searchReport() {
if (!selectedBatch.value) {
alert('請先選擇批次')
return
}
queriedBatch.value = selectedBatch.value
await loadTableData(queriedBatch.value)
await loadExecuteInfo(queriedBatch.value)
}
async function loadTableData(batchNo) {
loading.value = true
try {
const response = await api.get(`/sample/table?version=${batchNo}`)
tableRows.value = response.data || []
} catch (error) {
console.error('查詢表格資料失敗', error)
} finally {
loading.value = false
}
}
async function loadExecuteInfo(batchNo) {
try {
const response = await api.get(`/sample/process-info?version=${batchNo}`)
executeInfo.value = response.data?.[0] || null
} catch (error) {
console.error('查詢執行資訊失敗', error)
}
}
HTML:
<select v-model="selectedBatch">
<option value="">請選擇版本</option>
<option v-for="version in versionOptions" :key="version" :value="version">
{{ version }}
</option>
</select>
<button @click="searchReport">
查詢
</button>
這種寫法的好處是:
使用者可以自由切換下拉選單
但表格不會跟著亂跑
只有按下查詢時,資料才會正式更新
解法三:保留頁面快取,但不要自動查詢
有些系統會使用 Pinia 儲存頁面狀態,例如:
const store = useReportStore()
進入頁面時,如果之前已經查過,就直接還原資料:
onMounted(async () => {
await loadVersionOptions()
const cache = store.pageCache['reportPage']
if (cache && cache.rows?.length) {
tableRows.value = cache.rows
tableHeaders.value = cache.headers
filters.value = cache.filters
executeInfo.value = cache.executeInfo
}
})
查詢時再更新快取:
async function searchReport() {
if (!selectedBatch.value) {
alert('請先選擇版本')
return
}
loading.value = true
try {
const tableResult = await api.get(`/demo/report-data?version=${selectedBatch.value}`)
const jobResult = await api.get(`/demo/report-log?version=${selectedBatch.value}`)
tableRows.value = tableResult.data || []
executeInfo.value = jobResult.data?.[0] || null
store.savePageCache('reportPage', {
selectedBatch: selectedBatch.value,
rows: tableRows.value,
headers: tableHeaders.value,
filters: filters.value,
executeInfo: executeInfo.value
})
} finally {
loading.value = false
}
}
這樣就可以做到:
切換頁面回來 → 還原原本資料
更換下拉選單 → 不自動查詢
按下查詢 → 才重新打 API 並更新快取
建議的最終做法
如果你的需求是「更換版本後,必須按查詢才更新表格」,建議採用以下原則:
第一,移除這種自動查詢的 watch:
watch(selectedBatch, () => {
loadTableData()
})
第二,把查詢集中在一個方法:
async function searchReport() {
await loadTableData()
await loadExecuteInfo()
}
第三,按鈕只呼叫這個方法:
<button @click="searchReport">
查詢
</button>
第四,若有 Pinia 快取,查詢成功後再存入 store。
總結
Vue 3 的
watch
很好用,但如果使用不小心,就會造成「使用者只是改下拉選單,系統卻自動查詢」的問題。
這類問題的核心觀念是:
v-model 會改變資料
watch 會監聽資料變化
watch 裡面如果呼叫 API,就會自動查詢
所以如果你希望使用者「按查詢才查」,就不要在
watch 裡面呼叫查詢 API。
比較乾淨的設計是:
下拉選單:只負責選值
查詢按鈕:負責打 API
Pinia:負責保存已查詢結果
表格:只顯示目前已查詢資料
這樣前端邏輯會更清楚,也能避免不必要的 API 呼叫與資料誤更新。
留言
張貼留言