Browse Source

初始提交

master
wangsai 1 week ago
commit
1a6280c998
  1. 43
      app.js
  2. 49
      app.json
  3. BIN
      images/app-logo.png
  4. BIN
      images/icon/add-selected.png
  5. BIN
      images/icon/add-selected2.png
  6. BIN
      images/icon/add.png
  7. BIN
      images/icon/add1.png
  8. BIN
      images/icon/char1t.png
  9. BIN
      images/icon/chart-selected.png
  10. BIN
      images/icon/chart-selected11.png
  11. BIN
      images/icon/chart.png
  12. BIN
      images/icon/home-selected.png
  13. BIN
      images/icon/home-selected2.png
  14. BIN
      images/icon/home.png
  15. BIN
      images/icon/home1.png
  16. BIN
      images/icon/user-selected.png
  17. BIN
      images/icon/user-selected1.png
  18. BIN
      images/icon/user.png
  19. BIN
      images/icon/user1.png
  20. BIN
      images/wechat.png
  21. 215
      pages/add/add.js
  22. 142
      pages/add/add.wxml
  23. 313
      pages/add/add.wxss
  24. 157
      pages/category/category.js
  25. 75
      pages/category/category.wxml
  26. 230
      pages/category/category.wxss
  27. 69
      pages/index/index.js
  28. 50
      pages/index/index.wxml
  29. 165
      pages/index/index.wxss
  30. 9
      pages/launch/launch.js
  31. 3
      pages/launch/launch.json
  32. 7
      pages/launch/launch.wxml
  33. 18
      pages/launch/launch.wxss
  34. 213
      pages/login/login.js
  35. 7
      pages/login/login.json
  36. 83
      pages/login/login.wxml
  37. 180
      pages/login/login.wxss
  38. 146
      pages/profile/profile.js
  39. 75
      pages/profile/profile.wxml
  40. 128
      pages/profile/profile.wxss
  41. 209
      pages/statistics/statistics.js
  42. 88
      pages/statistics/statistics.wxml
  43. 234
      pages/statistics/statistics.wxss
  44. 111
      pages/utils/request.js
  45. 25
      project.config.json
  46. 14
      project.private.config.json

43
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
}
})

49
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"
}

BIN
images/app-logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
images/icon/add-selected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
images/icon/add-selected2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
images/icon/add.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
images/icon/add1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
images/icon/char1t.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
images/icon/chart-selected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
images/icon/chart-selected11.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
images/icon/chart.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
images/icon/home-selected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
images/icon/home-selected2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
images/icon/home.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
images/icon/home1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
images/icon/user-selected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
images/icon/user-selected1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
images/icon/user.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
images/icon/user1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
images/wechat.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

215
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);
}
})

142
pages/add/add.wxml

@ -0,0 +1,142 @@
<view class="container">
<!-- 类型选择 -->
<view class="type-selector">
<view class="type-btn {{type === 'out' ? 'active' : ''}}" bindtap="setType" data-type="out">
<text class="iconfont icon-minus"></text>
<text>支出</text>
</view>
<view class="type-btn {{type === 'in' ? 'active' : ''}}" bindtap="setType" data-type="in">
<text class="iconfont icon-plus"></text>
<text>收入</text>
</view>
</view>
<!-- 金额输入 -->
<view class="amount-input">
<text class="currency-symbol">¥</text>
<input
type="number"
placeholder="0.00"
value="{{amount}}"
bindinput="onAmountChange"
focus="true"
maxlength="10"
/>
</view>
<!-- 分类选择 -->
<view class="category-section" wx:if="{{type === 'in' }}">
<text class="section-title">选择分类</text>
<view class="category-grid">
<view
wx:for="{{filteredCategories}}"
wx:key="id"
class="category-item {{selectedCategoryId === item.id ? 'selected' : ''}}"
bindtap="selectCategory"
data-id="{{item.id}}"
>
<text class="iconfont icon-{{item.icon}}"></text>
<text>{{item.classificationName}}</text>
</view>
</view>
</view>
<!-- 分类选择2 -->
<!-- 分类选择触发按钮 -->
<view class="category-trigger" bindtap="showCategoryPopup" wx:if="{{type === 'out' }}">
<text class="section-title">选择分类</text>
<view class="selected-value">
<text>{{selectedCategoryName || '请选择分类'}}</text>
<text class="iconfont icon-arrow-down"></text>
</view>
</view>
<!-- 弹窗遮罩层 -->
<view class="category-mask" wx:if="{{showPopup}}" bindtap="hideCategoryPopup"></view>
<!-- 弹窗容器 -->
<view class="category-popup" wx:if="{{showPopup}}">
<view class="popup-header">
<text class="popup-title">选择分类</text>
<text class="iconfont icon-close" bindtap="hideCategoryPopup"></text>
</view>
<view class="popup-content">
<!-- 一级分类列表 -->
<view class="first-level-list">
<view
wx:for="{{firstLevelCategories}}"
wx:key="id"
class="first-level-item {{selectedFirstLevelId === item.id ? 'selected' : ''}}"
bindtap="selectFirstLevel"
data-id="{{item.id}}"
>
<text class="iconfont icon-{{item.icon}}"></text>
<text>{{item.classificationName}}</text>
</view>
</view>
<!-- 二级分类列表 -->
<view class="second-level-list">
<view wx:if="{{!selectedFirstLevelId}}" class="no-selection">
请先选择一级分类
</view>
<view
wx:for="{{filteredSecondLevelCategories}}"
wx:key="id"
class="second-level-item {{selectedSecondLevelId === item.id ? 'selected' : ''}}"
bindtap="selectSecondLevel"
data-id="{{item.id}}"
data-name="{{item.classificationName}}"
>
{{item.classificationName}}
<text class="iconfont icon-check" wx:if="{{selectedSecondLevelId === item.id}}"></text>
</view>
</view>
</view>
<view class="popup-footer">
<button class="confirm-btn" bindtap="confirmSelection" disabled="{{!selectedSecondLevelId}}">
确认选择
</button>
</view>
</view>
<!-- 备注 -->
<view class="note-section">
<text class="section-title">备注 (可选)</text>
<input
type="text"
placeholder="添加备注信息"
value="{{note}}"
bindinput="onNoteChange"
maxlength="50"
/>
</view>
<!-- 日期选择 -->
<view class="date-section">
<text class="section-title">日期</text>
<picker mode="date" value="{{useDate}}" start="2020-01-01" end="{{maxDate}}" bindchange="onDateChange">
<view class="date-picker">
<text class="iconfont icon-calendar"></text>
<text>{{useDate}}</text>
</view>
</picker>
</view>
<!-- 保存按钮 -->
<button
class="save-btn"
bindtap="saveRecord"
disabled="{{!amount || (type === 'in'&&!selectedCategoryId) || (type === 'out'&&!selectedSecondLevelId)}}"
>
保存记录
</button>
</view>

313
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;
}

157
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
});
}
})

75
pages/category/category.wxml

@ -0,0 +1,75 @@
<view class="type-tabs">
<view class="tab {{activeType === 'expense' ? 'active' : ''}}" bindtap="setActiveType" data-type="expense">支出分类</view>
<view class="tab {{activeType === 'income' ? 'active' : ''}}" bindtap="setActiveType" data-type="income">收入分类</view>
</view>
<view class="category-list">
<view wx:for="{{filteredCategories}}" wx:key="id" class="category-item">
<view class="category-info">
<view class="category-icon">
<text class="iconfont icon-{{item.icon}}"></text>
</view>
<text class="category-name">{{item.name}}</text>
</view>
<view class="category-actions">
<button class="edit-btn" bindtap="editCategory" data-id="{{item.id}}">编辑</button>
<button
class="delete-btn"
bindtap="deleteCategory"
data-id="{{item.id}}"
wx:if="{{filteredCategories.length > 1}}"
>
删除
</button>
</view>
</view>
<view wx:if="{{filteredCategories.length === 0}}" class="no-categories">
<text>暂无分类,请添加</text>
</view>
</view>
<button class="add-btn" bindtap="addCategory">
<text class="iconfont icon-plus"></text>
添加分类
</button>
<!-- 添加/编辑分类弹窗 -->
<view class="modal-mask" wx:if="{{showModal}}" bindtap="hideModal"></view>
<view class="modal-dialog" wx:if="{{showModal}}">
<view class="modal-title">{{isEditing ? '编辑分类' : '添加分类'}}</view>
<view class="modal-content">
<view class="form-item">
<text class="form-label">分类名称</text>
<input
type="text"
placeholder="请输入分类名称"
value="{{categoryName}}"
bindinput="onNameChange"
/>
</view>
<view class="form-item">
<text class="form-label">选择图标</text>
<view class="icon-selector">
<view
wx:for="{{icons}}"
wx:key="*this"
class="icon-item {{selectedIcon === item ? 'selected' : ''}}"
bindtap="selectIcon"
data-icon="{{item}}"
>
<text class="iconfont icon-{{item}}"></text>
</view>
</view>
</view>
</view>
<view class="modal-footer">
<button class="cancel-btn" bindtap="hideModal">取消</button>
<button class="confirm-btn" bindtap="saveCategory">确认</button>
</view>
</view>

230
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;
}

69
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)
});
}
})

50
pages/index/index.wxml

@ -0,0 +1,50 @@
<view class="container">
<!-- 顶部余额卡片 -->
<view class="balance-card">
<view class="balance-header">
<!-- <text class="balance-title">当前余额</text> -->
<text class="balance-title">本月收支</text>
<text class="balance-date">{{currentMonth}}</text>
</view>
<!-- <view class="balance-amount">¥ {{totalBalance}}</view> -->
<view class="balance-stats">
<view class="balance-item">
<text class="income-label">收入</text>
<text class="income-amount">¥ {{totalIncome}}</text>
</view>
<view class="balance-divider"></view>
<view class="balance-item">
<text class="expense-label">支出</text>
<text class="expense-amount">¥ {{totalExpense}}</text>
</view>
</view>
</view>
<!-- 近期记录 -->
<view class="records-section">
<view class="section-header">
<text class="section-title">近期记录</text>
<navigator url="/pages/statistics/statistics" class="see-all">查看全部</navigator>
</view>
<view class="records-list">
<view wx:for="{{recentRecords}}" wx:key="id" class="record-item">
<view class="record-icon {{item.type === 'in' ? 'income-icon' : 'expense-icon'}}">
<text class="iconfont icon-{{item.categoryIcon}}"></text>
</view>
<view class="record-details">
<view class="record-title">{{item.categoryName}}</view>
<view class="record-date">{{item.useDate}}</view>
</view>
<view class="record-amount {{item.type === 'in' ? 'income' : 'expense'}}">
{{item.type === 'in' ? '+' : '-'}}¥ {{item.money}}
</view>
</view>
<view wx:if="{{recentRecords.length === 0}}" class="no-records">
<text>暂无记录,开始添加吧~</text>
</view>
</view>
</view>
</view>

165
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;
}

9
pages/launch/launch.js

@ -0,0 +1,9 @@
{
onLoad() {
// 延迟500ms跳转(可选,可用于显示小程序logo/slogan)
setTimeout(() => {
// 调用app.js中的登录检查方法
getApp().checkLoginStatus()
}, 500)
}
}

3
pages/launch/launch.json

@ -0,0 +1,3 @@
{
"usingComponents": {}
}

7
pages/launch/launch.wxml

@ -0,0 +1,7 @@
<view class="launch-container">
<!-- 可选:显示小程序logo或启动动画 -->
<view class="logo">
<i class="fa fa-book text-primary text-5xl"></i>
</view>
<view class="slogan">智能记账本</view>
</view>

18
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;
}

213
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'
});
}
}
}
});

7
pages/login/login.json

@ -0,0 +1,7 @@
{
"navigationBarTitleText": "登录",
"navigationBarBackgroundColor": "#f5f7fa",
"navigationBarTextStyle": "black",
"backgroundColor": "#f5f7fa",
"usingComponents": {}
}

83
pages/login/login.wxml

@ -0,0 +1,83 @@
<view class="login-container">
<!-- 应用图标 -->
<view class="app-icon">
<image src="/images/app-logo.png" mode="widthFix" class="logo-img" alt="记账程序图标"></image>
<text class="app-name">财务管家</text>
</view>
<!-- 登录表单卡片 -->
<view class="login-card">
<!-- 账户输入框 -->
<view class="input-group {{accountError ? 'error' : ''}}">
<view class="input-icon">
<icon type="user" size="20" color="{{accountError ? '#ff4d4f' : '#8c8c8c'}}"></icon>
</view>
<input
type="text"
placeholder="请输入账号"
placeholder-class="placeholder"
bindinput="handleAccountInput"
value="{{account}}"
class="input-field"
/>
<view wx:if="{{accountError}}" class="error-icon">
<icon type="warn" size="20" color="#ff4d4f"></icon>
</view>
</view>
<view wx:if="{{accountError}}" class="error-message">{{accountErrorMsg}}</view>
<!-- 密码输入框 -->
<view class="input-group {{passwordError ? 'error' : ''}}">
<view class="input-icon">
<icon type="lock" size="20" color="{{passwordError ? '#ff4d4f' : '#8c8c8c'}}"></icon>
</view>
<input
type="{{showPassword ? 'text' : 'password'}}"
placeholder="请输入密码"
placeholder-class="placeholder"
bindinput="handlePasswordInput"
value="{{password}}"
class="input-field"
/>
<view class="toggle-password" bindtap="togglePassword">
<icon type="{{showPassword ? 'eye-o' : 'eye'}}" size="20" color="#8c8c8c"></icon>
</view>
</view>
<view wx:if="{{passwordError}}" class="error-message">{{passwordErrorMsg}}</view>
<!-- 忘记密码 -->
<view class="forgot-password" bindtap="navigateToForgot">
<text>忘记密码?</text>
</view>
<!-- 登录按钮 -->
<button
class="login-button"
bindtap="handleLogin"
loading="{{isLoading}}"
disabled="{{isLoading}}"
>
登录
</button>
<!-- 注册入口 -->
<view class="register-section">
<text>还没有账号?</text>
<text class="register-link" bindtap="navigateToRegister">立即注册</text>
</view>
</view>
<!-- 其他登录方式 -->
<view class="other-login">
<view class="line"></view>
<text class="other-login-text">其他登录方式</text>
<view class="line"></view>
</view>
<view class="social-login">
<button class="social-btn" open-type="getUserInfo" bindgetuserinfo="wxLogin">
<image src="/images/wechat.png" mode="widthFix" class="social-icon" alt="微信登录"></image>
<text>微信快捷登录</text>
</button>
</view>
</view>

180
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;
}

146
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: '我知道了'
});
}
})

75
pages/profile/profile.wxml

@ -0,0 +1,75 @@
<view class="container">
<!-- 用户信息 -->
<view class="user-info">
<view class="avatar">
<image src="/images/icon/user-avatar.png" mode="widthFix"></image>
</view>
<view class="user-details">
<text class="username">记账用户</text>
<text class="user-desc">开始你的记账之旅</text>
</view>
</view>
<!-- 功能列表 -->
<view class="function-list">
<navigator url="/pages/category/category" class="function-item">
<view class="function-icon">
<text class="iconfont icon-tags"></text>
</view>
<text class="function-name">分类管理</text>
<text class="iconfont icon-arrow-right"></text>
</navigator>
<view class="function-item" bindtap="exportData">
<view class="function-icon">
<text class="iconfont icon-download"></text>
</view>
<text class="function-name">导出数据</text>
<text class="iconfont icon-arrow-right"></text>
</view>
<view class="function-item" bindtap="clearData">
<view class="function-icon">
<text class="iconfont icon-trash"></text>
</view>
<text class="function-name">清空数据</text>
<text class="iconfont icon-arrow-right"></text>
</view>
<view class="function-item" bindtap="showAbout">
<view class="function-icon">
<text class="iconfont icon-info"></text>
</view>
<text class="function-name">关于我们</text>
<text class="iconfont icon-arrow-right"></text>
</view>
<view class="function-item" bindtap="outLogin">
<view class="function-icon">
<text class="iconfont icon-info"></text>
</view>
<text class="function-name">退出登录</text>
<text class="iconfont icon-arrow-right"></text>
</view>
</view>
<!-- 数据统计 -->
<view class="stats-card">
<text class="stats-title">记账统计</text>
<view class="stats-grid">
<view class="stats-item">
<text class="stats-value">{{totalRecords}}</text>
<text class="stats-label">总记录数</text>
</view>
<view class="stats-item">
<text class="stats-value">{{firstRecordDate || '-'}}</text>
<text class="stats-label">首次记账</text>
</view>
<view class="stats-item">
<text class="stats-value">{{consecutiveDays}}</text>
<text class="stats-label">连续记账(天)</text>
</view>
</view>
</view>
</view>

128
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;
}

209
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}`;
}
})

88
pages/statistics/statistics.wxml

@ -0,0 +1,88 @@
<!-- <view class="container"> -->
<!-- 月份选择器 -->
<view class="month-picker">
<picker mode="date" fields="month" value="{{selectedMonth}}" bindchange="onMonthChange">
<view class="picker-view">
<text class="iconfont icon-calendar"></text>
<text>{{selectedMonthText}}</text>
<text class="iconfont icon-arrow-down"></text>
</view>
</picker>
</view>
<!-- 收支概览 -->
<view class="summary-card">
<view class="summary-item">
<text class="summary-label">收入</text>
<text class="summary-amount income">¥ {{summary.income}}</text>
</view>
<view class="summary-item">
<text class="summary-label">支出</text>
<text class="summary-amount expense">¥ {{summary.expense}}</text>
</view>
<view class="summary-item">
<text class="summary-label">结余</text>
<text class="summary-amount balance">¥ {{summary.balance}}</text>
</view>
</view>
<!-- 分类统计 -->
<view class="category-stats">
<view class="section-header">
<text class="section-title">分类统计</text>
<view class="type-tabs">
<view class="tab {{activeType === 'expense' ? 'active' : ''}}" bindtap="setActiveType" data-type="expense">支出</view>
<view class="tab {{activeType === 'income' ? 'active' : ''}}" bindtap="setActiveType" data-type="income">收入</view>
</view>
</view>
<!-- 饼图容器 -->
<view class="chart-container">
<canvas canvas-id="pieChart" class="pie-chart"></canvas>
</view>
<!-- 分类详情 -->
<view class="category-details">
<view wx:for="{{categoryStats}}" wx:key="categoryId" class="category-detail-item">
<view class="category-info">
<view class="category-color" style="background-color: {{item.color}};"></view>
<text class="category-name">{{item.categoryName}}</text>
</view>
<view class="category-amount">
<text>¥ {{item.amount}}</text>
<text class="category-percentage">{{item.percentage}}%</text>
</view>
</view>
<view wx:if="{{categoryStats.length === 0}}" class="no-data">
<text>本月暂无{{activeType === 'expense' ? '支出' : '收入'}}记录</text>
</view>
</view>
</view>
<!-- 记录列表 -->
<view class="records-section">
<view class="section-header">
<text class="section-title">明细记录</text>
</view>
<view class="records-list">
<view wx:for="{{monthRecords}}" wx:key="id" class="record-item">
<view class="record-icon {{item.type === 'income' ? 'income-icon' : 'expense-icon'}}">
<text class="iconfont icon-{{item.categoryIcon}}"></text>
</view>
<view class="record-details">
<view class="record-title">{{item.categoryName}}</view>
<view class="record-note">{{item.note || '无备注'}}</view>
<view class="record-date">{{formatDate(item.date)}}</view>
</view>
<view class="record-amount {{item.type === 'income' ? 'income' : 'expense'}}">
{{item.type === 'income' ? '+' : '-'}}¥ {{item.amount}}
</view>
</view>
<view wx:if="{{monthRecords.length === 0}}" class="no-records">
<text>本月暂无记录</text>
</view>
</view>
</view>

234
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;
}

111
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

25
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": {}
}

14
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
}
}
Loading…
Cancel
Save