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,90 @@
// API 基础地址 - 上线后改成真实域名
let BASE_URL = 'http://106.14.208.43:3000/api'
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
// 微信小程序环境使用 HTTPS 域名(上线后配置)
// BASE_URL = 'https://your-domain.com/api'
}
interface RequestOptions {
url: string
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
data?: any
}
let isRedirectingToLogin = false
export function request<T = any>(options: RequestOptions): Promise<T> {
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('xc:token')
uni.request({
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data,
header: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {})
},
success: (res: any) => {
const { statusCode, data } = res
// HTTP 401 - token invalid or expired
if (statusCode === 401) {
uni.removeStorageSync('xc:token')
uni.removeStorageSync('xc:userId')
if (!isRedirectingToLogin) {
isRedirectingToLogin = true
uni.showToast({ title: '请重新登录', icon: 'none' })
setTimeout(() => {
// #ifdef H5
window.location.reload()
// #endif
// #ifdef MP-WEIXIN
// 重新触发 wx.login
reLogin()
// #endif
isRedirectingToLogin = false
}, 1500)
}
reject({ code: 40100, message: '未登录' })
return
}
// Business logic success
if (data.code === 0) {
resolve(data.data)
} else {
uni.showToast({ title: data.message || '请求失败', icon: 'none' })
reject(data)
}
},
fail: (err: any) => {
uni.showToast({ title: '网络错误', icon: 'none' })
reject(err)
}
})
})
}
// 微信小程序自动重新登录
function reLogin() {
// #ifdef MP-WEIXIN
uni.login({
provider: 'weixin',
success: async (loginRes) => {
if (loginRes.code) {
try {
const data = await request<{ token: string; userId: number }>({
url: '/auth/login',
method: 'POST',
data: { code: loginRes.code }
})
uni.setStorageSync('xc:token', data.token)
uni.setStorageSync('xc:userId', data.userId)
} catch (e) {
console.error('[Auth] Re-login failed:', e)
}
}
}
})
// #endif
}