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:
90
client/src/utils/request.ts
Normal file
90
client/src/utils/request.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user