From c747a31b4eca2c1b713517b03fb5dd8c062fc8af Mon Sep 17 00:00:00 2001 From: wangsai <1534599297@qq.com> Date: Thu, 25 Sep 2025 16:40:07 +0800 Subject: [PATCH] 11 --- app.json | 1 + images/left-arrow.png | Bin 0 -> 2489 bytes images/right-arrow.png | Bin 0 -> 2492 bytes pages/add/add.js | 4 +- pages/index/index.js | 20 +- pages/index/index.wxml | 2 +- pages/profile/profile.wxml | 16 +- pages/statistics/statistics.js | 194 ++++++++--- pages/statistics/statistics.wxml | 66 ++-- pages/statistics/statistics.wxss | 61 ++++ pages/statistics2/statistics.js | 380 ++++++++++++++++++++ pages/statistics2/statistics.json | 6 + pages/statistics2/statistics.wxml | 161 +++++++++ pages/statistics2/statistics.wxss | 343 ++++++++++++++++++ pages/utils/request.js | 4 + pages/utils/wx-chart.js | 560 ++++++++++++++++++++++++++++++ project.private.config.json | 2 +- 17 files changed, 1738 insertions(+), 82 deletions(-) create mode 100644 images/left-arrow.png create mode 100644 images/right-arrow.png create mode 100644 pages/statistics2/statistics.js create mode 100644 pages/statistics2/statistics.json create mode 100644 pages/statistics2/statistics.wxml create mode 100644 pages/statistics2/statistics.wxss create mode 100644 pages/utils/wx-chart.js diff --git a/app.json b/app.json index fdca928..3ed5a01 100644 --- a/app.json +++ b/app.json @@ -4,6 +4,7 @@ "pages/index/index", "pages/add/add", "pages/statistics/statistics", + "pages/statistics2/statistics", "pages/category/category", "pages/profile/profile" ], diff --git a/images/left-arrow.png b/images/left-arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..c190a26c0a6c4b1212580d37db02ce803554704a GIT binary patch literal 2489 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 new Date(b.date) - new Date(a.date)) - .slice(0, 10) + recentRecords: records }); } }) diff --git a/pages/index/index.wxml b/pages/index/index.wxml index 46f4463..bc5ab5b 100644 --- a/pages/index/index.wxml +++ b/pages/index/index.wxml @@ -25,7 +25,7 @@ 近期记录 - 查看全部 + diff --git a/pages/profile/profile.wxml b/pages/profile/profile.wxml index 99978fe..602ce9e 100644 --- a/pages/profile/profile.wxml +++ b/pages/profile/profile.wxml @@ -12,37 +12,37 @@ - + - + - + - + diff --git a/pages/statistics/statistics.js b/pages/statistics/statistics.js index 6cce705..a6743de 100644 --- a/pages/statistics/statistics.js +++ b/pages/statistics/statistics.js @@ -1,5 +1,16 @@ +const request = require('../utils/request') Page({ data: { + // 当前时间类型:year, month, quarter + currentTimeType: 'year', + // 当前周期文本 + currentPeriodText: '2023年', + // 当前时间 + currentDate: { + year: 2023, + month: 1, + quarter: 1 + }, selectedMonth: '', selectedMonthText: '', summary: { @@ -12,7 +23,7 @@ Page({ monthRecords: [] }, - onLoad() { + onShow() { // 初始化当前月份 const today = new Date(); const year = today.getFullYear(); @@ -21,12 +32,115 @@ Page({ this.setData({ selectedMonth, - selectedMonthText: `${year}年${month}月` + selectedMonthText: `${year}年${month}月`, + currentPeriodText:`${year}年`, + currentDate: { + year, + month + } }); this.updateStatistics(); }, - + // 上一个周期 + prevPeriod() { + const { currentDate, currentTimeType } = this.data; + const newDate = { ...currentDate }; + + switch (currentTimeType) { + case 'year': + newDate.year--; + break; + case 'month': + newDate.month--; + if (newDate.month < 1) { + newDate.month = 12; + newDate.year--; + } + break; + case 'quarter': + newDate.quarter--; + if (newDate.quarter < 1) { + newDate.quarter = 4; + newDate.year--; + } + break; + } + + this.setData({ + currentDate: newDate + }, () => { + this.updatePeriodDisplay(); + // 实际应用中这里应该根据新日期请求数据 + }); + }, + + // 下一个周期 + nextPeriod() { + const { currentDate, currentTimeType } = this.data; + const newDate = { ...currentDate }; + + switch (currentTimeType) { + case 'year': + newDate.year++; + break; + case 'month': + newDate.month++; + if (newDate.month > 12) { + newDate.month = 1; + newDate.year++; + } + break; + case 'quarter': + newDate.quarter++; + if (newDate.quarter > 4) { + newDate.quarter = 1; + newDate.year++; + } + break; + } + + this.setData({ + currentDate: newDate + }, () => { + this.updatePeriodDisplay(); + // 实际应用中这里应该根据新日期请求数据 + }); + }, + // 切换时间类型 + changeTimeType(e) { + const type = e.currentTarget.dataset.type; + if (this.data.currentTimeType === type) return; + + this.setData({ + currentTimeType: type + }, () => { + + this.updatePeriodDisplay(); + + }); + }, + // 更新时间周期显示 + updatePeriodDisplay() { + const { year, month, quarter } = this.data.currentDate; + let text = ''; + + switch (this.data.currentTimeType) { + case 'year': + text = `${year}年`; + break; + case 'month': + text = `${year}年${month}月`; + break; + case 'quarter': + text = `${year}年第${quarter}季度`; + break; + } + + this.setData({ + currentPeriodText: text + }); + }, onMonthChange(e) { const selectedMonth = e.detail.value; const [year, month] = selectedMonth.split('-'); @@ -42,36 +156,30 @@ Page({ 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('-'); - + async updateStatistics() { // 筛选当月记录 - 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)); - + const res = await request.get('/app-api/book/inout/allList') + let monthRecords=[] + if(res.code==0){ + monthRecords=res.data + } // 计算收支汇总 let income = 0; let expense = 0; - monthRecords.forEach(record => { - if (record.type === 'income') { - income += parseFloat(record.amount); - } else { - expense += parseFloat(record.amount); + const res1 = await request.get('/app-api/book/inout/myList-tol',{type:"in"}) + console.log('获取数据成功:', res1) + if(res1.code==0){ + income=res1.data } - }); + const res2 = await request.get('/app-api/book/inout/myList-tol',{type:"out"}) + console.log('获取数据成功:', res2) + if(res2.code==0){ + expense=res2.data + } const balance = income - expense; @@ -84,30 +192,27 @@ Page({ } }, () => { this.calculateCategoryStats(); - this.drawPieChart(); }); }, - calculateCategoryStats() { + async 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 + const res3 = await request.get('/app-api/book/classification/group-one',{type:activeType=='expense'?"out":"in"}) + console.log('获取数据成功:', res3) + if(res3.code==0){ + res3.data.forEach(record => { + categoryMap[record.id] = { + categoryId: record.id, + categoryName: record.classificationName, + amount: record.money, + count: record.num, }; - } - - categoryMap[record.categoryId].amount += parseFloat(record.amount); - categoryMap[record.categoryId].count += 1; - }); + }); + } + // 转换为数组并排序 let categoryStats = Object.values(categoryMap); @@ -132,13 +237,14 @@ Page({ }); this.setData({ categoryStats }); + this.drawPieChart(); }, drawPieChart() { const { categoryStats } = this.data; - + console.log("drawPieChart:",categoryStats) if (categoryStats.length === 0) { - return; + // return; } const ctx = wx.createCanvasContext('pieChart', this); @@ -191,11 +297,11 @@ Page({ ctx.setFillStyle('#333'); ctx.setTextAlign('center'); ctx.setTextBaseline('middle'); - ctx.fillText(`总计`, centerX, centerY - 10); + // ctx.fillText(`总计`, centerX, centerY - 10); ctx.setFontSize(18); ctx.setFillStyle(this.data.activeType === 'income' ? '#4CAF50' : '#f44336'); - ctx.fillText(`¥${total}`, centerX, centerY + 15); + // ctx.fillText(`¥${total}`, centerX, centerY + 15); ctx.draw(); }, diff --git a/pages/statistics/statistics.wxml b/pages/statistics/statistics.wxml index 0f43ccb..8b27e62 100644 --- a/pages/statistics/statistics.wxml +++ b/pages/statistics/statistics.wxml @@ -1,13 +1,43 @@ - - - - - - - {{selectedMonthText}} - - - + + + + + + + + + + + + + + {{currentPeriodText}} + + @@ -20,10 +50,6 @@ 支出 ¥ {{summary.expense}} - - 结余 - ¥ {{summary.balance}} - @@ -68,16 +94,16 @@ - + - {{item.categoryName}} - {{item.note || '无备注'}} - {{formatDate(item.date)}} + {{item.classificationName}} + {{item.remark || '无备注'}} + {{item.useDate}} - - {{item.type === 'income' ? '+' : '-'}}¥ {{item.amount}} + + {{item.type === 'in' ? '+' : '-'}}¥ {{item.money}} diff --git a/pages/statistics/statistics.wxss b/pages/statistics/statistics.wxss index 4c7695a..64a4d03 100644 --- a/pages/statistics/statistics.wxss +++ b/pages/statistics/statistics.wxss @@ -232,3 +232,64 @@ color: #999; font-size: 14px; } + + +/* 时间筛选 */ +.time-filter { + margin: 20rpx 0; +} + +.time-btns { + display: flex; + justify-content: center; + background-color: #f0f0f0; + border-radius: 60rpx; + padding: 6rpx; + width: 600rpx; + margin: 0 auto; +} + +.time-btn { + flex: 1; + height: 72rpx; + line-height: 72rpx; + border-radius: 50rpx; + background-color: transparent; + font-size: 30rpx; + font-weight: 500; + color: #666; +} + +.time-btn-active { + background-color: #3B82F6; + color: #fff; +} + +/* 当前时间选择 */ +.current-period { + margin: 30rpx 0; +} + +.period-container { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 10rpx; + padding: 20px 0; +} + + + +.arrow-icon { + width: 30rpx; + height: 30rpx; +} + +.period-text { + font-size: 30rpx; + font-weight: 600; + color: #333; + margin: 0 20rpx; +} + diff --git a/pages/statistics2/statistics.js b/pages/statistics2/statistics.js new file mode 100644 index 0000000..4b0bf8a --- /dev/null +++ b/pages/statistics2/statistics.js @@ -0,0 +1,380 @@ +// 引入wx-chart图表库 +import WxChart from '../utils/wx-chart.js'; + +Page({ + data: { + // 当前时间类型:year, month, quarter + currentTimeType: 'year', + // 当前周期文本 + currentPeriodText: '2023年', + // 当前选中的饼图类型:income, expense + currentPieType: 'income', + // 收入总额 + incomeTotal: '¥128,500.00', + // 支出总额 + expenseTotal: '¥85,300.00', + // 当前选中的标签页 + currentTab: 'statistics', + // 饼图图例数据 + currentPieLegend: [], + + // 图表实例 + trendChart: null, + categoryChart: null, + + // 时间数据 + timeData: { + year: { + labels: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], + income: [8500, 9200, 10500, 9800, 11200, 12500, 11800, 13200, 12800, 14500, 13800, 15200], + expense: [6200, 5800, 6500, 7200, 6800, 7500, 8200, 7800, 8500, 7900, 8200, 8800], + totalIncome: 128500, + totalExpense: 85300 + }, + month: { + labels: ['第1周', '第2周', '第3周', '第4周', '第5周'], + income: [3200, 3800, 3500, 4200, 2800], + expense: [2100, 2500, 1900, 2300, 1800], + totalIncome: 17500, + totalExpense: 10600 + }, + quarter: { + labels: ['Q1', 'Q2', 'Q3', 'Q4'], + income: [28200, 33500, 37800, 29000], + expense: [18500, 21800, 24500, 20500], + totalIncome: 128500, + totalExpense: 85300 + } + }, + + // 分类数据 + categoryData: { + income: { + categories: ['工资', '奖金', '投资收益', '兼职', '其他'], + data: [80000, 20000, 15000, 10000, 3500], + colors: [ + 'rgba(59, 130, 246, 0.8)', + 'rgba(59, 130, 246, 0.7)', + 'rgba(59, 130, 246, 0.6)', + 'rgba(59, 130, 246, 0.5)', + 'rgba(59, 130, 246, 0.4)' + ] + }, + expense: { + categories: ['餐饮', '住房', '交通', '购物', '娱乐', '其他'], + data: [25000, 30000, 8000, 12000, 5300, 5000], + colors: [ + 'rgba(239, 68, 68, 0.8)', + 'rgba(239, 68, 68, 0.7)', + 'rgba(239, 68, 68, 0.6)', + 'rgba(239, 68, 68, 0.5)', + 'rgba(239, 68, 68, 0.4)', + 'rgba(239, 68, 68, 0.3)' + ] + } + }, + + // 当前时间 + currentDate: { + year: 2023, + month: 1, + quarter: 1 + } + }, + + onLoad() { + // 初始化图表 + this.initCharts(); + // 初始化饼图图例 + this.updatePieLegend('income'); + }, + + onReady() { + // 页面渲染完成后执行 + }, + + // 初始化图表 + initCharts() { + // 初始化趋势图 + this.initTrendChart(); + // 初始化饼图 + this.initPieChart(); + }, + + // 初始化趋势图 + initTrendChart() { + const ctx = wx.createCanvasContext('trendChart', this); + const data = this.data.timeData[this.data.currentTimeType]; + + this.data.trendChart = new WxChart({ + canvasId: 'trendChart', + type: 'line', + categories: data.labels, + series: [ + { + name: '收入', + data: data.income, + color: '#3B82F6', + format: function (val) { + return '¥' + val.toFixed(0); + } + }, + { + name: '支出', + data: data.expense, + color: '#EF4444', + format: function (val) { + return '¥' + val.toFixed(0); + } + } + ], + yAxis: { + title: '金额 (¥)', + format: function (val) { + return val.toLocaleString(); + }, + min: 0 + }, + xAxis: { + disableGrid: true + }, + extra: { + lineStyle: 'curve' + } + }); + }, + + // 初始化饼图 + initPieChart() { + const ctx = wx.createCanvasContext('categoryChart', this); + const data = this.data.categoryData[this.data.currentPieType]; + + this.data.categoryChart = new WxChart({ + canvasId: 'categoryChart', + type: 'pie', + series: data.data, + labels: data.categories, + colors: data.colors, + extra: { + pie: { + offsetAngle: -90, + radius: 80 + } + }, + format: function (val, name) { + const total = data.data.reduce((a, b) => a + b, 0); + const percentage = Math.round((val / total) * 100) + '%'; + return `${name}: ¥${val.toLocaleString()} (${percentage})`; + } + }); + }, + + // 切换时间类型 + changeTimeType(e) { + const type = e.currentTarget.dataset.type; + if (this.data.currentTimeType === type) return; + + this.setData({ + currentTimeType: type + }, () => { + // 更新时间周期显示 + this.updatePeriodDisplay(); + // 更新趋势图数据 + this.updateTrendChart(); + // 更新总额显示 + this.updateTotalAmounts(); + }); + }, + + // 更新时间周期显示 + updatePeriodDisplay() { + const { year, month, quarter } = this.data.currentDate; + let text = ''; + + switch (this.data.currentTimeType) { + case 'year': + text = `${year}年`; + break; + case 'month': + text = `${year}年${month}月`; + break; + case 'quarter': + text = `${year}年第${quarter}季度`; + break; + } + + this.setData({ + currentPeriodText: text + }); + }, + + // 更新趋势图数据 + updateTrendChart() { + const data = this.data.timeData[this.data.currentTimeType]; + + this.data.trendChart.updateData({ + categories: data.labels, + series: [ + { + name: '收入', + data: data.income + }, + { + name: '支出', + data: data.expense + } + ] + }); + }, + + // 更新总额显示 + updateTotalAmounts() { + const data = this.data.timeData[this.data.currentTimeType]; + + this.setData({ + incomeTotal: this.formatCurrency(data.totalIncome), + expenseTotal: this.formatCurrency(data.totalExpense) + }); + }, + + // 切换饼图类型 + changePieType(e) { + const type = e.currentTarget.dataset.type; + if (this.data.currentPieType === type) return; + + this.setData({ + currentPieType: type + }, () => { + // 更新饼图数据 + this.updatePieChart(); + // 更新饼图图例 + this.updatePieLegend(type); + }); + }, + + // 更新饼图数据 + updatePieChart() { + const data = this.data.categoryData[this.data.currentPieType]; + + this.data.categoryChart.updateData({ + series: data.data, + labels: data.categories, + colors: data.colors + }); + }, + + // 更新饼图图例 + updatePieLegend(type) { + const data = this.data.categoryData[type]; + const total = data.data.reduce((a, b) => a + b, 0); + const legend = []; + + data.categories.forEach((category, index) => { + const value = data.data[index]; + const percentage = Math.round((value / total) * 100) + '%'; + + legend.push({ + name: category, + value: this.formatCurrency(value), + percent: percentage, + color: data.colors[index] + }); + }); + + this.setData({ + currentPieLegend: legend + }); + }, + + // 上一个周期 + prevPeriod() { + const { currentDate, currentTimeType } = this.data; + const newDate = { ...currentDate }; + + switch (currentTimeType) { + case 'year': + newDate.year--; + break; + case 'month': + newDate.month--; + if (newDate.month < 1) { + newDate.month = 12; + newDate.year--; + } + break; + case 'quarter': + newDate.quarter--; + if (newDate.quarter < 1) { + newDate.quarter = 4; + newDate.year--; + } + break; + } + + this.setData({ + currentDate: newDate + }, () => { + this.updatePeriodDisplay(); + // 实际应用中这里应该根据新日期请求数据 + }); + }, + + // 下一个周期 + nextPeriod() { + const { currentDate, currentTimeType } = this.data; + const newDate = { ...currentDate }; + + switch (currentTimeType) { + case 'year': + newDate.year++; + break; + case 'month': + newDate.month++; + if (newDate.month > 12) { + newDate.month = 1; + newDate.year++; + } + break; + case 'quarter': + newDate.quarter++; + if (newDate.quarter > 4) { + newDate.quarter = 1; + newDate.year++; + } + break; + } + + this.setData({ + currentDate: newDate + }, () => { + this.updatePeriodDisplay(); + // 实际应用中这里应该根据新日期请求数据 + }); + }, + + // 格式化货币 + formatCurrency(value) { + return '¥' + value.toLocaleString('zh-CN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + }, + + // 趋势图点击事件 + touchTrendChart(e) { + this.data.trendChart.showToolTip(e, { + format: function (item, category) { + return `${category} ${item.name}: ${item.data}`; + } + }); + }, + + // 饼图点击事件 + touchPieChart(e) { + this.data.categoryChart.showToolTip(e, { + format: function (item, category) { + return `${category}: ${item.data}`; + } + }); + } +}); diff --git a/pages/statistics2/statistics.json b/pages/statistics2/statistics.json new file mode 100644 index 0000000..cfb947b --- /dev/null +++ b/pages/statistics2/statistics.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "收支统计", + "navigationBarBackgroundColor": "#ffffff", + "navigationBarTextStyle": "black", + "enablePullDownRefresh": false +} diff --git a/pages/statistics2/statistics.wxml b/pages/statistics2/statistics.wxml new file mode 100644 index 0000000..752fb36 --- /dev/null +++ b/pages/statistics2/statistics.wxml @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + {{currentPeriodText}} + + + + + + + + + 收入总额 + + + + + {{incomeTotal}} + + + 12.5% + 较上期 + + + + + + 支出总额 + + + + + {{expenseTotal}} + + + 8.2% + 较上期 + + + + + + + + 收支趋势 + + + + 收入 + + + + 支出 + + + + + + + + + + 收支分类 + + + + + + + + + + + + + + {{item.name}} + {{item.value}} + {{item.percent}} + + + + + + + + + + + + 首页 + + + + 统计 + + + + + + + 明细 + + + + 我的 + + diff --git a/pages/statistics2/statistics.wxss b/pages/statistics2/statistics.wxss new file mode 100644 index 0000000..77fbe19 --- /dev/null +++ b/pages/statistics2/statistics.wxss @@ -0,0 +1,343 @@ +/* 基础样式 */ +.container { + padding: 20rpx; + background-color: #f5f5f5; + min-height: calc(100vh - 100rpx); + box-sizing: border-box; +} + +/* 时间筛选 */ +.time-filter { + margin: 20rpx 0; +} + +.time-btns { + display: flex; + justify-content: center; + background-color: #f0f0f0; + border-radius: 60rpx; + padding: 6rpx; + width: 600rpx; + margin: 0 auto; +} + +.time-btn { + flex: 1; + height: 72rpx; + line-height: 72rpx; + border-radius: 50rpx; + background-color: transparent; + font-size: 30rpx; + font-weight: 500; + color: #666; +} + +.time-btn-active { + background-color: #3B82F6; + color: #fff; +} + +/* 当前时间选择 */ +.current-period { + margin: 30rpx 0; +} + +.period-container { + display: flex; + align-items: center; + justify-content: center; + background-color: #fff; + border-radius: 20rpx; + padding: 20rpx; + width: 400rpx; + margin: 0 auto; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.period-btn { + width: 60rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; + background: transparent; +} + +.arrow-icon { + width: 30rpx; + height: 30rpx; +} + +.period-text { + font-size: 34rpx; + font-weight: 600; + color: #333; + margin: 0 20rpx; +} + +/* 总额卡片 */ +.total-cards { + display: flex; + justify-content: space-between; + margin: 30rpx 0; + gap: 20rpx; +} + +.card { + flex: 1; + background-color: #fff; + border-radius: 20rpx; + padding: 30rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 20rpx; +} + +.card-label { + font-size: 28rpx; + color: #666; +} + +.card-icon { + width: 60rpx; + height: 60rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.income-icon { + background-color: rgba(59, 130, 246, 0.1); +} + +.expense-icon { + background-color: rgba(239, 68, 68, 0.1); +} + +.icon-img { + width: 30rpx; + height: 30rpx; +} + +.card-amount { + font-size: 40rpx; + font-weight: 700; + margin-bottom: 15rpx; +} + +.income-card .card-amount { + color: #3B82F6; +} + +.expense-card .card-amount { + color: #EF4444; +} + +.card-change { + display: flex; + align-items: center; + font-size: 24rpx; +} + +.change-icon { + margin-right: 5rpx; +} + +.positive-change .change-icon, +.positive-change .change-value { + color: #10B981; +} + +.negative-change .change-icon, +.negative-change .change-value { + color: #EF4444; +} + +.change-desc { + color: #999; + margin-left: 5rpx; +} + +/* 图表容器 */ +.chart-container { + background-color: #fff; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 30rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.chart-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30rpx; +} + +.chart-title { + font-size: 32rpx; + font-weight: 600; + color: #333; +} + +.chart-legend { + display: flex; + gap: 20rpx; +} + +.legend-item { + display: flex; + align-items: center; + font-size: 26rpx; + color: #666; +} + +.legend-color { + width: 20rpx; + height: 20rpx; + border-radius: 50%; + margin-right: 10rpx; +} + +.income-color { + background-color: #3B82F6; +} + +.expense-color { + background-color: #EF4444; +} + +.chart-canvas { + width: 100%; + height: 400rpx; +} + +/* 饼图部分 */ +.pie-type-switch { + display: flex; + background-color: #f0f0f0; + border-radius: 40rpx; + padding: 4rpx; +} + +.type-btn { + padding: 0 20rpx; + height: 56rpx; + line-height: 56rpx; + border-radius: 30rpx; + background-color: transparent; + font-size: 26rpx; + color: #666; +} + +.type-btn-active { + background-color: #3B82F6; + color: #fff; +} + +.pie-content { + display: flex; + flex-direction: column; +} + +.pie-canvas { + width: 100%; + height: 300rpx; + margin: 0 auto; +} + +.pie-legend { + margin-top: 20rpx; +} + +.legend-list { + display: flex; + flex-direction: column; + gap: 15rpx; +} + +.pie-legend .legend-item { + justify-content: space-between; + padding: 5rpx 0; + border-bottom: 1rpx solid #f0f0f0; +} + +.pie-legend .legend-item:last-child { + border-bottom: none; +} + +.legend-value { + font-weight: 500; + margin-right: 15rpx; +} + +.legend-percent { + color: #999; +} + +/* 底部导航 */ +.tab-bar { + display: flex; + justify-content: space-around; + align-items: center; + height: 100rpx; + background-color: #fff; + border-top: 1rpx solid #eee; + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 999; +} + +.tab-bar-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; +} + +.tab-bar-icon { + width: 44rpx; + height: 44rpx; + margin-bottom: 8rpx; +} + +.tab-bar-text { + font-size: 20rpx; + color: #999; +} + +.tab-bar-text-active { + color: #3B82F6; +} + +.add-btn { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + background-color: #3B82F6; + display: flex; + align-items: center; + justify-content: center; + margin-top: -40rpx; + box-shadow: 0 4rpx 10rpx rgba(59, 130, 246, 0.3); +} + +.add-btn .tab-bar-icon { + width: 40rpx; + height: 40rpx; + margin-bottom: 0; +} + +.currentTab { + color: #3B82F6; +} diff --git a/pages/utils/request.js b/pages/utils/request.js index 6c3bac6..deb9f17 100644 --- a/pages/utils/request.js +++ b/pages/utils/request.js @@ -17,6 +17,10 @@ const request = (url, options = {}) => { if (token) { header['Authorization'] = `Bearer ${token}` } + header['terminal'] = 10; + + header['Accept'] = '*/*'; + header['tenant-id'] = 1; // 合并请求配置 const config = { url:baseUrl+url, diff --git a/pages/utils/wx-chart.js b/pages/utils/wx-chart.js new file mode 100644 index 0000000..6d24b00 --- /dev/null +++ b/pages/utils/wx-chart.js @@ -0,0 +1,560 @@ +/** + * wx-chart.js v1.0.0 + * 微信小程序图表库 + * 简化版,支持折线图和饼图 + */ +class WxChart { + constructor(options) { + this.canvasId = options.canvasId; + this.ctx = wx.createCanvasContext(this.canvasId, options.context); + this.type = options.type || 'line'; + this.categories = options.categories || []; + this.series = options.series || []; + this.labels = options.labels || []; + this.colors = options.colors || this.getDefaultColors(); + this.width = options.width || 300; + this.height = options.height || 200; + this.yAxis = options.yAxis || {}; + this.xAxis = options.xAxis || {}; + this.extra = options.extra || {}; + this.format = options.format || function (val) { return val; }; + + // 初始化画布尺寸 + this.initCanvasSize(); + + // 绘制图表 + this.draw(); + } + + // 获取默认颜色 + getDefaultColors() { + return [ + '#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', + '#EC4899', '#6366F1', '#14B8A6', '#F97316', '#64748B' + ]; + } + + // 初始化画布尺寸 + initCanvasSize() { + const query = wx.createSelectorQuery(); + query.select(`#${this.canvasId}`) + .boundingClientRect() + .exec((res) => { + if (res && res[0]) { + this.width = res[0].width; + this.height = res[0].height; + this.draw(); + } + }); + } + + // 更新数据 + updateData(data) { + if (data.categories) this.categories = data.categories; + if (data.series) this.series = data.series; + if (data.labels) this.labels = data.labels; + if (data.colors) this.colors = data.colors; + + this.draw(); + } + + // 绘制图表 + draw() { + switch (this.type) { + case 'line': + this.drawLineChart(); + break; + case 'pie': + this.drawPieChart(); + break; + } + } + + // 绘制折线图 + drawLineChart() { + const ctx = this.ctx; + const { width, height } = this; + const padding = 40; // 边距 + const innerWidth = width - padding * 2; + const innerHeight = height - padding * 2; + + // 清除画布 + ctx.clearRect(0, 0, width, height); + + // 绘制坐标轴 + this.drawAxis(ctx, padding, innerWidth, innerHeight); + + // 计算数据范围 + const dataRange = this.calculateDataRange(); + const yStep = innerHeight / (dataRange.max - dataRange.min); + + // 计算X轴步长 + const xStep = this.categories.length > 1 ? innerWidth / (this.categories.length - 1) : innerWidth; + + // 绘制网格线 + this.drawGridLines(ctx, padding, innerWidth, innerHeight, xStep, yStep, dataRange); + + // 绘制数据线和点 + this.series.forEach((series, index) => { + const color = series.color || this.colors[index % this.colors.length]; + + // 绘制区域填充 + if (this.extra && this.extra.fill) { + this.drawAreaFill(ctx, padding, innerHeight, xStep, yStep, dataRange, series.data, color); + } + + // 绘制线 + this.drawLine(ctx, padding, innerHeight, xStep, yStep, dataRange, series.data, color); + + // 绘制点 + this.drawPoints(ctx, padding, innerHeight, xStep, yStep, dataRange, series.data, color); + }); + + // 绘制X轴标签 + this.drawXAxisLabels(ctx, padding, innerWidth, innerHeight, xStep); + + // 绘制Y轴标签 + this.drawYAxisLabels(ctx, padding, innerHeight, yStep, dataRange); + + // 绘制Y轴标题 + if (this.yAxis.title) { + this.drawYAxisTitle(ctx, padding, innerHeight); + } + + // 绘制完成 + ctx.draw(); + } + + // 绘制饼图 + drawPieChart() { + const ctx = this.ctx; + const { width, height } = this; + const centerX = width / 2; + const centerY = height / 2; + const radius = this.extra && this.extra.pie && this.extra.pie.radius || Math.min(width, height) / 3; + const offsetAngle = this.extra && this.extra.pie && this.extra.pie.offsetAngle || 0; + + // 清除画布 + ctx.clearRect(0, 0, width, height); + + // 计算总和 + const total = this.series.reduce((sum, value) => sum + value, 0); + let startAngle = offsetAngle * Math.PI / 180; + + // 绘制饼图 + this.series.forEach((value, index) => { + const percentage = value / total; + const endAngle = startAngle + percentage * 2 * Math.PI; + const color = this.colors[index % this.colors.length]; + + // 绘制扇形 + ctx.beginPath(); + ctx.setFillStyle(color); + ctx.moveTo(centerX, centerY); + ctx.arc(centerX, centerY, radius, startAngle, endAngle); + ctx.closePath(); + ctx.fill(); + + startAngle = endAngle; + }); + + // 绘制中间空白 + if (this.extra && this.extra.pie && this.extra.pie.holeRadius) { + const holeRadius = this.extra.pie.holeRadius; + ctx.beginPath(); + ctx.setFillStyle('#ffffff'); + ctx.arc(centerX, centerY, holeRadius, 0, 2 * Math.PI); + ctx.fill(); + } + + // 绘制完成 + ctx.draw(); + } + + // 计算数据范围 + calculateDataRange() { + let min = Infinity; + let max = -Infinity; + + this.series.forEach(series => { + series.data.forEach(value => { + if (value < min) min = value; + if (value > max) max = value; + }); + }); + + // 如果设置了Y轴最小值,使用设置的值 + if (this.yAxis.min !== undefined) { + min = this.yAxis.min; + } else if (min > 0) { + min = 0; // 如果最小值大于0,从0开始 + } + + // 添加一些边距 + const padding = (max - min) * 0.1; + return { + min: min - padding, + max: max + padding + }; + } + + // 绘制坐标轴 + drawAxis(ctx, padding, innerWidth, innerHeight) { + ctx.beginPath(); + ctx.setStrokeStyle('#e5e5e5'); + ctx.setLineWidth(1); + + // X轴 + ctx.moveTo(padding, padding + innerHeight); + ctx.lineTo(padding + innerWidth, padding + innerHeight); + + // Y轴 + ctx.moveTo(padding, padding); + ctx.lineTo(padding, padding + innerHeight); + + ctx.stroke(); + } + + // 绘制网格线 + drawGridLines(ctx, padding, innerWidth, innerHeight, xStep, yStep, dataRange) { + ctx.beginPath(); + ctx.setStrokeStyle('#f0f0f0'); + ctx.setLineWidth(1); + + // 垂直网格线 + if (!this.xAxis.disableGrid) { + this.categories.forEach((category, index) => { + const x = padding + index * xStep; + ctx.moveTo(x, padding); + ctx.lineTo(x, padding + innerHeight); + }); + } + + // 水平网格线 (5条) + for (let i = 0; i <= 5; i++) { + const y = padding + innerHeight - (i * innerHeight / 5); + ctx.moveTo(padding, y); + ctx.lineTo(padding + innerWidth, y); + } + + ctx.stroke(); + } + + // 绘制区域填充 + drawAreaFill(ctx, padding, innerHeight, xStep, yStep, dataRange, data, color) { + ctx.beginPath(); + ctx.setFillStyle(color.replace('rgb', 'rgba').replace(')', ', 0.1)')); + + // 起点 + ctx.moveTo(padding, padding + innerHeight); + + // 绘制数据点 + data.forEach((value, index) => { + const x = padding + index * xStep; + const y = padding + innerHeight - (value - dataRange.min) * yStep; + ctx.lineTo(x, y); + }); + + // 终点 + ctx.lineTo(padding + (data.length - 1) * xStep, padding + innerHeight); + ctx.closePath(); + ctx.fill(); + } + + // 绘制线 + drawLine(ctx, padding, innerHeight, xStep, yStep, dataRange, data, color) { + ctx.beginPath(); + ctx.setStrokeStyle(color); + ctx.setLineWidth(2); + + data.forEach((value, index) => { + const x = padding + index * xStep; + const y = padding + innerHeight - (value - dataRange.min) * yStep; + + if (index === 0) { + ctx.moveTo(x, y); + } else { + // 支持曲线 + if (this.extra && this.extra.lineStyle === 'curve' && index > 0 && index < data.length - 1) { + const nextX = padding + (index + 1) * xStep; + const nextY = padding + innerHeight - (data[index + 1] - dataRange.min) * yStep; + ctx.quadraticCurveTo(x, y, (x + nextX) / 2, (y + nextY) / 2); + } else { + ctx.lineTo(x, y); + } + } + }); + + ctx.stroke(); + } + + // 绘制点 + drawPoints(ctx, padding, innerHeight, xStep, yStep, dataRange, data, color) { + data.forEach((value, index) => { + const x = padding + index * xStep; + const y = padding + innerHeight - (value - dataRange.min) * yStep; + + // 绘制外圆 + ctx.beginPath(); + ctx.setFillStyle(color); + ctx.arc(x, y, 4, 0, 2 * Math.PI); + ctx.fill(); + + // 绘制内圆 + ctx.beginPath(); + ctx.setFillStyle('#ffffff'); + ctx.arc(x, y, 2, 0, 2 * Math.PI); + ctx.fill(); + }); + } + + // 绘制X轴标签 + drawXAxisLabels(ctx, padding, innerWidth, innerHeight, xStep) { + ctx.setFontSize(12); + ctx.setFillStyle('#666666'); + ctx.setTextAlign('center'); + ctx.setTextBaseline('top'); + + this.categories.forEach((category, index) => { + const x = padding + index * xStep; + const y = padding + innerHeight + 5; + ctx.fillText(category, x, y); + }); + } + + // 绘制Y轴标签 + drawYAxisLabels(ctx, padding, innerHeight, yStep, dataRange) { + ctx.setFontSize(12); + ctx.setFillStyle('#666666'); + ctx.setTextAlign('right'); + ctx.setTextBaseline('middle'); + + // 5个标签 + for (let i = 0; i <= 5; i++) { + const value = dataRange.min + (dataRange.max - dataRange.min) * (i / 5); + const y = padding + innerHeight - (i * innerHeight / 5); + const text = this.yAxis.format ? this.yAxis.format(value) : value.toFixed(0); + + ctx.fillText(text, padding - 5, y); + } + } + + // 绘制Y轴标题 + drawYAxisTitle(ctx, padding, innerHeight) { + ctx.setFontSize(14); + ctx.setFillStyle('#333333'); + ctx.setTextAlign('center'); + ctx.setTextBaseline('middle'); + + // 旋转文本 + ctx.save(); + ctx.translate(padding - 30, padding + innerHeight / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillText(this.yAxis.title, 0, 0); + ctx.restore(); + } + + // 显示工具提示 + showToolTip(e, options) { + const touch = e.touches[0]; + const canvasRect = wx.createSelectorQuery().select(`#${this.canvasId}`).boundingClientRect(); + + canvasRect.exec((res) => { + if (!res || !res[0]) return; + + const rect = res[0]; + const x = touch.x - rect.left; + const y = touch.y - rect.top; + + // 根据图表类型处理 + if (this.type === 'line') { + this.showLineToolTip(x, y, options); + } else if (this.type === 'pie') { + this.showPieToolTip(x, y, options); + } + }); + } + + // 显示折线图工具提示 + showLineToolTip(x, y, options) { + const padding = 40; + const innerWidth = this.width - padding * 2; + const innerHeight = this.height - padding * 2; + const xStep = this.categories.length > 1 ? innerWidth / (this.categories.length - 1) : innerWidth; + const dataRange = this.calculateDataRange(); + const yStep = innerHeight / (dataRange.max - dataRange.min); + + // 找到最近的点 + let closestIndex = 0; + let minDistance = Infinity; + + this.categories.forEach((category, index) => { + const pointX = padding + index * xStep; + const distance = Math.abs(x - pointX); + + if (distance < minDistance) { + minDistance = distance; + closestIndex = index; + } + }); + + // 如果距离太远,不显示 + if (minDistance > 30) return; + + // 准备提示信息 + const category = this.categories[closestIndex]; + const items = this.series.map((series, index) => { + return { + name: series.name, + data: this.format(series.data[closestIndex], series.name), + color: series.color || this.colors[index % this.colors.length] + }; + }); + + // 显示提示 + this.drawToolTip( + padding + closestIndex * xStep, + padding + innerHeight - (this.series[0].data[closestIndex] - dataRange.min) * yStep, + category, + items, + options + ); + } + + // 显示饼图工具提示 + showPieToolTip(x, y, options) { + const centerX = this.width / 2; + const centerY = this.height / 2; + const radius = this.extra && this.extra.pie && this.extra.pie.radius || Math.min(this.width, this.height) / 3; + + // 计算点击位置到中心的距离 + const dx = x - centerX; + const dy = y - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + // 如果不在饼图范围内,不显示 + if (distance > radius) return; + + // 计算角度 + let angle = Math.atan2(dy, dx) * 180 / Math.PI; + angle = (angle + 360) % 360; // 转换为0-360度 + + // 计算总和和每个部分的角度 + const total = this.series.reduce((sum, value) => sum + value, 0); + const offsetAngle = this.extra && this.extra.pie && this.extra.pie.offsetAngle || 0; + let startAngle = offsetAngle; + let selectedIndex = -1; + + this.series.forEach((value, index) => { + const percentage = value / total; + const endAngle = startAngle + percentage * 360; + + if (angle >= startAngle && angle < endAngle) { + selectedIndex = index; + } + + startAngle = endAngle; + }); + + // 如果没有选中,不显示 + if (selectedIndex === -1) return; + + // 准备提示信息 + const name = this.labels[selectedIndex]; + const value = this.series[selectedIndex]; + const items = [{ + name: name, + data: this.format(value, name), + color: this.colors[selectedIndex % this.colors.length] + }]; + + // 显示提示 + this.drawToolTip(x, y, '', items, options); + } + + // 绘制工具提示 + drawToolTip(x, y, title, items, options) { + const ctx = this.ctx; + const padding = 10; + const itemHeight = 20; + const titleHeight = title ? 25 : 0; + const width = 120; + const height = titleHeight + items.length * itemHeight + padding * 2; + + // 计算位置(避免超出画布) + let tipX = x - width / 2; + let tipY = y - height - 10; + + if (tipX < 0) tipX = 0; + if (tipX + width > this.width) tipX = this.width - width; + if (tipY < 0) tipY = y + 10; + + // 重绘图表 + this.draw(); + + // 绘制提示框背景 + ctx.setFillStyle('rgba(255, 255, 255, 0.95)'); + ctx.setShadowBlur(5); + ctx.setShadowColor('rgba(0, 0, 0, 0.1)'); + ctx.fillRoundRect(tipX, tipY, width, height, 5); + ctx.setShadowBlur(0); + + // 绘制边框 + ctx.setStrokeStyle('#e5e5e5'); + ctx.setLineWidth(1); + ctx.strokeRoundRect(tipX, tipY, width, height, 5); + + // 绘制标题 + if (title) { + ctx.setFontSize(14); + ctx.setFillStyle('#333333'); + ctx.setTextAlign('center'); + ctx.setTextBaseline('top'); + ctx.fillText(title, tipX + width / 2, tipY + padding); + } + + // 绘制项目 + items.forEach((item, index) => { + const itemY = tipY + padding + titleHeight + index * itemHeight; + + // 绘制颜色点 + ctx.setFillStyle(item.color); + ctx.beginPath(); + ctx.arc(tipX + padding + 5, itemY + 10, 4, 0, 2 * Math.PI); + ctx.fill(); + + // 绘制文本 + ctx.setFontSize(12); + ctx.setFillStyle('#666666'); + ctx.setTextAlign('left'); + ctx.setTextBaseline('middle'); + + const text = options && options.format ? + options.format(item, title) : + `${item.name}: ${item.data}`; + + ctx.fillText(text, tipX + padding + 15, itemY + 10); + }); + + // 绘制连接线 + ctx.beginPath(); + ctx.setStrokeStyle('rgba(255, 255, 255, 0.95)'); + ctx.setLineWidth(2); + ctx.moveTo(x, y); + + if (tipY < y - 10) { + // 提示框在上方 + ctx.lineTo(x, tipY + height); + } else { + // 提示框在下方 + ctx.lineTo(x, tipY); + } + + ctx.stroke(); + + // 绘制完成 + ctx.draw(true); + } +} + +module.exports = WxChart; diff --git a/project.private.config.json b/project.private.config.json index 8d2d774..7699371 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -2,7 +2,7 @@ "libVersion": "3.10.1", "projectname": "accounting-wechat-miniprogram%20(1)", "setting": { - "urlCheck": false, + "urlCheck": true, "coverView": true, "lazyloadPlaceholderEnable": false, "skylineRenderEnable": false,