feat: 小菜记账 v1.0 - 完整功能实现

核心功能:
- 记账CRUD(支出/收入/分类/备注/日期)
- 统计分析(概览/分类占比/每日趋势)
- 预算管理(按月设置/进度条/超支提醒)
- 数据导出CSV

安全与认证:
- HMAC-SHA256签名token认证
- 用户数据隔离
- 输入验证与错误处理
- CORS配置

前端优化:
- 骨架屏加载
- 账单按日期分组
- 预算页面重构(快捷预设+Numpad)
- SvgIcon组件(H5+微信双端适配)
- 下拉刷新

后端优化:
- 共享日期工具函数
- 数据库连接池优化
- 健康检查端点
- 优雅关闭处理

技术栈:
- 前端:Uni-app (Vue 3 + Pinia)
- 后端:Node.js + Express + MySQL
This commit is contained in:
2026-05-29 16:14:15 +08:00
commit c7f29aa7a7
70 changed files with 32282 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { request } from '@/utils/request'
import { getCurrentMonth } from '@/utils/format'
export interface Budget {
id: number
amount: number
month: string
}
export const useBudgetStore = defineStore('budget', () => {
const budget = ref<Budget | null>(null)
const loading = ref(false)
async function fetchBudget(month?: string) {
loading.value = true
try {
budget.value = await request<Budget>({
url: '/budget',
data: { month: month || getCurrentMonth() }
})
} catch {
budget.value = null
} finally {
loading.value = false
}
}
async function setBudget(amount: number, month?: string) {
await request({
url: '/budget',
method: 'POST',
data: { amount, month: month || getCurrentMonth() }
})
await fetchBudget(month)
}
return { budget, loading, fetchBudget, setBudget }
})

View File

@@ -0,0 +1,54 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { request } from '@/utils/request'
export interface Category {
id: number
name: string
icon: string
color: string
type: 'expense' | 'income'
sort_order: number
is_custom: number
}
export const useCategoryStore = defineStore('category', () => {
const categories = ref<Category[]>([])
const loading = ref(false)
async function fetchCategories() {
loading.value = true
try {
categories.value = await request<Category[]>({ url: '/categories' })
} catch {
categories.value = []
} finally {
loading.value = false
}
}
function getByType(type: 'expense' | 'income') {
return categories.value.filter(c => c.type === type)
}
function getById(id: number) {
return categories.value.find(c => c.id === id)
}
async function addCategory(data: { name: string; icon: string; color: string; type: 'expense' | 'income' }) {
const result = await request<{ id: number }>({
url: '/categories',
method: 'POST',
data
})
await fetchCategories()
return result.id
}
async function deleteCategory(id: number) {
await request({ url: `/categories/${id}`, method: 'DELETE' })
await fetchCategories()
}
return { categories, loading, fetchCategories, getByType, getById, addCategory, deleteCategory }
})

View File

@@ -0,0 +1,66 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { request } from '@/utils/request'
import { getCurrentMonth } from '@/utils/format'
export interface Overview {
expense: number
income: number
count: number
daily: number
}
export interface CategoryStat {
id: number
name: string
icon: string
color: string
amount: number
count: number
}
export interface TrendPoint {
date: string
amount: number
}
export const useStatsStore = defineStore('stats', () => {
const overview = ref<Overview>({ expense: 0, income: 0, count: 0, daily: 0 })
const categoryStats = ref<CategoryStat[]>([])
const trendData = ref<TrendPoint[]>([])
async function fetchOverview(month?: string) {
try {
overview.value = await request<Overview>({
url: '/stats/overview',
data: { month: month || getCurrentMonth() }
})
} catch {
overview.value = { expense: 0, income: 0, count: 0, daily: 0 }
}
}
async function fetchCategoryStats(month?: string, type: string = 'expense') {
try {
categoryStats.value = await request<CategoryStat[]>({
url: '/stats/category',
data: { month: month || getCurrentMonth(), type }
})
} catch {
categoryStats.value = []
}
}
async function fetchTrend(month?: string, type: string = 'expense') {
try {
trendData.value = await request<TrendPoint[]>({
url: '/stats/trend',
data: { month: month || getCurrentMonth(), type }
})
} catch {
trendData.value = []
}
}
return { overview, categoryStats, trendData, fetchOverview, fetchCategoryStats, fetchTrend }
})

View File

@@ -0,0 +1,74 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { request } from '@/utils/request'
export interface Transaction {
id: number
amount: number
type: 'expense' | 'income'
category_id: number
note?: string
date: string
category_name?: string
category_icon?: string
category_color?: string
}
export interface TransactionList {
list: Transaction[]
total: number
page: number
pageSize: number
}
export const useTransactionStore = defineStore('transaction', () => {
const transactions = ref<Transaction[]>([])
const total = ref(0)
const loading = ref(false)
const currentPage = ref(1)
async function fetchTransactions(params?: { type?: string; startDate?: string; endDate?: string; page?: number; pageSize?: number }) {
loading.value = true
try {
const data = await request<TransactionList>({
url: '/transactions',
data: params
})
transactions.value = data.list
total.value = data.total
currentPage.value = data.page
} catch {
transactions.value = []
total.value = 0
} finally {
loading.value = false
}
}
async function addTransaction(data: { amount: number; type: string; category_id?: number; note?: string; date: string }) {
const result = await request<{ id: number }>({
url: '/transactions',
method: 'POST',
data
})
return result.id
}
async function updateTransaction(id: number, data: { amount: number; type: string; category_id?: number; note?: string; date: string }) {
await request({ url: `/transactions/${id}`, method: 'PUT', data })
}
async function deleteTransaction(id: number) {
await request({ url: `/transactions/${id}`, method: 'DELETE' })
}
async function fetchTransactionById(id: number): Promise<Transaction | undefined> {
try {
return await request<Transaction>({ url: `/transactions/${id}` })
} catch {
return undefined
}
}
return { transactions, total, loading, currentPage, fetchTransactions, addTransaction, updateTransaction, deleteTransaction, fetchTransactionById }
})