commit 1a6280c998326b4a33e2a557d2687a889b1dd27f Author: wangsai <1534599297@qq.com> Date: Wed Sep 24 10:23:37 2025 +0800 初始提交 diff --git a/app.js b/app.js new file mode 100644 index 0000000..7c72f39 --- /dev/null +++ b/app.js @@ -0,0 +1,43 @@ +App({ + onLaunch() { + this.checkLoginStatus() + // 初始化本地存储 + if (!wx.getStorageSync('records')) { + wx.setStorageSync('records', []); + } + + const defaultCategories = [ + { id: 1, name: '餐饮', type: 'out', icon: 'cutlery' }, + { id: 2, name: '交通', type: 'out', icon: 'car' }, + { id: 3, name: '购物', type: 'out', icon: 'shopping-cart' }, + { id: 4, name: '娱乐', type: 'out', icon: 'film' }, + { id: 5, name: '工资', type: 'in', icon: 'money' }, + { id: 6, name: '奖金', type: 'in', icon: 'gift' } + ]; + wx.setStorageSync('categories', defaultCategories); + }, + // 检查登录状态 + checkLoginStatus() { + // 从本地存储获取token + const token = wx.getStorageSync('token') + // const tokenExpire = wx.getStorageSync('tokenExpire') // 过期时间(可选) + + // // 检查token是否存在且未过期 + // const isLogin = token && (!tokenExpire || Date.now() < tokenExpire) + + if (token) { + // 已登录,跳转首页 + wx.switchTab({ + url: '/pages/index/index' // 首页路径 + }) + } else { + // 未登录或token过期,跳转登录页 + wx.redirectTo({ + url: '/pages/login/login' // 登录页路径 + }) + } + }, + globalData: { + userInfo: null + } +}) diff --git a/app.json b/app.json new file mode 100644 index 0000000..fdca928 --- /dev/null +++ b/app.json @@ -0,0 +1,49 @@ +{ + "pages": [ + "pages/login/login", + "pages/index/index", + "pages/add/add", + "pages/statistics/statistics", + "pages/category/category", + "pages/profile/profile" + ], + "window": { + "backgroundTextStyle": "light", + "navigationBarBackgroundColor": "#4CAF50", + "navigationBarTitleText": "记账账啊", + "navigationBarTextStyle": "white" + }, + "tabBar": { + "color": "#7A7E83", + "selectedColor": "#4CAF50", + "borderStyle": "black", + "backgroundColor": "#ffffff", + "list": [ + { + "pagePath": "pages/index/index", + "text": "首页", + "iconPath": "/images/icon/home.png", + "selectedIconPath": "/images/icon/home-selected.png" + }, + { + "pagePath": "pages/add/add", + "text": "记账", + "iconPath": "/images/icon/add.png", + "selectedIconPath": "/images/icon/add-selected.png" + }, + { + "pagePath": "pages/statistics/statistics", + "text": "统计", + "iconPath": "/images/icon/chart.png", + "selectedIconPath": "/images/icon/chart-selected.png" + }, + { + "pagePath": "pages/profile/profile", + "text": "我的", + "iconPath": "/images/icon/user.png", + "selectedIconPath": "/images/icon/user-selected.png" + } + ] + }, + "sitemapLocation": "sitemap.json" +} diff --git a/images/app-logo.png b/images/app-logo.png new file mode 100644 index 0000000..bacb4bd Binary files /dev/null and b/images/app-logo.png differ diff --git a/images/icon/add-selected.png b/images/icon/add-selected.png new file mode 100644 index 0000000..7288211 Binary files /dev/null and b/images/icon/add-selected.png differ diff --git a/images/icon/add-selected2.png b/images/icon/add-selected2.png new file mode 100644 index 0000000..9352905 Binary files /dev/null and b/images/icon/add-selected2.png differ diff --git a/images/icon/add.png b/images/icon/add.png new file mode 100644 index 0000000..7288211 Binary files /dev/null and b/images/icon/add.png differ diff --git a/images/icon/add1.png b/images/icon/add1.png new file mode 100644 index 0000000..9352905 Binary files /dev/null and b/images/icon/add1.png differ diff --git a/images/icon/char1t.png b/images/icon/char1t.png new file mode 100644 index 0000000..bacb4bd Binary files /dev/null and b/images/icon/char1t.png differ diff --git a/images/icon/chart-selected.png b/images/icon/chart-selected.png new file mode 100644 index 0000000..9e89c5e Binary files /dev/null and b/images/icon/chart-selected.png differ diff --git a/images/icon/chart-selected11.png b/images/icon/chart-selected11.png new file mode 100644 index 0000000..bacb4bd Binary files /dev/null and b/images/icon/chart-selected11.png differ diff --git a/images/icon/chart.png b/images/icon/chart.png new file mode 100644 index 0000000..9e89c5e Binary files /dev/null and b/images/icon/chart.png differ diff --git a/images/icon/home-selected.png b/images/icon/home-selected.png new file mode 100644 index 0000000..0bfb67d Binary files /dev/null and b/images/icon/home-selected.png differ diff --git a/images/icon/home-selected2.png b/images/icon/home-selected2.png new file mode 100644 index 0000000..bacb4bd Binary files /dev/null and b/images/icon/home-selected2.png differ diff --git a/images/icon/home.png b/images/icon/home.png new file mode 100644 index 0000000..0bfb67d Binary files /dev/null and b/images/icon/home.png differ diff --git a/images/icon/home1.png b/images/icon/home1.png new file mode 100644 index 0000000..bacb4bd Binary files /dev/null and b/images/icon/home1.png differ diff --git a/images/icon/user-selected.png b/images/icon/user-selected.png new file mode 100644 index 0000000..3d40a39 Binary files /dev/null and b/images/icon/user-selected.png differ diff --git a/images/icon/user-selected1.png b/images/icon/user-selected1.png new file mode 100644 index 0000000..bacb4bd Binary files /dev/null and b/images/icon/user-selected1.png differ diff --git a/images/icon/user.png b/images/icon/user.png new file mode 100644 index 0000000..3d40a39 Binary files /dev/null and b/images/icon/user.png differ diff --git a/images/icon/user1.png b/images/icon/user1.png new file mode 100644 index 0000000..bacb4bd Binary files /dev/null and b/images/icon/user1.png differ diff --git a/images/wechat.png b/images/wechat.png new file mode 100644 index 0000000..bacb4bd Binary files /dev/null and b/images/wechat.png differ diff --git a/pages/add/add.js b/pages/add/add.js new file mode 100644 index 0000000..6b83d36 --- /dev/null +++ b/pages/add/add.js @@ -0,0 +1,215 @@ +const request = require('../utils/request') +Page({ + data: { + type: 'out', // 默认为支出 + amount: '', + selectedCategoryId: null, + categories: [], + filteredCategories: [], + note: '', + useDate: '', + maxDate: '', +// 控制弹窗显示 +showPopup: false, + +// 一级分类数据 +firstLevelCategories: [ + +], + +// 二级分类数据 +secondLevelCategories: [ + +], + +// 选中状态 +selectedFirstLevelId: null, +selectedSecondLevelId: null, +selectedCategoryName: '', +filteredSecondLevelCategories: [] + }, +// 显示分类选择弹窗 +showCategoryPopup() { + this.setData({ + showPopup: true + }); +}, + +// 隐藏分类选择弹窗 +hideCategoryPopup() { + this.setData({ + showPopup: false + }); +}, + +// 选择一级分类 +selectFirstLevel(e) { + const parentId = e.currentTarget.dataset.id; + + // 筛选对应的二级分类 + const filteredSeconds = this.data.secondLevelCategories.filter( + item => item.parentId == parentId + ); + + this.setData({ + selectedFirstLevelId: parentId, + filteredSecondLevelCategories: filteredSeconds, + // 重置二级分类选中状态 + selectedSecondLevelId: null + }); +}, + +// 选择二级分类 +selectSecondLevel(e) { + const secondLevelId = e.currentTarget.dataset.id; + const secondLevelName = e.currentTarget.dataset.name; + + // 如果点击的是已选中的,则取消选中 + if (this.data.selectedSecondLevelId === secondLevelId) { + this.setData({ + selectedSecondLevelId: null + }); + } else { + this.setData({ + selectedSecondLevelId: secondLevelId, + selectedCategoryName: secondLevelName + }); + } +}, + +// 确认选择 +confirmSelection() { + if (this.data.selectedSecondLevelId) { + // 这里可以处理选中后的逻辑 + console.log('选中的二级分类ID:', this.data.selectedSecondLevelId); + console.log('选中的二级分类名称:', this.data.selectedCategoryName); + + // 关闭弹窗 + this.hideCategoryPopup(); + } +}, +async getClassification() { + const res=await request.get('/admin-api/book/classification/oneTwoLevelList',{}) + console.log('resss:',res) + if(res.code==0){ + this.setData( + { + firstLevelCategories:res.data.outOne, + secondLevelCategories:res.data.outTwo, + categories:res.data.inOne, + filteredCategories: res.data.inOne + } + ) + } +}, +onShow(){ + this.getClassification() +}, + onLoad() { + // 获取当前日期 + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + const currentDate = `${year}-${month}-${day}`; + + this.setData({ + useDate: currentDate, + maxDate: currentDate + }); + + + }, + + setType(e) { + const type = e.currentTarget.dataset.type; + this.setData({ + type, + selectedCategoryId: null + }); + console.log('type:',type) + }, + + onAmountChange(e) { + let amount = e.detail.value; + + // 处理金额输入格式 + if (amount) { + // 只保留数字和一个小数点 + amount = amount.replace(/[^\d.]/g, ''); + // 确保只有一个小数点 + const dotIndex = amount.indexOf('.'); + if (dotIndex !== -1) { + amount = amount.substring(0, dotIndex + 3); // 限制两位小数 + } + } + + this.setData({ amount }); + }, + + selectCategory(e) { + const categoryId = e.currentTarget.dataset.id; + this.setData({ selectedCategoryId: categoryId }); + }, + + onNoteChange(e) { + this.setData({ note: e.detail.value }); + }, + + onDateChange(e) { + this.setData({ useDate: e.detail.value }); + }, + + async saveRecord() { + if (!this.data.amount ) { + wx.showToast({ + title: '请填写金额和选择分类', + icon: 'none', + duration: 2000 + }); + return; + } + + + + // 创建新记录 + const newRecord = { + type: this.data.type, + money: parseFloat(parseFloat(this.data.amount).toFixed(2)), + classId: this.data.type=='out'?this.data.selectedSecondLevelId:this.data.selectedCategoryId, + + remark: this.data.note, + useDate: this.data.useDate + }; + // 自动携带token + const res = await request.post('/admin-api/book/inout/create',newRecord) + console.log("新增结果:",res) + if(res.code==0){ + // 显示成功提示 + wx.showToast({ + title: '记录保存成功', + icon: 'success', + duration: 1500 + }); + this.setData({ + amount: '', + selectedCategoryId: '', + note: '', + selectedSecondLevelId: null, + }); + console.log('this.data:',this.data) + }else{ + wx.showToast({ + title: '记录保存失败', + icon: 'none', + duration: 2000 + }); + } + // 返回首页 + setTimeout(() => { + wx.navigateBack({ + delta: 1 + }); + }, 1500); + } +}) diff --git a/pages/add/add.wxml b/pages/add/add.wxml new file mode 100644 index 0000000..03bd807 --- /dev/null +++ b/pages/add/add.wxml @@ -0,0 +1,142 @@ + + + + + + 支出 + + + + 收入 + + + + + + ¥ + + + + + + 选择分类 + + + + {{item.classificationName}} + + + + + + + + 选择分类 + + {{selectedCategoryName || '请选择分类'}} + + + + + + + + + + + 选择分类 + + + + + + + + + {{item.classificationName}} + + + + + + + 请先选择一级分类 + + + + {{item.classificationName}} + + + + + + + + + + + + + + + + + + 备注 (可选) + + + + + + 日期 + + + + {{useDate}} + + + + + + + diff --git a/pages/add/add.wxss b/pages/add/add.wxss new file mode 100644 index 0000000..cb19886 --- /dev/null +++ b/pages/add/add.wxss @@ -0,0 +1,313 @@ +.container { + padding: 20px 15px; + background-color: #f5f5f5; + min-height: 100vh; +} + +.type-selector { + display: flex; + justify-content: center; + margin-bottom: 30px; +} + +.type-btn { + width: 120px; + height: 50px; + border-radius: 25px; + display: flex; + align-items: center; + justify-content: center; + margin: 0 10px; + font-size: 18px; + font-weight: 500; + border: 2px solid #ddd; + transition: all 0.3s; +} + +.type-btn.active { + border-color: #4CAF50; + color: #4CAF50; +} + +.type-btn .iconfont { + margin-right: 8px; + font-size: 20px; +} + +.amount-input { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 40px; +} + +.currency-symbol { + font-size: 36px; + color: #333; + margin-right: 10px; +} + +.amount-input input { + font-size: 48px; + text-align: left; + color: #333; + width: 250px; + padding: 0; + height: auto; +} + +.section-title { + display: block; + font-size: 16px; + color: #666; + margin-bottom: 15px; +} + +.category-grid { + display: flex; + flex-wrap: wrap; + gap: 15px; + margin-bottom: 30px; +} + +.category-item { + width: 70px; + height: 70px; + border-radius: 10px; + background-color: white; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 14px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); + transition: all 0.2s; +} + +.category-item .iconfont { + font-size: 24px; + margin-bottom: 5px; + color: #666; +} + +.category-item.selected { + background-color: #e8f5e9; + color: #4CAF50; +} + +.category-item.selected .iconfont { + color: #4CAF50; +} + +.note-section, .date-section { + margin-bottom: 30px; +} + +.note-section input, .date-picker { + width: 100%; + height: 50px; + background-color: white; + border-radius: 10px; + padding: 0 15px; + font-size: 16px; + box-sizing: border-box; +} + +.date-picker { + display: flex; + align-items: center; + color: #333; +} + +.date-picker .iconfont { + margin-right: 10px; + color: #999; +} + +.save-btn { + width: 100%; + height: 55px; + background-color: #4CAF50; + color: white; + border-radius: 27.5px; + font-size: 18px; + font-weight: 500; + line-height: 55px; + margin-top: 20px; +} + +.save-btn[disabled] { + background-color: #a5d6a7; + color: #e8f5e9; +} + +/************************/ +/* 触发区域样式 */ +.category-trigger { + margin-bottom: 30px; + + +} + +.note-section, .date-section { + margin-bottom: 30px; +} + +.section-title { + font-size: 34rpx; + font-weight: 600; + color: #333; + display: block; + margin-bottom: 20rpx; + padding-bottom: 15rpx; + border-bottom: 1px solid #f5f5f5; +} + +.selected-value { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20rpx; + background-color: #f9f9f9; + border-radius: 12rpx; + font-size: 28rpx; + color: #666; +} + +/* 遮罩层样式 */ +.category-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 998; +} + +/* 弹窗样式 */ +.category-popup { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: #fff; + border-top-left-radius: 24rpx; + border-top-right-radius: 24rpx; + z-index: 999; + max-height: 80vh; + display: flex; + flex-direction: column; +} + +/* 弹窗头部 */ +.popup-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 24rpx 30rpx; + border-bottom: 1px solid #f5f5f5; +} + +.popup-title { + font-size: 32rpx; + font-weight: 600; + color: #333; +} + +/* 弹窗内容区 */ +.popup-content { + display: flex; + flex: 1; + overflow: hidden; +} + +/* 一级分类列表 */ +.first-level-list { + width: 35%; + background-color: #f9f9f9; + overflow-y: auto; +} + +.first-level-item { + display: flex; + align-items: center; + padding: 28rpx 20rpx; + font-size: 28rpx; + color: #555; + border-left: 6rpx solid transparent; +} + +.first-level-item.selected { + background-color: #fff; + color: #387ef5; + border-left-color: #387ef5; + font-weight: 500; +} + +.first-level-item .iconfont { + font-size: 36rpx; + margin-right: 16rpx; + width: 40rpx; + text-align: center; +} + +/* 二级分类列表 */ +.second-level-list { + width: 65%; + padding: 20rpx; + overflow-y: auto; +} + +.no-selection { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + color: #999; + font-size: 28rpx; +} + +.second-level-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 22rpx 20rpx; + margin-bottom: 8rpx; + background-color: #f5f5f5; + border-radius: 12rpx; + font-size: 26rpx; + color: #555; +} + +.second-level-item.selected { + background-color: #edf4ff; + color: #387ef5; +} + +.second-level-item .icon-check { + font-size: 28rpx; +} + +/* 弹窗底部 */ +.popup-footer { + padding: 20rpx 30rpx; + border-top: 1px solid #f5f5f5; +} + +.confirm-btn { + width: 100%; + padding: 20rpx 0; + background-color: #387ef5; + color: #fff; + font-size: 30rpx; + border-radius: 12rpx; + line-height: normal; +} + +.confirm-btn:disabled { + background-color: #b3d1ff; + color: #e6f0ff; +} + + \ No newline at end of file diff --git a/pages/category/category.js b/pages/category/category.js new file mode 100644 index 0000000..f6fa8ed --- /dev/null +++ b/pages/category/category.js @@ -0,0 +1,157 @@ +Page({ + data: { + activeType: 'expense', + categories: [], + filteredCategories: [], + showModal: false, + isEditing: false, + editingId: null, + categoryName: '', + selectedIcon: '', + icons: [ + 'cutlery', 'car', 'shopping-cart', 'home', 'gift', 'money', + 'book', 'film', 'plane', 'medkit', 'coffee', 'shirt', + 'graduation-cap', 'briefcase', 'gamepad', 'heart', 'pet' + ] + }, + + onLoad() { + this.loadCategories(); + }, + + loadCategories() { + const categories = wx.getStorageSync('categories') || []; + this.setData({ + categories, + filteredCategories: categories.filter(cat => cat.type === this.data.activeType) + }); + }, + + setActiveType(e) { + const type = e.currentTarget.dataset.type; + this.setData({ + activeType: type, + filteredCategories: this.data.categories.filter(cat => cat.type === type) + }); + }, + + addCategory() { + this.setData({ + showModal: true, + isEditing: false, + editingId: null, + categoryName: '', + selectedIcon: this.data.icons[0] + }); + }, + + editCategory(e) { + const categoryId = e.currentTarget.dataset.id; + const category = this.data.categories.find(cat => cat.id === categoryId); + + if (category) { + this.setData({ + showModal: true, + isEditing: true, + editingId: categoryId, + categoryName: category.name, + selectedIcon: category.icon + }); + } + }, + + deleteCategory(e) { + const categoryId = e.currentTarget.dataset.id; + + wx.showModal({ + title: '确认删除', + content: '删除分类后,该分类下的记录将不受影响,是否继续?', + cancelText: '取消', + confirmText: '删除', + success: (res) => { + if (res.confirm) { + let categories = this.data.categories; + categories = categories.filter(cat => cat.id !== categoryId); + + wx.setStorageSync('categories', categories); + this.loadCategories(); + + wx.showToast({ + title: '分类已删除', + icon: 'success', + duration: 1500 + }); + } + } + }); + }, + + hideModal() { + this.setData({ showModal: false }); + }, + + onNameChange(e) { + this.setData({ categoryName: e.detail.value }); + }, + + selectIcon(e) { + const icon = e.currentTarget.dataset.icon; + this.setData({ selectedIcon: icon }); + }, + + saveCategory() { + const { categoryName, selectedIcon, isEditing, editingId, activeType, categories } = this.data; + + if (!categoryName.trim()) { + wx.showToast({ + title: '请输入分类名称', + icon: 'none', + duration: 2000 + }); + return; + } + + if (!selectedIcon) { + wx.showToast({ + title: '请选择图标', + icon: 'none', + duration: 2000 + }); + return; + } + + let updatedCategories = [...categories]; + + if (isEditing) { + // 编辑现有分类 + const index = updatedCategories.findIndex(cat => cat.id === editingId); + if (index !== -1) { + updatedCategories[index] = { + ...updatedCategories[index], + name: categoryName, + icon: selectedIcon + }; + } + } else { + // 添加新分类 + const newId = Date.now(); + updatedCategories.push({ + id: newId, + name: categoryName, + type: activeType, + icon: selectedIcon + }); + } + + // 保存分类 + wx.setStorageSync('categories', updatedCategories); + this.loadCategories(); + this.hideModal(); + + wx.showToast({ + title: isEditing ? '分类已更新' : '分类已添加', + icon: 'success', + duration: 1500 + }); + } +}) diff --git a/pages/category/category.wxml b/pages/category/category.wxml new file mode 100644 index 0000000..6ef32e3 --- /dev/null +++ b/pages/category/category.wxml @@ -0,0 +1,75 @@ + + + 支出分类 + 收入分类 + + + + + + + + + + {{item.name}} + + + + + + + + + 暂无分类,请添加 + + + + + + + + + {{isEditing ? '编辑分类' : '添加分类'}} + + + + 分类名称 + + + + + 选择图标 + + + + + + + + + + + + + diff --git a/pages/category/category.wxss b/pages/category/category.wxss new file mode 100644 index 0000000..bc5ab0e --- /dev/null +++ b/pages/category/category.wxss @@ -0,0 +1,230 @@ +.container { + background-color: #f5f5f5; + min-height: 100vh; + padding-bottom: 80px; +} + +.type-tabs { + display: flex; + background-color: white; + border-bottom: 1px solid #f5f5f5; +} + +.tab { + flex: 1; + text-align: center; + padding: 15px 0; + font-size: 16px; + color: #666; + position: relative; +} + +.tab.active { + color: #4CAF50; +} + +.tab.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 3px; + background-color: #4CAF50; +} + +.category-list { + margin: 15px; +} + +.category-item { + display: flex; + justify-content: space-between; + align-items: center; + background-color: white; + border-radius: 10px; + padding: 15px; + margin-bottom: 10px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); +} + +.category-info { + display: flex; + align-items: center; +} + +.category-icon { + width: 40px; + height: 40px; + border-radius: 50%; + background-color: #e8f5e9; + color: #4CAF50; + display: flex; + align-items: center; + justify-content: center; + margin-right: 15px; +} + +.category-icon .iconfont { + font-size: 20px; +} + +.category-name { + font-size: 16px; + color: #333; +} + +.category-actions { + display: flex; +} + +.edit-btn, .delete-btn { + padding: 5px 12px; + font-size: 14px; + border-radius: 4px; + margin-left: 10px; + height: auto; + line-height: normal; +} + +.edit-btn { + color: #4CAF50; + background-color: #e8f5e9; +} + +.delete-btn { + color: #f44336; + background-color: #ffebee; +} + +.no-categories { + text-align: center; + padding: 40px 0; + color: #999; + font-size: 14px; + background-color: white; + border-radius: 10px; +} + +.add-btn { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + width: 90%; + height: 50px; + background-color: #4CAF50; + color: white; + border-radius: 25px; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3); +} + +.add-btn .iconfont { + margin-right: 8px; +} + +/* 弹窗样式 */ +.modal-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +.modal-dialog { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: white; + border-top-left-radius: 15px; + border-top-right-radius: 15px; + padding: 20px; + z-index: 1001; +} + +.modal-title { + font-size: 18px; + font-weight: 600; + color: #333; + margin-bottom: 20px; + text-align: center; +} + +.form-item { + margin-bottom: 20px; +} + +.form-label { + display: block; + font-size: 14px; + color: #666; + margin-bottom: 10px; +} + +.form-item input { + width: 100%; + height: 45px; + border: 1px solid #ddd; + border-radius: 8px; + padding: 0 15px; + font-size: 16px; + box-sizing: border-box; +} + +.icon-selector { + display: flex; + flex-wrap: wrap; + gap: 15px; + max-height: 200px; + overflow-y: auto; +} + +.icon-item { + width: 50px; + height: 50px; + border-radius: 8px; + background-color: #f5f5f5; + display: flex; + align-items: center; + justify-content: center; +} + +.icon-item.selected { + background-color: #e8f5e9; + color: #4CAF50; +} + +.icon-item .iconfont { + font-size: 24px; +} + +.modal-footer { + display: flex; + margin-top: 10px; +} + +.cancel-btn, .confirm-btn { + flex: 1; + height: 45px; + border-radius: 8px; + font-size: 16px; + margin: 0 5px; +} + +.cancel-btn { + background-color: #f5f5f5; + color: #666; +} + +.confirm-btn { + background-color: #4CAF50; + color: white; +} diff --git a/pages/index/index.js b/pages/index/index.js new file mode 100644 index 0000000..99c7f11 --- /dev/null +++ b/pages/index/index.js @@ -0,0 +1,69 @@ +const request = require('../utils/request') +Page({ + data: { + currentMonth: '', + totalBalance: '0.00', + totalIncome: '0.00', + totalExpense: '0.00', + recentRecords: [] + }, + + onShow() { + this.updateData(); + }, + + async updateData() { + // 更新当前月份 + const date = new Date(); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + this.setData({ + currentMonth: `${year}年${month}月` + }); + + // 获取所有记录 + let records = []; + + // 筛选本月记录 + const currentMonthRecords = records.filter(record => { + const recordDate = new Date(record.date); + return recordDate.getFullYear() === year && recordDate.getMonth() + 1 === month; + }); + + // 计算收支和余额 + let totalIncome = 0; + let totalExpense = 0; + + currentMonthRecords.forEach(record => { + if (record.type === 'income') { + totalIncome += parseFloat(record.amount); + } else { + totalExpense += parseFloat(record.amount); + } + }); + + const totalBalance = totalIncome - totalExpense; + + try { + // 自动携带token + const res = await request.get('/admin-api/book/inout/myList?pageNo=1&pageSize=10') + console.log('获取数据成功:', res) + if(res.code==0){ + records=res.data.list + } + // 处理数据... + } catch (error) { + console.error('加载失败:', error) + } + console.log('records:',records) + this.setData({ + totalBalance: totalBalance.toFixed(2), + totalIncome: totalIncome.toFixed(2), + totalExpense: totalExpense.toFixed(2), + // 按日期排序,取最近10条 + recentRecords: [...records] + // .sort((a, b) => new Date(b.date) - new Date(a.date)) + .slice(0, 10) + }); + } +}) diff --git a/pages/index/index.wxml b/pages/index/index.wxml new file mode 100644 index 0000000..9a679dd --- /dev/null +++ b/pages/index/index.wxml @@ -0,0 +1,50 @@ + + + + + + 本月收支 + {{currentMonth}} + + + + + + 收入 + ¥ {{totalIncome}} + + + + 支出 + ¥ {{totalExpense}} + + + + + + + + 近期记录 + 查看全部 + + + + + + + + + {{item.categoryName}} + {{item.useDate}} + + + {{item.type === 'in' ? '+' : '-'}}¥ {{item.money}} + + + + + 暂无记录,开始添加吧~ + + + + diff --git a/pages/index/index.wxss b/pages/index/index.wxss new file mode 100644 index 0000000..50860d4 --- /dev/null +++ b/pages/index/index.wxss @@ -0,0 +1,165 @@ +.container { + background-color: #f5f5f5; + min-height: 100vh; + padding-bottom: 60px; +} + +.balance-card { + background: linear-gradient(135deg, #4CAF50 0%, #8BC34A 100%); + color: white; + padding: 20px 15px; + border-radius: 15px; + margin: 15px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.balance-header { + display: flex; + justify-content: space-between; + margin-bottom: 15px; +} + +.balance-title { + font-size: 16px; + opacity: 0.9; +} + +.balance-date { + font-size: 14px; + opacity: 0.8; +} + +.balance-amount { + font-size: 32px; + font-weight: bold; + margin-bottom: 20px; +} + +.balance-stats { + display: flex; + justify-content: space-around; + border-top: 1px solid rgba(255, 255, 255, 0.2); + padding-top: 15px; +} + +.balance-item { + text-align: center; +} + +.income-label, .expense-label { + font-size: 14px; + opacity: 0.8; + display: block; + margin-bottom: 5px; +} + +.income-amount, .expense-amount { + font-size: 18px; + font-weight: 500; +} + +.balance-divider { + width: 1px; + background-color: rgba(255, 255, 255, 0.2); +} + +.records-section { + margin: 15px; + background-color: white; + border-radius: 15px; + padding: 15px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.section-title { + font-size: 18px; + font-weight: 600; + color: #333; +} + +.see-all { + font-size: 14px; + color: #4CAF50; +} + +.records-list { + max-height: 400px; + overflow-y: auto; +} + +.record-item { + display: flex; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #f5f5f5; +} + +.record-item:last-child { + border-bottom: none; +} + +.record-icon { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 15px; +} + +.income-icon { + background-color: #e8f5e9; + color: #4CAF50; +} + +.expense-icon { + background-color: #ffebee; + color: #f44336; +} + +.iconfont { + font-size: 20px; +} + +.record-details { + flex: 1; +} + +.record-title { + font-size: 16px; + color: #333; + margin-bottom: 3px; +} + +.record-date { + font-size: 12px; + color: #999; +} + +.record-amount { + font-size: 16px; + font-weight: 500; +} + +.income { + color: #4CAF50; +} + +.expense { + color: #f44336; +} + +.no-records { + text-align: center; + padding: 40px 0; + color: #999; + font-size: 14px; +} diff --git a/pages/launch/launch.js b/pages/launch/launch.js new file mode 100644 index 0000000..cb157a6 --- /dev/null +++ b/pages/launch/launch.js @@ -0,0 +1,9 @@ +{ + onLoad() { + // 延迟500ms跳转(可选,可用于显示小程序logo/slogan) + setTimeout(() => { + // 调用app.js中的登录检查方法 + getApp().checkLoginStatus() + }, 500) + } +} diff --git a/pages/launch/launch.json b/pages/launch/launch.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/pages/launch/launch.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/pages/launch/launch.wxml b/pages/launch/launch.wxml new file mode 100644 index 0000000..1cfc408 --- /dev/null +++ b/pages/launch/launch.wxml @@ -0,0 +1,7 @@ + + + + 智能记账本 + diff --git a/pages/launch/launch.wxss b/pages/launch/launch.wxss new file mode 100644 index 0000000..203d60b --- /dev/null +++ b/pages/launch/launch.wxss @@ -0,0 +1,18 @@ +.launch-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background-color: #f5f7fa; +} + +.logo { + margin-bottom: 20rpx; +} + +.slogan { + font-size: 36rpx; + color: #3B82F6; + font-weight: bold; +} diff --git a/pages/login/login.js b/pages/login/login.js new file mode 100644 index 0000000..dc5fa51 --- /dev/null +++ b/pages/login/login.js @@ -0,0 +1,213 @@ +const request = require('../utils/request') +Page({ + data: { + // 账号密码 + account: '', + password: '', + // 显示密码切换 + showPassword: false, + // 错误状态 + accountError: false, + accountErrorMsg: '', + passwordError: false, + passwordErrorMsg: '', + // 加载状态 + isLoading: false + }, + + // 处理账号输入 + handleAccountInput(e) { + const account = e.detail.value.trim(); + this.setData({ + account, + accountError: false, + accountErrorMsg: '' + }); + }, + + // 处理密码输入 + handlePasswordInput(e) { + const password = e.detail.value; + this.setData({ + password, + passwordError: false, + passwordErrorMsg: '' + }); + }, + + // 切换密码显示状态 + togglePassword() { + this.setData({ + showPassword: !this.data.showPassword + }); + }, + + // 表单验证 + validateForm() { + let isValid = true; + const { account, password } = this.data; + + // 验证账号 + if (!account) { + this.setData({ + accountError: true, + accountErrorMsg: '请输入账号' + }); + isValid = false; + } else if (account.length < 4) { + this.setData({ + accountError: true, + accountErrorMsg: '账号长度不能少于4位' + }); + isValid = false; + } + + // 验证密码 + if (!password) { + this.setData({ + passwordError: true, + passwordErrorMsg: '请输入密码' + }); + isValid = false; + } else if (password.length < 6) { + this.setData({ + passwordError: true, + passwordErrorMsg: '密码长度不能少于6位' + }); + isValid = false; + } + + return isValid; + }, + + // 处理登录 + handleLogin() { + // 表单验证 + if (!this.validateForm()) { + return; + } + + const { account, password } = this.data; + + // 显示加载状态 + this.setData({ + isLoading: true + }); + + wx.request({ + url: 'http://localhost:48080/admin-api/system/auth/login', // 仅为示例,并非真实的接口地址 + method: 'POST', // 请求方法,可以是 GET, POST, PUT, DELETE 等 + data: { + "tenantName": "芋道源码", + "username": account, + "password": password, + "rememberMe": true + }, + header: { + 'Tenant-Id':1, + 'content-type': 'application/json' // 默认值,也可以根据后端要求设置其他值,如 'application/x-www-form-urlencoded' + }, + success :(res)=> { + console.log(res.data) + // 处理返回数据 + if(res.data.code==0){ + + wx.setStorageSync('token', res.data.data.accessToken); + console.log(wx.getStorageSync('token')) + // 跳转到主界面 + wx.switchTab({ + url: '/pages/index/index', + success: () => { + console.log('执行成功') + this.setData({ + isLoading: false + }); + } + }); + }else{ + this.setData({ + passwordError: true, + passwordErrorMsg: res.data.msg, + isLoading: false + }); + // 震动反馈 + wx.vibrateShort(); + } + }, + fail (err) { + console.error(err) + + }, + complete () { + // 请求完成时执行,无论成功或失败 + } + }) + }, + + // 微信快捷登录 + wxLogin(e) { + // 检查用户是否授权 + if (e.detail.userInfo) { + // 用户同意授权 + this.setData({ + isLoading: true + }); + + // 模拟微信登录过程 + setTimeout(() => { + // 保存用户信息 + wx.setStorageSync('userInfo', { + nickname: e.detail.userInfo.nickName, + avatar: e.detail.userInfo.avatarUrl, + loginTime: new Date().getTime(), + isWechatLogin: true + }); + + // 跳转到主界面 + wx.redirectTo({ + url: '/pages/index/index', + success: () => { + this.setData({ + isLoading: false + }); + } + }); + }, 1500); + } else { + // 用户拒绝授权 + wx.showToast({ + title: '授权失败,无法登录', + icon: 'none', + duration: 2000 + }); + } + }, + + // 跳转到忘记密码页面 + navigateToForgot() { + wx.navigateTo({ + url: '/pages/forgot-password/forgot-password' + }); + }, + + // 跳转到注册页面 + navigateToRegister() { + wx.navigateTo({ + url: '/pages/register/register' + }); + }, + + onLoad() { + // 检查是否已登录 + const userInfo = wx.getStorageSync('userInfo'); + if (userInfo && userInfo.loginTime) { + // 如果30天内登录过,自动跳转 + const now = new Date().getTime(); + if (now - userInfo.loginTime < 30 * 24 * 60 * 60 * 1000) { + wx.redirectTo({ + url: '/pages/index/index' + }); + } + } + } +}); diff --git a/pages/login/login.json b/pages/login/login.json new file mode 100644 index 0000000..ca1ac3b --- /dev/null +++ b/pages/login/login.json @@ -0,0 +1,7 @@ +{ + "navigationBarTitleText": "登录", + "navigationBarBackgroundColor": "#f5f7fa", + "navigationBarTextStyle": "black", + "backgroundColor": "#f5f7fa", + "usingComponents": {} +} diff --git a/pages/login/login.wxml b/pages/login/login.wxml new file mode 100644 index 0000000..f731f5c --- /dev/null +++ b/pages/login/login.wxml @@ -0,0 +1,83 @@ + + + + 记账程序图标 + 财务管家 + + + + + + + + + + + + + + + {{accountErrorMsg}} + + + + + + + + + + + + {{passwordErrorMsg}} + + + + 忘记密码? + + + + + + + + 还没有账号? + 立即注册 + + + + + + + + + + + + + + diff --git a/pages/login/login.wxss b/pages/login/login.wxss new file mode 100644 index 0000000..b1d4313 --- /dev/null +++ b/pages/login/login.wxss @@ -0,0 +1,180 @@ +.login-container { + display: flex; + flex-direction: column; + min-height: 100vh; + background: linear-gradient(135deg, #f5f7fa 0%, #e4eaf1 100%); + padding: 0 30rpx; + box-sizing: border-box; +} + +/* 应用图标区域 */ +.app-icon { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 120rpx; + margin-bottom: 80rpx; +} + +.logo-img { + width: 160rpx; + height: 160rpx; + border-radius: 24rpx; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1); + margin-bottom: 30rpx; +} + +.app-name { + font-size: 36rpx; + font-weight: 600; + color: #333; + letter-spacing: 2rpx; +} + +/* 登录卡片 */ +.login-card { + background-color: #fff; + border-radius: 24rpx; + padding: 60rpx 40rpx; + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.08); + margin-bottom: 50rpx; +} + +/* 输入框样式 */ +.input-group { + display: flex; + align-items: center; + border-bottom: 2rpx solid #eee; + padding: 25rpx 0; + position: relative; + transition: all 0.3s ease; +} + +.input-group.error { + border-bottom-color: #ff4d4f; +} + +.input-icon { + margin-right: 20rpx; +} + +.input-field { + flex: 1; + font-size: 30rpx; + color: #333; + height: 40rpx; +} + +.placeholder { + color: #c9c9c9; +} + +.toggle-password { + margin-left: 20rpx; +} + +.error-icon { + margin-left: 20rpx; +} + +.error-message { + color: #ff4d4f; + font-size: 24rpx; + margin-top: 10rpx; + margin-bottom: 10rpx; + height: 28rpx; + line-height: 28rpx; +} + +/* 忘记密码 */ +.forgot-password { + display: flex; + justify-content: flex-end; + margin: 15rpx 0 40rpx 0; +} + +.forgot-password text { + font-size: 26rpx; + color: #5a89e7; +} + +/* 登录按钮 */ +.login-button { + width: 100%; + height: 90rpx; + line-height: 90rpx; + background: linear-gradient(90deg, #5a89e7 0%, #3b6fe4 100%); + color: #fff; + font-size: 32rpx; + border-radius: 45rpx; + margin: 20rpx 0; + box-shadow: 0 6rpx 16rpx rgba(90, 137, 231, 0.4); + letter-spacing: 4rpx; +} + +.login-button::after { + border: none; +} + +/* 注册区域 */ +.register-section { + display: flex; + justify-content: center; + margin-top: 40rpx; + font-size: 26rpx; + color: #666; +} + +.register-link { + color: #5a89e7; + margin-left: 10rpx; + font-weight: 500; +} + +/* 其他登录方式 */ +.other-login { + display: flex; + align-items: center; + margin: 30rpx 0; +} + +.line { + flex: 1; + height: 1rpx; + background-color: #ddd; +} + +.other-login-text { + padding: 0 20rpx; + font-size: 24rpx; + color: #999; +} + +/* 社交登录 */ +.social-login { + display: flex; + justify-content: center; + margin-top: 20rpx; +} + +.social-btn { + display: flex; + align-items: center; + justify-content: center; + background-color: #f0f7ff; + color: #52c41a; + font-size: 28rpx; + padding: 0 30rpx; + height: 80rpx; + border-radius: 40rpx; +} + +.social-btn::after { + border: none; +} + +.social-icon { + width: 36rpx; + height: 36rpx; + margin-right: 15rpx; +} diff --git a/pages/profile/profile.js b/pages/profile/profile.js new file mode 100644 index 0000000..ac2f5f9 --- /dev/null +++ b/pages/profile/profile.js @@ -0,0 +1,146 @@ +Page({ + data: { + totalRecords: 0, + firstRecordDate: '', + consecutiveDays: 0 + }, + + onShow() { + this.updateStats(); + }, + + updateStats() { + const records = wx.getStorageSync('records') || []; + const totalRecords = records.length; + + let firstRecordDate = ''; + let consecutiveDays = 0; + + if (totalRecords > 0) { + // 计算首次记账日期 + const sortedRecords = [...records].sort((a, b) => new Date(a.date) - new Date(b.date)); + const firstDate = new Date(sortedRecords[0].date); + firstRecordDate = `${firstDate.getFullYear()}-${(firstDate.getMonth() + 1).toString().padStart(2, '0')}-${firstDate.getDate().toString().padStart(2, '0')}`; + + // 计算连续记账天数 + consecutiveDays = this.calculateConsecutiveDays(records); + } + + this.setData({ + totalRecords, + firstRecordDate, + consecutiveDays + }); + }, + + calculateConsecutiveDays(records) { + // 获取去重的日期列表 + const dateSet = new Set(); + records.forEach(record => { + dateSet.add(record.date); + }); + + const dateList = Array.from(dateSet).sort((a, b) => new Date(b) - new Date(a)); + + if (dateList.length === 0) { + return 0; + } + + // 检查是否包含今天 + const today = new Date(); + today.setHours(0, 0, 0, 0); + const lastDate = new Date(dateList[0]); + lastDate.setHours(0, 0, 0, 0); + + if (lastDate.getTime() !== today.getTime()) { + return 0; + } + + // 计算连续天数 + let consecutiveDays = 1; + for (let i = 0; i < dateList.length - 1; i++) { + const currentDate = new Date(dateList[i]); + currentDate.setHours(0, 0, 0, 0); + + const nextDate = new Date(dateList[i + 1]); + nextDate.setHours(0, 0, 0, 0); + + // 检查是否连续(相差一天) + const diffTime = currentDate.getTime() - nextDate.getTime(); + const diffDays = diffTime / (1000 * 60 * 60 * 24); + + if (diffDays === 1) { + consecutiveDays++; + } else { + break; + } + } + + return consecutiveDays; + }, + + exportData() { + const records = wx.getStorageSync('records') || []; + if (records.length === 0) { + wx.showToast({ + title: '暂无数据可导出', + icon: 'none', + duration: 2000 + }); + return; + } + + // 转换为CSV格式 + let csvContent = "日期,类型,分类,金额,备注\n"; + records.forEach(record => { + const type = record.type === 'income' ? '收入' : '支出'; + csvContent += `${record.date},${type},${record.categoryName},${record.amount},${record.note || ''}\n`; + }); + + // 这里仅做演示,实际导出功能需要后端支持或使用微信的文件系统API + wx.showModal({ + title: '导出成功', + content: '数据已导出为CSV格式', + showCancel: false + }); + }, + + clearData() { + wx.showModal({ + title: '确认清空', + content: '确定要清空所有记账数据吗?此操作不可恢复。', + cancelText: '取消', + confirmText: '确认', + success: (res) => { + if (res.confirm) { + wx.setStorageSync('records', []); + wx.showToast({ + title: '数据已清空', + icon: 'success', + duration: 2000 + }); + this.updateStats(); + } + } + }); + }, + + showAbout() { + wx.showModal({ + title: '关于简易记账', + content: '简易记账是一款简单实用的记账工具,帮助你记录和管理个人财务。', + showCancel: false, + confirmText: '我知道了' + }); + }, + + outLogin() { + wx.showModal({ + title: '关于简易记账', + content: '简易记账是一款简单实用的记账工具,帮助你记录和管理个人财务。', + showCancel: false, + confirmText: '我知道了' + }); + } + +}) diff --git a/pages/profile/profile.wxml b/pages/profile/profile.wxml new file mode 100644 index 0000000..4caa26c --- /dev/null +++ b/pages/profile/profile.wxml @@ -0,0 +1,75 @@ + + + + + + + + 记账用户 + 开始你的记账之旅 + + + + + + + + + + 分类管理 + + + + + + + + 导出数据 + + + + + + + + 清空数据 + + + + + + + + 关于我们 + + + + + + + + 退出登录 + + + + + + + + 记账统计 + + + {{totalRecords}} + 总记录数 + + + {{firstRecordDate || '-'}} + 首次记账 + + + {{consecutiveDays}} + 连续记账(天) + + + + diff --git a/pages/profile/profile.wxss b/pages/profile/profile.wxss new file mode 100644 index 0000000..fa65c79 --- /dev/null +++ b/pages/profile/profile.wxss @@ -0,0 +1,128 @@ +.container { + background-color: #f5f5f5; + min-height: 100vh; + padding-bottom: 15px; +} + +.user-info { + background: linear-gradient(135deg, #4CAF50 0%, #8BC34A 100%); + padding: 30px 20px; + display: flex; + align-items: center; +} + +.avatar { + width: 80px; + height: 80px; + border-radius: 50%; + background-color: white; + padding: 5px; + margin-right: 15px; +} + +.avatar image { + width: 100%; + height: 100%; + border-radius: 50%; +} + +.user-details { + color: white; +} + +.username { + font-size: 20px; + font-weight: 600; + display: block; + margin-bottom: 5px; +} + +.user-desc { + font-size: 14px; + opacity: 0.9; +} + +.function-list { + background-color: white; + margin: 15px; + border-radius: 15px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.function-item { + display: flex; + align-items: center; + padding: 15px 20px; + border-bottom: 1px solid #f5f5f5; + color: #333; +} + +.function-item:last-child { + border-bottom: none; +} + +.function-icon { + width: 30px; + height: 30px; + border-radius: 50%; + background-color: #e8f5e9; + color: #4CAF50; + display: flex; + align-items: center; + justify-content: center; + margin-right: 15px; +} + +.function-icon .iconfont { + font-size: 18px; +} + +.function-name { + flex: 1; + font-size: 16px; +} + +.function-item .icon-arrow-right { + color: #ccc; + font-size: 16px; +} + +.stats-card { + background-color: white; + margin: 0 15px; + border-radius: 15px; + padding: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.stats-title { + font-size: 18px; + font-weight: 600; + color: #333; + display: block; + margin-bottom: 15px; +} + +.stats-grid { + display: flex; + justify-content: space-around; +} + +.stats-item { + text-align: center; + flex: 1; +} + +.stats-value { + font-size: 22px; + font-weight: 600; + color: #333; + display: block; + margin-bottom: 5px; +} + +.stats-label { + font-size: 14px; + color: #666; +} diff --git a/pages/statistics/statistics.js b/pages/statistics/statistics.js new file mode 100644 index 0000000..6cce705 --- /dev/null +++ b/pages/statistics/statistics.js @@ -0,0 +1,209 @@ +Page({ + data: { + selectedMonth: '', + selectedMonthText: '', + summary: { + income: '0.00', + expense: '0.00', + balance: '0.00' + }, + activeType: 'expense', + categoryStats: [], + monthRecords: [] + }, + + onLoad() { + // 初始化当前月份 + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const selectedMonth = `${year}-${month}`; + + this.setData({ + selectedMonth, + selectedMonthText: `${year}年${month}月` + }); + + this.updateStatistics(); + }, + + onMonthChange(e) { + const selectedMonth = e.detail.value; + const [year, month] = selectedMonth.split('-'); + this.setData({ + selectedMonth, + selectedMonthText: `${year}年${month}月` + }); + + this.updateStatistics(); + }, + + setActiveType(e) { + const type = e.currentTarget.dataset.type; + this.setData({ activeType: type }, () => { + this.calculateCategoryStats(); + this.drawPieChart(); + }); + }, + + updateStatistics() { + // 获取所有记录 + const records = wx.getStorageSync('records') || []; + const [year, month] = this.data.selectedMonth.split('-'); + + // 筛选当月记录 + const monthRecords = records.filter(record => { + const recordDate = new Date(record.date); + return recordDate.getFullYear() === parseInt(year) && + (recordDate.getMonth() + 1) === parseInt(month); + }); + + // 按日期倒序排列 + monthRecords.sort((a, b) => new Date(b.date) - new Date(a.date)); + + // 计算收支汇总 + let income = 0; + let expense = 0; + + monthRecords.forEach(record => { + if (record.type === 'income') { + income += parseFloat(record.amount); + } else { + expense += parseFloat(record.amount); + } + }); + + const balance = income - expense; + + this.setData({ + monthRecords, + summary: { + income: income.toFixed(2), + expense: expense.toFixed(2), + balance: balance.toFixed(2) + } + }, () => { + this.calculateCategoryStats(); + this.drawPieChart(); + }); + }, + + calculateCategoryStats() { + const { monthRecords, activeType } = this.data; + const typeRecords = monthRecords.filter(record => record.type === activeType); + + // 按分类汇总 + const categoryMap = {}; + + typeRecords.forEach(record => { + if (!categoryMap[record.categoryId]) { + categoryMap[record.categoryId] = { + categoryId: record.categoryId, + categoryName: record.categoryName, + amount: 0, + count: 0 + }; + } + + categoryMap[record.categoryId].amount += parseFloat(record.amount); + categoryMap[record.categoryId].count += 1; + }); + + // 转换为数组并排序 + let categoryStats = Object.values(categoryMap); + categoryStats.sort((a, b) => b.amount - a.amount); + + // 计算百分比 + const total = activeType === 'income' + ? parseFloat(this.data.summary.income) + : parseFloat(this.data.summary.expense); + + if (total > 0) { + categoryStats.forEach(item => { + item.percentage = Math.round((item.amount / total) * 100); + item.amount = item.amount.toFixed(2); + }); + } + + // 分配颜色 + const colors = ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39']; + categoryStats.forEach((item, index) => { + item.color = colors[index % colors.length]; + }); + + this.setData({ categoryStats }); + }, + + drawPieChart() { + const { categoryStats } = this.data; + + if (categoryStats.length === 0) { + return; + } + + const ctx = wx.createCanvasContext('pieChart', this); + const centerX = 120; // 饼图中心X坐标 + const centerY = 120; // 饼图中心Y坐标 + const radius = 90; // 饼图半径 + + let startAngle = 0; + + categoryStats.forEach(item => { + const percentage = parseFloat(item.percentage); + const endAngle = startAngle + 2 * Math.PI * (percentage / 100); + + // 绘制扇形 + ctx.beginPath(); + ctx.setFillStyle(item.color); + ctx.moveTo(centerX, centerY); + ctx.arc(centerX, centerY, radius, startAngle, endAngle, false); + ctx.closePath(); + ctx.fill(); + + // 计算文本位置 + const midAngle = startAngle + (endAngle - startAngle) / 2; + const textRadius = radius * 0.6; // 文本距离中心的距离 + const textX = centerX + Math.cos(midAngle) * textRadius; + const textY = centerY + Math.sin(midAngle) * textRadius; + + // 绘制百分比文本 + ctx.setFontSize(14); + ctx.setFillStyle('#333'); + ctx.setTextAlign('center'); + ctx.setTextBaseline('middle'); + ctx.fillText(`${percentage}%`, textX, textY); + + startAngle = endAngle; + }); + + // 绘制中心空白区域 + ctx.beginPath(); + ctx.setFillStyle('#ffffff'); + ctx.arc(centerX, centerY, radius * 0.4, 0, 2 * Math.PI, false); + ctx.fill(); + + // 绘制中心文本 + const total = this.data.activeType === 'income' + ? this.data.summary.income + : this.data.summary.expense; + + ctx.setFontSize(16); + ctx.setFillStyle('#333'); + ctx.setTextAlign('center'); + ctx.setTextBaseline('middle'); + ctx.fillText(`总计`, centerX, centerY - 10); + + ctx.setFontSize(18); + ctx.setFillStyle(this.data.activeType === 'income' ? '#4CAF50' : '#f44336'); + ctx.fillText(`¥${total}`, centerX, centerY + 15); + + ctx.draw(); + }, + + formatDate(dateStr) { + const date = new Date(dateStr); + const month = date.getMonth() + 1; + const day = date.getDate(); + return `${month}月${day}日`; + } +}) diff --git a/pages/statistics/statistics.wxml b/pages/statistics/statistics.wxml new file mode 100644 index 0000000..0f43ccb --- /dev/null +++ b/pages/statistics/statistics.wxml @@ -0,0 +1,88 @@ + + + + + + + {{selectedMonthText}} + + + + + + + + + 收入 + ¥ {{summary.income}} + + + 支出 + ¥ {{summary.expense}} + + + 结余 + ¥ {{summary.balance}} + + + + + + + 分类统计 + + 支出 + 收入 + + + + + + + + + + + + + + {{item.categoryName}} + + + ¥ {{item.amount}} + {{item.percentage}}% + + + + + 本月暂无{{activeType === 'expense' ? '支出' : '收入'}}记录 + + + + + + + + 明细记录 + + + + + + + + + {{item.categoryName}} + {{item.note || '无备注'}} + {{formatDate(item.date)}} + + + {{item.type === 'income' ? '+' : '-'}}¥ {{item.amount}} + + + + + 本月暂无记录 + + + diff --git a/pages/statistics/statistics.wxss b/pages/statistics/statistics.wxss new file mode 100644 index 0000000..4c7695a --- /dev/null +++ b/pages/statistics/statistics.wxss @@ -0,0 +1,234 @@ +.container { + background-color: #f5f5f5; + min-height: 100vh; + padding-bottom: 60px; +} + +.month-picker { + padding: 15px; + background-color: white; +} + +.picker-view { + display: flex; + align-items: center; + justify-content: center; + height: 40px; + background-color: #f5f5f5; + border-radius: 20px; + color: #333; + font-size: 16px; +} + +.picker-view .iconfont { + margin: 0 8px; + color: #666; +} + +.summary-card { + display: flex; + justify-content: space-around; + background-color: white; + margin: 15px; + border-radius: 15px; + padding: 20px 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.summary-item { + text-align: center; + flex: 1; +} + +.summary-label { + display: block; + font-size: 14px; + color: #666; + margin-bottom: 5px; +} + +.summary-amount { + font-size: 22px; + font-weight: 600; +} + +.income { + color: #4CAF50; +} + +.expense { + color: #f44336; +} + +.balance { + color: #2196F3; +} + +.category-stats, .records-section { + background-color: white; + margin: 15px; + border-radius: 15px; + padding: 15px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.section-title { + font-size: 18px; + font-weight: 600; + color: #333; +} + +.type-tabs { + display: flex; +} + +.tab { + padding: 5px 12px; + font-size: 14px; + border-radius: 15px; + margin-left: 10px; +} + +.tab.active { + background-color: #e8f5e9; + color: #4CAF50; +} + +.chart-container { + display: flex; + justify-content: center; + margin-bottom: 20px; +} + +.pie-chart { + width: 240px; + height: 240px; +} + +.category-details { + margin-bottom: 10px; +} + +.category-detail-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0; + border-bottom: 1px solid #f5f5f5; +} + +.category-detail-item:last-child { + border-bottom: none; +} + +.category-info { + display: flex; + align-items: center; +} + +.category-color { + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 10px; +} + +.category-name { + font-size: 16px; + color: #333; +} + +.category-amount { + text-align: right; +} + +.category-amount text:first-child { + font-size: 16px; + font-weight: 500; + margin-right: 10px; +} + +.category-percentage { + font-size: 14px; + color: #999; +} + +.records-list { + max-height: 300px; + overflow-y: auto; +} + +.record-item { + display: flex; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #f5f5f5; +} + +.record-item:last-child { + border-bottom: none; +} + +.record-icon { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 15px; +} + +.income-icon { + background-color: #e8f5e9; + color: #4CAF50; +} + +.expense-icon { + background-color: #ffebee; + color: #f44336; +} + +.iconfont { + font-size: 20px; +} + +.record-details { + flex: 1; +} + +.record-title { + font-size: 16px; + color: #333; + margin-bottom: 3px; +} + +.record-note { + font-size: 12px; + color: #999; + margin-bottom: 2px; +} + +.record-date { + font-size: 12px; + color: #ccc; +} + +.record-amount { + font-size: 16px; + font-weight: 500; +} + +.no-data, .no-records { + text-align: center; + padding: 30px 0; + color: #999; + font-size: 14px; +} diff --git a/pages/utils/request.js b/pages/utils/request.js new file mode 100644 index 0000000..80de07a --- /dev/null +++ b/pages/utils/request.js @@ -0,0 +1,111 @@ +/** + * 封装微信小程序请求,自动处理token + */ +const baseUrl='http://localhost:48080' + +const request = (url, options = {}) => { + // 返回Promise,方便使用async/await + return new Promise((resolve, reject) => { + // 默认请求头 + const header = { + 'Content-Type': 'application/json', + ...options.header + } + + // 从本地存储获取token并添加到请求头 + const token = wx.getStorageSync('token') + if (token) { + header['Authorization'] = `Bearer ${token}` + } + // 合并请求配置 + const config = { + url:baseUrl+url, + method: options.method || 'GET', + data: options.data || {}, + header, + // 成功回调 + success: (res) => { + if (res.statusCode === 200) { + // 业务成功,返回数据 + if(res.data&&res.data.code==401){ + console.log("登录已过期,请重新登录") + // token失效或未登录,清除token并跳转登录页 + wx.removeStorageSync('token') + wx.showToast({ + title: '登录已过期,请重新登录', + icon: 'none' + }) + + // 记录当前页面,登录后返回 + const pages = getCurrentPages() + const currentPage = pages[pages.length - 1] + wx.redirectTo({ + url: `/pages/login/login?redirect=${currentPage.route}` + }) + + reject(new Error('未授权或token已过期')) + }else{ + console.log("111") + resolve(res.data) + } + } else if (res.statusCode === 401) { + console.log("登录已过期,请重新登录") + // token失效或未登录,清除token并跳转登录页 + wx.removeStorageSync('token') + wx.showToast({ + title: '登录已过期,请重新登录', + icon: 'none' + }) + + // 记录当前页面,登录后返回 + const pages = getCurrentPages() + const currentPage = pages[pages.length - 1] + wx.redirectTo({ + url: `/pages/login/login?redirect=${currentPage.route}` + }) + + reject(new Error('未授权或token已过期')) + } else { + // 其他错误(如400、500等) + wx.showToast({ + title: res.data?.message || '请求失败', + icon: 'none' + }) + reject(new Error(`请求错误: ${res.statusCode}`)) + } + }, + // 失败回调(如网络错误) + fail: (err) => { + wx.showToast({ + title: '网络异常,请稍后再试', + icon: 'none' + }) + reject(err) + }, + ...options + } + console.log('config.url',config.url) + // 发起原生请求 + wx.request(config) + }) +} + +// 快捷方法 +request.get = (url, data, options = {}) => { + return request(url, { ...options, method: 'GET', data }) +} + +request.post = (url, data, options = {}) => { + return request(url, { ...options, method: 'POST', data }) +} + +request.put = (url, data, options = {}) => { + return request(url, { ...options, method: 'PUT', data }) +} + +request.delete = (url, data, options = {}) => { + return request(url, { ...options, method: 'DELETE', data }) +} + +module.exports = request + diff --git a/project.config.json b/project.config.json new file mode 100644 index 0000000..39e75ee --- /dev/null +++ b/project.config.json @@ -0,0 +1,25 @@ +{ + "setting": { + "es6": true, + "postcss": true, + "minified": true, + "uglifyFileName": false, + "enhance": true, + "packNpmRelationList": [], + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "useCompilerPlugins": false, + "minifyWXML": true + }, + "compileType": "miniprogram", + "simulatorPluginLibVersion": {}, + "packOptions": { + "ignore": [], + "include": [] + }, + "appid": "wx8002cc7a350eb380", + "editorSetting": {} +} \ No newline at end of file diff --git a/project.private.config.json b/project.private.config.json new file mode 100644 index 0000000..8d2d774 --- /dev/null +++ b/project.private.config.json @@ -0,0 +1,14 @@ +{ + "libVersion": "3.10.1", + "projectname": "accounting-wechat-miniprogram%20(1)", + "setting": { + "urlCheck": false, + "coverView": true, + "lazyloadPlaceholderEnable": false, + "skylineRenderEnable": false, + "preloadBackgroundData": false, + "autoAudits": false, + "showShadowRootInWxmlPanel": true, + "compileHotReLoad": true + } +} \ No newline at end of file