feat: 前端分離+功能api化

1. 新增資產管理CRUD、googleOAuth登入
2. 更改原記帳controller
This commit is contained in:
2026-06-25 14:19:07 +08:00
parent 7bb931c97b
commit 85972f950c
70 changed files with 4754 additions and 1520 deletions

View File

@@ -1,6 +1,7 @@
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import { Pie, Bar } from 'vue-chartjs'
import { computed } from "vue"; // 必須匯入 computed
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Pie, Bar } from "vue-chartjs";
import {
Chart as ChartJS,
ArcElement,
@@ -9,114 +10,128 @@ import {
CategoryScale,
LinearScale,
BarElement,
} from 'chart.js'
} from "chart.js";
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
ChartJS.register(
ArcElement,
Tooltip,
Legend,
CategoryScale,
LinearScale,
BarElement,
);
const props = defineProps({
monthlyTotal: Number,
categoryStats: Array,
monthlyTrend: Array,
})
// 建議加上預設值,防止 API 還沒回傳時崩潰
// const props = defineProps({
// monthlyTotal: { type: Number, default: 0 },
// categoryStats: { type: Array, default: () => [] },
// monthlyTrend: { type: Array, default: () => [] },
// });
// 圓餅圖
const pieData = {
labels: props.categoryStats.map(c => c.name),
datasets: [{
data: props.categoryStats.map(c => c.total),
backgroundColor: props.categoryStats.map(c => c.color),
}],
}
// --- 修改重點:使用 computed 並加上空陣列保護 ---
const pieOptions = {
responsive: true,
plugins: {
legend: { position: 'bottom' },
},
}
// 圓餅圖資料
// const pieData = computed(() => ({
// labels: (props.categoryStats || []).map((c) => c.name),
// datasets: [
// {
// data: (props.categoryStats || []).map((c) => c.total),
// backgroundColor: (props.categoryStats || []).map((c) => c.color),
// },
// ],
// }));
// 長條圖
const barData = {
labels: props.monthlyTrend.map(m => m.month),
datasets: [{
label: '每月花費',
data: props.monthlyTrend.map(m => m.total),
backgroundColor: '#3B82F6',
borderRadius: 4,
}],
}
// const pieOptions = {
// responsive: true,
// plugins: {
// legend: { position: "bottom" },
// },
// };
const barOptions = {
responsive: true,
plugins: {
legend: { display: false },
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: (val) => '$' + val.toLocaleString(),
},
},
},
}
// 長條圖資料
// const barData = computed(() => ({
// labels: (props.monthlyTrend || []).map((m) => m.month),
// datasets: [
// {
// label: "每月花費",
// data: (props.monthlyTrend || []).map((m) => m.total),
// backgroundColor: "#3B82F6",
// borderRadius: 4,
// },
// ],
// }));
// const barOptions = {
// responsive: true,
// plugins: {
// legend: { display: false },
// },
// scales: {
// y: {
// beginAtZero: true,
// ticks: {
// callback: (val) => "$" + val.toLocaleString(),
// },
// },
// },
// };
</script>
<template>
<AuthenticatedLayout>
<template #header>
<h2 class="text-xl font-semibold">總覽</h2>
</template>
<!-- <AuthenticatedLayout> -->
<template #header>
<h2 class="text-xl font-semibold">總覽</h2>
</template>
<div class="py-8 max-w-5xl mx-auto px-4 space-y-6">
<!-- 本月花費 -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="bg-white rounded-lg shadow p-6">
<p class="text-sm text-gray-500">本月花費</p>
<p class="text-3xl font-bold text-blue-500 mt-1">
${{ monthlyTotal.toLocaleString() }}
</p>
</div>
<div class="bg-white rounded-lg shadow p-6">
<p class="text-sm text-gray-500">本月筆數</p>
<p class="text-3xl font-bold text-gray-700 mt-1">
{{ categoryStats.reduce((s, c) => s, 0) }}
{{ categoryStats.length }} 個分類
</p>
</div>
<div class="bg-white rounded-lg shadow p-6">
<p class="text-sm text-gray-500">最高花費分類</p>
<p class="text-3xl font-bold text-gray-700 mt-1">
{{ categoryStats.length > 0
? categoryStats.reduce((a, b) => a.total > b.total ? a : b).name
: '—' }}
</p>
</div>
<div class="py-8 max-w-5xl mx-auto px-4 space-y-6">
<!-- 本月花費 -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="bg-white rounded-lg shadow p-6">
<p class="text-sm text-gray-500">本月花費</p>
<!-- <p class="text-3xl font-bold text-blue-500 mt-1">
${{ monthlyTotal.toLocaleString() }}
</p> -->
</div>
<!-- 圖表 -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<!-- 圓餅圖 -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium mb-4">本月分類佔比</h3>
<div v-if="categoryStats.length > 0" class="max-w-xs mx-auto">
<Pie :data="pieData" :options="pieOptions" />
</div>
<div v-else class="py-12 text-center text-gray-400">
本月還沒有記錄
</div>
</div>
<!-- 長條圖 -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium mb-4">近6個月趨勢</h3>
<Bar :data="barData" :options="barOptions" />
</div>
<div class="bg-white rounded-lg shadow p-6">
<p class="text-sm text-gray-500">本月筆數</p>
<!-- <p class="text-3xl font-bold text-gray-700 mt-1">
{{ categoryStats.reduce((s, c) => s, 0) }}
{{ categoryStats.length }} 個分類
</p> -->
</div>
<div class="bg-white rounded-lg shadow p-6">
<p class="text-sm text-gray-500">最高花費分類</p>
<!-- <p class="text-3xl font-bold text-gray-700 mt-1">
{{
categoryStats.length > 0
? categoryStats.reduce((a, b) =>
a.total > b.total ? a : b,
).name
: "—"
}}
</p> -->
</div>
</div>
</AuthenticatedLayout>
</template>
<!-- 圖表 -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<!-- 圓餅圖 -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium mb-4">本月分類佔比</h3>
<div v-if="categoryStats.length > 0" class="max-w-xs mx-auto">
<!-- <Pie :data="pieData" :options="pieOptions" /> -->
</div>
<div v-else class="py-12 text-center text-gray-400">
本月還沒有記錄
</div>
</div>
<!-- 長條圖 -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium mb-4">近6個月趨勢</h3>
<!-- <Bar :data="barData" :options="barOptions" /> -->
</div>
</div>
</div>
<!-- </AuthenticatedLayout> -->
</template>