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:
40
client/src/stores/budget.ts
Normal file
40
client/src/stores/budget.ts
Normal 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 }
|
||||
})
|
||||
54
client/src/stores/category.ts
Normal file
54
client/src/stores/category.ts
Normal 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 }
|
||||
})
|
||||
66
client/src/stores/stats.ts
Normal file
66
client/src/stores/stats.ts
Normal 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 }
|
||||
})
|
||||
74
client/src/stores/transaction.ts
Normal file
74
client/src/stores/transaction.ts
Normal 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 }
|
||||
})
|
||||
Reference in New Issue
Block a user