mirror of
https://github.com/henry4682/finance_app.git
synced 2026-07-16 08:20:00 +00:00
353 lines
16 KiB
Vue
353 lines
16 KiB
Vue
<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>
|