iT邦幫忙

2024 iThome 鐵人賽

DAY 22
0
Software Development

我獨自開發 - 30天進化之路,掌握 Laravel + Nuxt系列 第 22

D22 - 實作交易紀錄管理:建立交易紀錄列表與新增/編輯頁面

  • 分享至 

  • xImage
  •  

哈囉,大家好!在前面的文章中,我們已經實作了銀行帳戶管理和分類管理的功能,建立了相應的列表頁面以及新增/編輯頁面。
今天,我們將繼續完善我們的個人財務管理系統,專注於交易紀錄管理的開發。
交易紀錄是財務管理系統的核心,透過記錄每一筆收入和支出,我們可以清楚地了解自己的財務狀況。
接下來,我們將建立交易紀錄列表頁面以及新增/編輯交易紀錄頁面,讓使用者可以方便地管理他們的交易紀錄。

一、建立交易紀錄列表頁面

首先,我們要建立交易紀錄列表頁面,讓使用者可以查看、編輯和刪除他們的交易紀錄。

1. 建立頁面檔案

在 pages/ 目錄下建立 transactions.vue 檔案。

touch pages/transactions.vue

2. 編寫頁面結構

在 transactions.vue 中,我們建立基本的頁面結構,顯示交易紀錄列表。

<template>
  <div>
    <h2 class="text-2xl font-bold mb-4">交易紀錄</h2>
    <div v-if="error" class="text-red-500">
      {{ error }}
    </div>
    <div v-else>
      <button @click="goToAddTransaction" class="bg-blue-600 text-white px-4 py-2 mb-4">
        新增交易
      </button>
      <table class="w-full text-left">
        <thead>
          <tr>
            <th class="border px-4 py-2">日期</th>
            <th class="border px-4 py-2">類型</th>
            <th class="border px-4 py-2">分類</th>
            <th class="border px-4 py-2">金額</th>
            <th class="border px-4 py-2">帳戶</th>
            <th class="border px-4 py-2">描述</th>
            <th class="border px-4 py-2">操作</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="transaction in transactions" :key="transaction.id">
            <td class="border px-4 py-2">{{ transaction.date }}</td>
            <td class="border px-4 py-2">{{ transaction.type === 'income' ? '收入' : '支出' }}</td>
            <td class="border px-4 py-2">{{ transaction.category.name }}</td>
            <td class="border px-4 py-2">{{ transaction.amount }}</td>
            <td class="border px-4 py-2">{{ transaction.bank_account.account_name }}</td>
            <td class="border px-4 py-2">{{ transaction.description }}</td>
            <td class="border px-4 py-2">
              <button @click="goToEditTransaction(transaction.id)" class="text-blue-600 mr-2">
                編輯
              </button>
              <button @click="deleteTransaction(transaction.id)" class="text-red-600">
                刪除
              </button>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<script>
export default {
  middleware: 'auth',
  data() {
    return {
      transactions: [],
      error: null,
    }
  },
  async mounted() {
    try {
      const response = await this.$axios.get('/api/transactions')
      if (response.data.status === 'success') {
        this.transactions = response.data.data
      } else {
        this.error = response.data.message || '無法獲取交易紀錄資料。'
      }
    } catch (error) {
      this.error = '伺服器發生錯誤,請稍後再試。'
      console.error('Error fetching transactions:', error)
    }
  },
  methods: {
    goToAddTransaction() {
      this.$router.push('/transactions/add')
    },
    goToEditTransaction(id) {
      this.$router.push(`/transactions/edit/${id}`)
    },
    async deleteTransaction(id) {
      if (confirm('確定要刪除這筆交易嗎?')) {
        try {
          await this.$axios.delete(`/api/transactions/${id}`)
          this.transactions = this.transactions.filter(transaction => transaction.id !== id)
        } catch (error) {
          alert('刪除失敗,請稍後再試。')
          console.error('Error deleting transaction:', error)
        }
      }
    },
  },
}
</script>

<style scoped>
/* 頁面專屬的樣式 */
</style>

3. 說明

  • 資料取得:在 mounted 生命週期鉤子中,我們向後端 API 請求交易紀錄列表,並將資料存入 transactions。
  • 新增交易:點擊「新增交易」按鈕,導向新增交易的頁面。
  • 編輯交易:在列表中,點擊「編輯」按鈕,導向編輯該筆交易的頁面。
  • 刪除交易:點擊「刪除」按鈕,彈出確認對話框,確認後發送刪除請求,並從列表中移除該筆交易。

4. 處理權限保護

  • 使用 middleware: 'auth',確保只有已登入的使用者才能訪問此頁面。

二、建立新增/編輯交易紀錄頁面

接下來,我們建立新增和編輯交易紀錄的頁面,使用共用的表單元件來實現。

1. 建立頁面檔案

在 pages/transactions/ 目錄下建立以下檔案:

mkdir -p pages/transactions
touch pages/transactions/add.vue
touch pages/transactions/_id.vue
touch pages/transactions/_form.vue

2. 編寫 _form.vue 元件

在 _form.vue 中,我們建立交易紀錄的表單元件,供新增和編輯頁面使用。

<template>
  <div>
    <h2 class="text-2xl font-bold mb-4">{{ isEdit ? '編輯交易' : '新增交易' }}</h2>
    <form @submit.prevent="submitForm">
      <div class="mb-4">
        <label class="block">日期</label>
        <input v-model="form.date" type="date" class="border p-2 w-full" required />
        <p v-if="errors.date" class="text-red-500">{{ errors.date }}</p>
      </div>
      <div class="mb-4">
        <label class="block">類型</label>
        <select v-model="form.type" @change="fetchCategories" class="border p-2 w-full" required>
          <option value="">請選擇</option>
          <option value="income">收入</option>
          <option value="expense">支出</option>
        </select>
        <p v-if="errors.type" class="text-red-500">{{ errors.type }}</p>
      </div>
      <div class="mb-4">
        <label class="block">分類</label>
        <select v-model="form.category_id" class="border p-2 w-full" required>
          <option value="">請選擇</option>
          <option v-for="category in categories" :key="category.id" :value="category.id">
            {{ category.name }}
          </option>
        </select>
        <p v-if="errors.category_id" class="text-red-500">{{ errors.category_id }}</p>
      </div>
      <div class="mb-4">
        <label class="block">金額</label>
        <input v-model="form.amount" type="number" step="0.01" class="border p-2 w-full" required />
        <p v-if="errors.amount" class="text-red-500">{{ errors.amount }}</p>
      </div>
      <div class="mb-4">
        <label class="block">帳戶</label>
        <select v-model="form.bank_account_id" class="border p-2 w-full" required>
          <option value="">請選擇</option>
          <option v-for="account in bankAccounts" :key="account.id" :value="account.id">
            {{ account.account_name }}
          </option>
        </select>
        <p v-if="errors.bank_account_id" class="text-red-500">{{ errors.bank_account_id }}</p>
      </div>
      <div class="mb-4">
        <label class="block">描述</label>
        <textarea v-model="form.description" class="border p-2 w-full"></textarea>
        <p v-if="errors.description" class="text-red-500">{{ errors.description }}</p>
      </div>
      <div v-if="error" class="text-red-500 mb-4">
        {{ error }}
      </div>
      <button type="submit" class="bg-blue-600 text-white px-4 py-2">
        {{ isEdit ? '更新' : '新增' }}
      </button>
    </form>
  </div>
</template>

<script>
export default {
  props: {
    isEdit: {
      type: Boolean,
      default: false,
    },
    transactionId: {
      type: Number,
      default: null,
    },
  },
  data() {
    return {
      form: {
        date: '',
        type: '',
        category_id: '',
        amount: 0,
        bank_account_id: '',
        description: '',
      },
      errors: {},
      error: null,
      categories: [],
      bankAccounts: [],
    }
  },
  async mounted() {
    await this.fetchBankAccounts()
    if (this.isEdit && this.transactionId) {
      try {
        const response = await this.$axios.get(`/api/transactions/${this.transactionId}`)
        if (response.data.status === 'success') {
          this.form = response.data.data
          await this.fetchCategories()
        } else {
          this.error = response.data.message || '無法獲取交易資料。'
        }
      } catch (error) {
        this.error = '伺服器發生錯誤,請稍後再試。'
        console.error('Error fetching transaction:', error)
      }
    }
  },
  methods: {
    async fetchCategories() {
      if (!this.form.type) return
      try {
        const response = await this.$axios.get(`/api/categories?type=${this.form.type}`)
        if (response.data.status === 'success') {
          this.categories = response.data.data
        } else {
          this.error = response.data.message || '無法獲取分類資料。'
        }
      } catch (error) {
        this.error = '伺服器發生錯誤,請稍後再試。'
        console.error('Error fetching categories:', error)
      }
    },
    async fetchBankAccounts() {
      try {
        const response = await this.$axios.get('/api/bank-accounts')
        if (response.data.status === 'success') {
          this.bankAccounts = response.data.data
        } else {
          this.error = response.data.message || '無法獲取銀行帳戶資料。'
        }
      } catch (error) {
        this.error = '伺服器發生錯誤,請稍後再試。'
        console.error('Error fetching bank accounts:', error)
      }
    },
    async submitForm() {
      this.errors = {}
      this.error = null
      try {
        if (this.isEdit) {
          await this.$axios.put(`/api/transactions/${this.transactionId}`, this.form)
        } else {
          await this.$axios.post('/api/transactions', this.form)
        }
        this.$router.push('/transactions')
      } catch (error) {
        if (error.response && error.response.status === 422) {
          this.errors = error.response.data.errors
        } else {
          this.error = '提交失敗,請稍後再試。'
        }
        console.error('Error submitting transaction form:', error)
      }
    },
  },
}
</script>

<style scoped>
/* 元件專屬的樣式 */
</style>

3. 編寫新增與編輯頁面

新增頁面 add.vue

<template>
  <FormComponent />
</template>

<script>
import FormComponent from './_form.vue'

export default {
  middleware: 'auth',
  components: {
    FormComponent,
  },
}
</script>

<style scoped>
/* 頁面專屬的樣式 */
</style>

編輯頁面 _id.vue

<template>
  <FormComponent :isEdit="true" :transactionId="transactionId" />
</template>

<script>
import FormComponent from './_form.vue'

export default {
  middleware: 'auth',
  components: {
    FormComponent,
  },
  computed: {
    transactionId() {
      return parseInt(this.$route.params.id)
    },
  },
}
</script>

<style scoped>
/* 頁面專屬的樣式 */
</style>

4. 說明

  • 共用表單元件:將新增和編輯的表單抽取為 _form.vue,以便重用。
  • 動態路由:使用 _id.vue 處理帶有 id 參數的路由,方便取得要編輯的交易資料。
  • 資料取得:
    • 分類:根據選擇的類型(收入或支出),動態獲取對應的分類列表。
    • 銀行帳戶:在表單載入時獲取所有的銀行帳戶。
  • 資料驗證與錯誤處理:在提交表單時,處理後端回傳的驗證錯誤,並顯示在對應的欄位下方。

三、更新導航列

為了方便使用者訪問交易紀錄管理頁面,我們需要在導航列中添加對應的連結。

1. 更新 components/Header.vue

如果之前已經有交易紀錄的連結,可以忽略此步驟,否則請添加以下內容:

<!-- 在導航列中添加交易紀錄的連結 -->
<li><NuxtLink to="/transactions">交易紀錄</NuxtLink></li>

四、測試與驗證

現在,我們已經完成了交易紀錄列表頁面和新增/編輯頁面的開發。讓我們進行測試,確保功能正常運作。

1. 測試交易紀錄列表頁面

  • 登入應用程式,點擊導航列中的「交易紀錄」連結。
  • 應該看到交易紀錄列表,如果尚無交易,列表為空。
  • 確認「新增交易」按鈕可以正常使用。

2. 測試新增交易

  • 點擊「新增交易」按鈕,進入新增頁面。
  • 填寫表單,例如:
    • 日期:選擇今天的日期
    • 類型:支出
    • 分類:餐飲
    • 金額:200
    • 帳戶:現金
    • 描述:午餐
  • 提交表單,應該返回交易紀錄列表,並顯示新添加的交易。

3. 測試編輯交易

  • 在交易紀錄列表中,點擊某筆交易的「編輯」按鈕。
  • 應該進入編輯頁面,表單中已填入該筆交易的資料。
  • 修改金額或描述,提交表單,確認列表中的資料已更新。

4. 測試刪除交易

  • 在交易紀錄列表中,點擊某筆交易的「刪除」按鈕。
  • 應該彈出確認對話框,確認後該筆交易從列表中移除。

5. 測試權限保護

  • 登出應用程式,嘗試直接訪問 /transactions,應該被重導向到登入頁面。

五、實作建議

  • 共用元件:善用共用元件,減少重複程式碼,提高開發效率。
  • 動態資料:根據使用者的選擇動態獲取資料(如分類),提升使用者體驗。
  • 資料驗證:確保前端與後端的驗證規則一致,避免不必要的錯誤。
  • 錯誤處理:處理好各種錯誤情況,提供清晰的錯誤訊息,提升使用者體驗。

小結

今天,我們成功地實作了交易紀錄列表頁面以及新增/編輯頁面。透過這次的開發,我們學習了:

  • 如何在 Nuxt 中建立頁面和元件,並使用動態路由。
  • 如何與後端 API 互動,處理交易紀錄的資料取得和提交。
  • 如何根據使用者的選擇動態更新表單內容(如分類選項)。
  • 如何處理表單驗證和錯誤訊息,提升使用者體驗。

希望這篇文章能夠對你有所幫助,讓我們一起繼續學習和進步,打造出更加完善的應用程式!
感謝你的閱讀,如果你有任何問題或建議,歡迎在下方留言討論。我們下次見!


上一篇
D21 - 實作分類管理:建立分類列表與新增/編輯頁面
下一篇
D23 - 獨自開發的省思:技術深度與開發效率的平衡
系列文
我獨自開發 - 30天進化之路,掌握 Laravel + Nuxt30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言