mirror of
https://github.com/henry4682/finance_app.git
synced 2026-07-16 00:10:00 +00:00
buid new app
This commit is contained in:
581
resources/js/Pages/Expenses/Index.vue
Normal file
581
resources/js/Pages/Expenses/Index.vue
Normal file
@@ -0,0 +1,581 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useForm, usePage } from "@inertiajs/vue3";
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
|
||||
|
||||
const props = defineProps({
|
||||
expenses: Array,
|
||||
categories: Array,
|
||||
rules: Array,
|
||||
});
|
||||
// 規則管理
|
||||
const showRules = ref(false);
|
||||
|
||||
const ruleForm = useForm({
|
||||
keyword: "",
|
||||
category_id: "",
|
||||
});
|
||||
const flash = computed(() => usePage().props.flash);
|
||||
// ── 分類管理 ──
|
||||
const showCategories = ref(false);
|
||||
|
||||
const categoryForm = useForm({
|
||||
name: "",
|
||||
color: "#3B82F6",
|
||||
});
|
||||
|
||||
const submitCategory = () => {
|
||||
categoryForm.post(route("categories.store"), {
|
||||
onSuccess: () => categoryForm.reset(),
|
||||
});
|
||||
};
|
||||
|
||||
const submitRule = () => {
|
||||
ruleForm.post(route("category-rules.store"), {
|
||||
onSuccess: () => ruleForm.reset(),
|
||||
});
|
||||
};
|
||||
|
||||
const destroyRule = (id) => {
|
||||
if (confirm("確定刪除此規則?")) {
|
||||
useForm({}).delete(route("category-rules.destroy", id));
|
||||
}
|
||||
};
|
||||
|
||||
const editingCategory = ref(null);
|
||||
const editCategoryForm = useForm({ name: "", color: "" });
|
||||
|
||||
const startEditCategory = (cat) => {
|
||||
editingCategory.value = cat.id;
|
||||
editCategoryForm.name = cat.name;
|
||||
editCategoryForm.color = cat.color;
|
||||
};
|
||||
|
||||
const submitEditCategory = (id) => {
|
||||
editCategoryForm.put(route("categories.update", id), {
|
||||
onSuccess: () => (editingCategory.value = null),
|
||||
});
|
||||
};
|
||||
|
||||
const destroyCategory = (id) => {
|
||||
if (confirm("確定刪除?該分類的支出會變成未分類。")) {
|
||||
useForm({}).delete(route("categories.destroy", id));
|
||||
}
|
||||
};
|
||||
|
||||
// ── 支出 ──
|
||||
const form = useForm({
|
||||
amount: "",
|
||||
category_id: "",
|
||||
note: "",
|
||||
date: new Date().toISOString().split("T")[0],
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route("expenses.store"), {
|
||||
onSuccess: () => form.reset(),
|
||||
});
|
||||
};
|
||||
|
||||
const formatDate = (date) => (date ? date.toString().split("T")[0] : "");
|
||||
|
||||
const editing = ref(null);
|
||||
const editForm = useForm({
|
||||
amount: "",
|
||||
category_id: "",
|
||||
note: "",
|
||||
date: "",
|
||||
});
|
||||
|
||||
const startEdit = (expense) => {
|
||||
editing.value = expense.id;
|
||||
editForm.amount = expense.amount;
|
||||
editForm.category_id = expense.category_id ?? "";
|
||||
editForm.note = expense.note ?? "";
|
||||
editForm.date = formatDate(expense.date);
|
||||
};
|
||||
|
||||
const submitEdit = (id) => {
|
||||
editForm.put(route("expenses.update", id), {
|
||||
onSuccess: () => (editing.value = null),
|
||||
});
|
||||
};
|
||||
|
||||
const destroy = (id) => {
|
||||
if (confirm("確定刪除?")) {
|
||||
useForm({}).delete(route("expenses.destroy", id));
|
||||
}
|
||||
};
|
||||
|
||||
const selected = ref([]);
|
||||
|
||||
const toggleAll = (e) => {
|
||||
selected.value = e.target.checked ? props.expenses.map((ex) => ex.id) : [];
|
||||
};
|
||||
|
||||
const deleteSelected = () => {
|
||||
if (selected.value.length === 0) return;
|
||||
if (!confirm(`確定刪除 ${selected.value.length} 筆?`)) return;
|
||||
useForm({ ids: selected.value }).delete(route("expenses.destroyBatch"), {
|
||||
onSuccess: () => (selected.value = []),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticatedLayout>
|
||||
<div
|
||||
v-if="flash?.message"
|
||||
class="bg-green-50 text-green-700 p-4 rounded mb-4"
|
||||
>
|
||||
{{ flash.message }}
|
||||
</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
|
||||
v-if="categories.length === 0"
|
||||
class="px-4 py-6 text-center text-gray-400 text-sm"
|
||||
>
|
||||
還沒有分類
|
||||
</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>
|
||||
|
||||
<!-- 規則列表 -->
|
||||
<div class="divide-y border rounded">
|
||||
<div
|
||||
v-for="rule in rules"
|
||||
:key="rule.id"
|
||||
class="px-4 py-3 flex items-center justify-between"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<span
|
||||
class="font-mono bg-gray-100 px-2 py-1 rounded text-sm"
|
||||
>
|
||||
{{ rule.keyword }}
|
||||
</span>
|
||||
<span class="text-gray-400">→</span>
|
||||
<span
|
||||
class="px-2 py-1 rounded text-xs text-white"
|
||||
:style="{
|
||||
background: rule.category?.color,
|
||||
}"
|
||||
>
|
||||
{{ rule.category?.name }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
@click="destroyRule(rule.id)"
|
||||
class="text-red-400 hover:text-red-600 text-sm"
|
||||
>
|
||||
刪除
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="rules.length === 0"
|
||||
class="px-4 py-6 text-center text-gray-400 text-sm"
|
||||
>
|
||||
還沒有規則,匯入發票時分類將會是空白
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<a
|
||||
:href="route('expenses.import')"
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded text-sm hover:bg-blue-600"
|
||||
>
|
||||
匯入發票
|
||||
</a>
|
||||
</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">
|
||||
<div
|
||||
class="px-4 py-3 border-b flex items-center justify-between"
|
||||
>
|
||||
<span class="text-sm text-gray-500">
|
||||
共 {{ expenses.length }} 筆
|
||||
</span>
|
||||
<button
|
||||
@click="deleteSelected"
|
||||
:class="
|
||||
selected.length > 0
|
||||
? 'opacity-100'
|
||||
: 'opacity-0 pointer-events-none'
|
||||
"
|
||||
class="bg-red-500 text-white px-3 py-1 rounded text-sm hover:bg-red-600 transition-opacity"
|
||||
>
|
||||
刪除選取 ({{ selected.length }})
|
||||
</button>
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
<tr v-if="expenses.length === 0">
|
||||
<td
|
||||
colspan="5"
|
||||
class="px-4 py-8 text-center text-gray-400"
|
||||
>
|
||||
還沒有記錄
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
Reference in New Issue
Block a user