buid new app

This commit is contained in:
2026-03-09 01:00:49 +08:00
commit 97aed742af
126 changed files with 19341 additions and 0 deletions

View 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>