Files
finance_app/resources/js/Pages/Expenses/Index.vue
henry yo 85972f950c feat: 前端分離+功能api化
1. 新增資產管理CRUD、googleOAuth登入
2. 更改原記帳controller
2026-06-25 14:19:07 +08:00

353 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { ref, reactive, onMounted, computed } from "vue";
import axios from "axios";
// 假設你已經將這些組件搬移到新專案的 components 資料夾
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
// --- 1. 資料狀態管理 (取代 Props) ---
const expenses = ref([]);
const categories = ref([]);
const rules = ref([]);
const loading = ref(false);
const flash = ref("");
// 取得初始資料
const fetchAllData = async () => {
loading.value = true;
try {
// 同步取得所有必要的資料
const [expRes, catRes, ruleRes] = await Promise.all([
axios.get("/api/expenses"),
axios.get("/api/categories"),
axios.get("/api/category-rules"),
]);
expenses.value = expRes.data;
categories.value = catRes.data;
rules.value = ruleRes.data;
} catch (error) {
console.error("載入失敗", error);
flash.value = "無法取得資料,請檢查網路連線";
} finally {
loading.value = false;
}
};
onMounted(fetchAllData);
// --- 2. 支出管理 (Expenses) ---
const form = reactive({
amount: "",
category_id: "",
note: "",
date: new Date().toISOString().split("T")[0],
});
const submit = async () => {
try {
const response = await axios.post("/api/expenses", form);
// 手動將新資料推入列表頂部
expenses.value.unshift(response.data);
// 重置表單
Object.assign(form, { amount: "", category_id: "", note: "" });
flash.value = "新增成功!";
} catch (error) {
alert("新增失敗");
}
};
const editing = ref(null);
const editForm = reactive({ amount: "", category_id: "", note: "", date: "" });
const startEdit = (expense) => {
editing.value = expense.id;
Object.assign(editForm, {
amount: expense.amount,
category_id: expense.category_id ?? "",
note: expense.note ?? "",
date: expense.date ? expense.date.split("T")[0] : "",
});
};
const submitEdit = async (id) => {
try {
const response = await axios.put(`/api/expenses/${id}`, editForm);
const index = expenses.value.findIndex((ex) => ex.id === id);
expenses.value[index] = response.data;
editing.value = null;
} catch (error) {
alert("修改失敗");
}
};
const destroy = async (id) => {
if (!confirm("確定刪除?")) return;
try {
await axios.delete(`/api/expenses/${id}`);
expenses.value = expenses.value.filter((ex) => ex.id !== id);
} catch (error) {
alert("刪除失敗");
}
};
// --- 3. 分類管理 (Categories) ---
const showCategories = ref(false);
const categoryForm = reactive({ name: "", color: "#3B82F6" });
const editingCategory = ref(null);
const editCategoryForm = reactive({ name: "", color: "" });
const submitCategory = async () => {
try {
const response = await axios.post("/api/categories", categoryForm);
categories.value.push(response.data);
Object.assign(categoryForm, { name: "", color: "#3B82F6" });
} catch (error) {
alert("分類新增失敗");
}
};
const submitEditCategory = async (id) => {
try {
const response = await axios.put(`/api/categories/${id}`, editCategoryForm);
const index = categories.value.findIndex((cat) => cat.id === id);
categories.value[index] = response.data;
editingCategory.value = null;
} catch (error) {
alert("分類修改失敗");
}
};
// --- 4. 規則管理 (Rules) ---
const showRules = ref(false);
const ruleForm = reactive({ keyword: "", category_id: "" });
const submitRule = async () => {
try {
const response = await axios.post("/api/category-rules", ruleForm);
rules.value.push(response.data);
Object.assign(ruleForm, { keyword: "", category_id: "" });
} catch (error) {
alert("規則新增失敗");
}
};
// --- 5. 批量操作 ---
const selected = ref([]);
const toggleAll = (e) => {
selected.value = e.target.checked ? expenses.value.map((ex) => ex.id) : [];
};
const deleteSelected = async () => {
if (selected.value.length === 0) return;
if (!confirm(`確定刪除 ${selected.value.length} 筆?`)) return;
try {
await axios.post("/api/expenses/destroy-batch", { ids: selected.value });
expenses.value = expenses.value.filter((ex) => !selected.value.includes(ex.id));
selected.value = [];
} catch (error) {
alert("批量刪除失敗");
}
};
</script>
<template>
<AuthenticatedLayout>
<!-- 修改 1: 使用本地 ref flash 訊息 -->
<div
v-if="flash"
class="bg-green-50 text-green-700 p-4 rounded mb-4"
>
{{ flash }}
</div>
<template #header>
<div class="flex justify-between items-center">
<h2 class="text-xl font-semibold">記帳</h2>
</div>
</template>
<div class="py-8 max-w-4xl mx-auto px-4 space-y-6">
<!-- 分類管理 -->
<div class="bg-white rounded-lg shadow p-6">
<div
class="flex justify-between items-center cursor-pointer"
@click="showCategories = !showCategories"
>
<h3 class="text-lg font-medium">分類管理</h3>
<span class="text-gray-400 text-sm">
{{ showCategories ? "▲ 收起" : "▼ 展開" }}
</span>
</div>
<div v-if="showCategories" class="mt-4 space-y-4">
<form @submit.prevent="submitCategory" class="flex gap-3 items-end">
<div class="flex-1">
<label class="block text-sm text-gray-600 mb-1">名稱</label>
<input
v-model="categoryForm.name"
type="text"
class="w-full border rounded px-3 py-2"
placeholder="分類名稱"
required
/>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">顏色</label>
<input
v-model="categoryForm.color"
type="color"
class="h-10 w-16 border rounded cursor-pointer"
/>
</div>
<button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
新增
</button>
</form>
<div class="divide-y border rounded">
<div v-for="cat in categories" :key="cat.id" class="px-4 py-3">
<div v-if="editingCategory !== cat.id" class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="w-5 h-5 rounded-full" :style="{ background: cat.color }"></span>
<span>{{ cat.name }}</span>
</div>
<div class="space-x-3">
<button @click="startEditCategory(cat)" class="text-blue-400 hover:text-blue-600 text-sm">編輯</button>
<button @click="destroyCategory(cat.id)" class="text-red-400 hover:text-red-600 text-sm">刪除</button>
</div>
</div>
<div v-else class="flex items-center gap-3">
<input v-model="editCategoryForm.color" type="color" class="h-8 w-12 border rounded cursor-pointer" />
<input v-model="editCategoryForm.name" type="text" class="flex-1 border rounded px-3 py-1 text-sm" />
<button @click="submitEditCategory(cat.id)" class="text-green-500 hover:text-green-700 text-sm">儲存</button>
<button @click="editingCategory = null" class="text-gray-400 hover:text-gray-600 text-sm">取消</button>
</div>
</div>
</div>
</div>
</div>
<!-- 自動分類規則 -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex justify-between items-center cursor-pointer" @click="showRules = !showRules">
<h3 class="text-lg font-medium">自動分類規則</h3>
<span class="text-gray-400 text-sm">{{ showRules ? "▲ 收起" : "▼ 展開" }}</span>
</div>
<div v-if="showRules" class="mt-4 space-y-4">
<form @submit.prevent="submitRule" class="flex gap-3 items-end">
<div class="flex-1">
<label class="block text-sm text-gray-600 mb-1">關鍵字</label>
<input v-model="ruleForm.keyword" type="text" class="w-full border rounded px-3 py-2" placeholder="例如:全家" required />
</div>
<div class="flex-1">
<label class="block text-sm text-gray-600 mb-1">對應分類</label>
<select v-model="ruleForm.category_id" class="w-full border rounded px-3 py-2" required>
<option value="">選擇分類</option>
<option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
</select>
</div>
<button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">新增</button>
</form>
<!-- 規則列表部分邏輯不變僅確保資料來自 ref -->
</div>
</div>
<!-- 新增支出 -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium">新增支出</h3>
<!-- 修改 2: 改為 router-link -->
<router-link
to="/expenses/import"
class="bg-blue-500 text-white px-4 py-2 rounded text-sm hover:bg-blue-600"
>
匯入發票
</router-link>
</div>
<form @submit.prevent="submit" class="grid grid-cols-2 gap-4">
<!-- 表單內容保持不變 -->
<div>
<label class="block text-sm text-gray-600 mb-1">金額</label>
<input v-model="form.amount" type="number" step="1" class="w-full border rounded px-3 py-2" placeholder="0" required />
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">日期</label>
<input v-model="form.date" type="date" class="w-full border rounded px-3 py-2" required />
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">分類</label>
<select v-model="form.category_id" class="w-full border rounded px-3 py-2">
<option value="">未分類</option>
<option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">備註</label>
<input v-model="form.note" type="text" class="w-full border rounded px-3 py-2" placeholder="備註..." />
</div>
<div class="col-span-2">
<button type="submit" class="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600">
新增
</button>
</div>
</form>
</div>
<!-- 支出列表 -->
<div class="bg-white rounded-lg shadow overflow-x-auto">
<!-- 列表內容與原版相似但確保所有操作都綁定到剛才改寫的 axios 函式 -->
<table class="w-full">
<thead class="bg-gray-50 text-sm text-gray-600">
<tr>
<th class="px-4 py-3"><input type="checkbox" @change="toggleAll" /></th>
<th class="px-4 py-3 text-left">日期</th>
<th class="px-4 py-3 text-left">分類</th>
<th class="px-4 py-3 text-left">備註</th>
<th class="px-4 py-3 text-right">金額</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<template v-for="expense in expenses" :key="expense.id">
<tr v-if="editing !== expense.id" class="hover:bg-gray-50">
<td class="px-4 py-3">
<input type="checkbox" :value="expense.id" v-model="selected" />
</td>
<td class="px-4 py-3 text-sm">{{ formatDate(expense.date) }}</td>
<td class="px-4 py-3 text-sm">
<span v-if="expense.category" class="px-2 py-1 rounded text-xs text-white" :style="{ background: expense.category.color }">
{{ expense.category.name }}
</span>
<span v-else class="text-gray-400 text-xs">未分類</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600">{{ expense.note }}</td>
<td class="px-4 py-3 text-right font-medium">${{ Number(expense.amount).toLocaleString() }}</td>
<td class="px-4 py-3 text-right space-x-2">
<button @click="startEdit(expense)" class="text-blue-400 hover:text-blue-600 text-sm">編輯</button>
<button @click="destroy(expense.id)" class="text-red-400 hover:text-red-600 text-sm">刪除</button>
</td>
</tr>
<tr v-else class="bg-blue-50">
<!-- 編輯狀態的 Input 保持與 editForm 綁定 -->
<td class="px-4 py-2"><input v-model="editForm.date" type="date" class="w-full border rounded px-2 py-1 text-sm" /></td>
<td class="px-4 py-2">
<select v-model="editForm.category_id" class="w-full border rounded px-2 py-1 text-sm">
<option value="">未分類</option>
<option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
</select>
</td>
<td class="px-4 py-2"><input v-model="editForm.note" type="text" class="w-full border rounded px-2 py-1 text-sm" /></td>
<td class="px-4 py-2"><input v-model="editForm.amount" type="number" class="w-full border rounded px-2 py-1 text-sm" /></td>
<td class="px-4 py-2 text-right space-x-2">
<button @click="submitEdit(expense.id)" class="text-green-500 hover:text-green-700 text-sm">儲存</button>
<button @click="editing = null" class="text-gray-400 hover:text-gray-600 text-sm">取消</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</AuthenticatedLayout>
</template>