@ -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 |
||||
|
} |
||||
|
}) |
@ -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" |
||||
|
} |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.1 KiB |
@ -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); |
||||
|
} |
||||
|
}) |
@ -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> |
@ -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; |
||||
|
} |
||||
|
|
||||
|
|
@ -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 |
||||
|
}); |
||||
|
} |
||||
|
}) |
@ -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> |
@ -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; |
||||
|
} |
@ -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) |
||||
|
}); |
||||
|
} |
||||
|
}) |
@ -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> |
@ -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; |
||||
|
} |
@ -0,0 +1,9 @@ |
|||||
|
{ |
||||
|
onLoad() { |
||||
|
// 延迟500ms跳转(可选,可用于显示小程序logo/slogan)
|
||||
|
setTimeout(() => { |
||||
|
// 调用app.js中的登录检查方法
|
||||
|
getApp().checkLoginStatus() |
||||
|
}, 500) |
||||
|
} |
||||
|
} |
@ -0,0 +1,3 @@ |
|||||
|
{ |
||||
|
"usingComponents": {} |
||||
|
} |
@ -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> |
@ -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; |
||||
|
} |
@ -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' |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"navigationBarTitleText": "登录", |
||||
|
"navigationBarBackgroundColor": "#f5f7fa", |
||||
|
"navigationBarTextStyle": "black", |
||||
|
"backgroundColor": "#f5f7fa", |
||||
|
"usingComponents": {} |
||||
|
} |
@ -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> |
@ -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; |
||||
|
} |
@ -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: '我知道了' |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
}) |
@ -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> |
@ -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; |
||||
|
} |
@ -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}日`; |
||||
|
} |
||||
|
}) |
@ -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> |
@ -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; |
||||
|
} |
@ -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 |
||||
|
|
@ -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": {} |
||||
|
} |
@ -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 |
||||
|
} |
||||
|
} |