Compare commits

...

2 Commits

Author SHA1 Message Date
wangsai c747a31b4e 11 1 week ago
wangsai 7d291b4ea7 11 1 week ago
  1. 1
      app.json
  2. BIN
      images/left-arrow.png
  3. BIN
      images/right-arrow.png
  4. 4
      pages/add/add.js
  5. 2
      pages/add/add.wxss
  6. 20
      pages/index/index.js
  7. 4
      pages/index/index.wxml
  8. 10
      pages/login/login.js
  9. 28
      pages/profile/profile.js
  10. 20
      pages/profile/profile.wxml
  11. 188
      pages/statistics/statistics.js
  12. 50
      pages/statistics/statistics.wxml
  13. 61
      pages/statistics/statistics.wxss
  14. 380
      pages/statistics2/statistics.js
  15. 6
      pages/statistics2/statistics.json
  16. 161
      pages/statistics2/statistics.wxml
  17. 343
      pages/statistics2/statistics.wxss
  18. 6
      pages/utils/request.js
  19. 560
      pages/utils/wx-chart.js
  20. 2
      project.private.config.json

1
app.json

@ -4,6 +4,7 @@
"pages/index/index",
"pages/add/add",
"pages/statistics/statistics",
"pages/statistics2/statistics",
"pages/category/category",
"pages/profile/profile"
],

BIN
images/left-arrow.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
images/right-arrow.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

4
pages/add/add.js

@ -89,7 +89,7 @@ confirmSelection() {
}
},
async getClassification() {
const res=await request.get('/admin-api/book/classification/oneTwoLevelList',{})
const res=await request.get('/app-api/book/classification/oneTwoLevelList',{})
console.log('resss:',res)
if(res.code==0){
this.setData(
@ -182,7 +182,7 @@ onShow(){
useDate: this.data.useDate
};
// 自动携带token
const res = await request.post('/admin-api/book/inout/create',newRecord)
const res = await request.post('/app-api/book/inout/create',newRecord)
console.log("新增结果:",res)
if(res.code==0){
// 显示成功提示

2
pages/add/add.wxss

@ -195,7 +195,7 @@
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
z-index: 999;
max-height: 80vh;
max-height: 50vh;
display: flex;
flex-direction: column;
}

20
pages/index/index.js

@ -46,10 +46,20 @@ Page({
try {
// 自动携带token
const res = await request.get('/admin-api/book/inout/myList?pageNo=1&pageSize=10')
const res = await request.get('/app-api/book/inout/allList')
console.log('获取数据成功:', res)
if(res.code==0){
records=res.data.list
records=res.data
}
const res1 = await request.get('/app-api/book/inout/myList-tol',{type:"in"})
console.log('获取数据成功:', res1)
if(res1.code==0){
totalIncome=res1.data
}
const res2 = await request.get('/app-api/book/inout/myList-tol',{type:"out"})
console.log('获取数据成功:', res2)
if(res2.code==0){
totalExpense=res2.data
}
// 处理数据...
} catch (error) {
@ -57,13 +67,11 @@ Page({
}
console.log('records:',records)
this.setData({
totalBalance: totalBalance.toFixed(2),
// 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)
recentRecords: records
});
}
})

4
pages/index/index.wxml

@ -25,7 +25,7 @@
<view class="records-section">
<view class="section-header">
<text class="section-title">近期记录</text>
<navigator url="/pages/statistics/statistics" class="see-all">查看全部</navigator>
<!-- <navigator url="/pages/statistics/statistics" class="see-all">查看全部</navigator> -->
</view>
<view class="records-list">
@ -34,7 +34,7 @@
<text class="iconfont icon-{{item.categoryIcon}}"></text>
</view>
<view class="record-details">
<view class="record-title">{{item.categoryName}}</view>
<view class="record-title">{{item.classificationName}}</view>
<view class="record-date">{{item.useDate}}</view>
</view>
<view class="record-amount {{item.type === 'in' ? 'income' : 'expense'}}">

10
pages/login/login.js

@ -95,13 +95,13 @@ Page({
});
wx.request({
url: 'http://localhost:48080/admin-api/system/auth/login', // 仅为示例,并非真实的接口地址
url: 'http://192.168.250.38:48080/app-api/member/auth/login', // 仅为示例,并非真实的接口地址
method: 'POST', // 请求方法,可以是 GET, POST, PUT, DELETE 等
data: {
"tenantName": "芋道源码",
"username": account,
"password": password,
"rememberMe": true
mobile: account,
// 账号
password: password
// 密码
},
header: {
'Tenant-Id':1,

28
pages/profile/profile.js

@ -1,3 +1,4 @@
const request = require('../utils/request')
Page({
data: {
totalRecords: 0,
@ -134,12 +135,29 @@ Page({
});
},
outLogin() {
async outLogin() {
wx.showModal({
title: '关于简易记账',
content: '简易记账是一款简单实用的记账工具,帮助你记录和管理个人财务。',
showCancel: false,
confirmText: '我知道了'
title: '确认退出',
content: '是否确认退出?',
cancelText: '取消',
confirmText: '确认',
success: async (res) => {
if (res.confirm) {
const ress=await request.post('/app-api/member/auth/logout')
console.log('resss:',ress)
if(ress.code==0){
wx.showToast({
title: '退出登录成功',
icon: 'success',
duration: 2000
});
wx.redirectTo({
url: `/pages/login/login`
})
}
}
}
});
}

20
pages/profile/profile.wxml

@ -12,37 +12,37 @@
<!-- 功能列表 -->
<view class="function-list">
<navigator url="/pages/category/category" class="function-item">
<!-- <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>
</navigator> -->
<view class="function-item" bindtap="exportData">
<!-- <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> -->
<view class="function-item" bindtap="clearData">
<!-- <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> -->
<view class="function-item" bindtap="showAbout">
<!-- <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> -->
<view class="function-item" bindtap="outLogin">
<view class="function-icon">
@ -55,7 +55,7 @@
</view>
<!-- 数据统计 -->
<view class="stats-card">
<!-- <view class="stats-card">
<text class="stats-title">记账统计</text>
<view class="stats-grid">
<view class="stats-item">
@ -71,5 +71,5 @@
<text class="stats-label">连续记账(天)</text>
</view>
</view>
</view>
</view> -->
</view>

188
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();
},

50
pages/statistics/statistics.wxml

@ -1,6 +1,24 @@
<!-- <view class="container"> -->
<!-- 时间维度切换 -->
<view class="time-filter">
<view class="time-btns">
<button
class="time-btn {{currentTimeType === 'year' ? 'time-btn-active' : ''}}"
bindtap="changeTimeType"
data-type="year"
>
按年
</button>
<button
class="time-btn {{currentTimeType === 'month' ? 'time-btn-active' : ''}}"
bindtap="changeTimeType"
data-type="month"
>
按月
</button>
</view>
</view>
<!-- 月份选择器 -->
<view class="month-picker">
<!-- <view class="month-picker">
<picker mode="date" fields="month" value="{{selectedMonth}}" bindchange="onMonthChange">
<view class="picker-view">
<text class="iconfont icon-calendar"></text>
@ -8,6 +26,18 @@
<text class="iconfont icon-arrow-down"></text>
</view>
</picker>
</view> -->
<!-- 当前时间选择 -->
<view class="current-period">
<view class="period-container">
<button class="period-btn" bindtap="prevPeriod">
<image src="/images/left-arrow.png" class="arrow-icon"></image>
</button>
<view class="period-text">{{currentPeriodText}}</view>
<button class="period-btn" bindtap="nextPeriod">
<image src="/images/right-arrow.png" class="arrow-icon"></image>
</button>
</view>
</view>
<!-- 收支概览 -->
@ -20,10 +50,6 @@
<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>
<!-- 分类统计 -->
@ -68,16 +94,16 @@
<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'}}">
<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-note">{{item.note || '无备注'}}</view>
<view class="record-date">{{formatDate(item.date)}}</view>
<view class="record-title">{{item.classificationName}}</view>
<view class="record-note">{{item.remark || '无备注'}}</view>
<view class="record-date">{{item.useDate}}</view>
</view>
<view class="record-amount {{item.type === 'income' ? 'income' : 'expense'}}">
{{item.type === 'income' ? '+' : '-'}}¥ {{item.amount}}
<view class="record-amount {{item.type === 'in' ? 'income' : 'expense'}}">
{{item.type === 'in' ? '+' : '-'}}¥ {{item.money}}
</view>
</view>

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

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

6
pages/statistics2/statistics.json

@ -0,0 +1,6 @@
{
"navigationBarTitleText": "收支统计",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": false
}

161
pages/statistics2/statistics.wxml

@ -0,0 +1,161 @@
<view class="container">
<!-- 时间维度切换 -->
<view class="time-filter">
<view class="time-btns">
<button
class="time-btn {{currentTimeType === 'year' ? 'time-btn-active' : ''}}"
bindtap="changeTimeType"
data-type="year"
>
按年
</button>
<button
class="time-btn {{currentTimeType === 'month' ? 'time-btn-active' : ''}}"
bindtap="changeTimeType"
data-type="month"
>
按月
</button>
<button
class="time-btn {{currentTimeType === 'quarter' ? 'time-btn-active' : ''}}"
bindtap="changeTimeType"
data-type="quarter"
>
按季
</button>
</view>
</view>
<!-- 当前时间选择 -->
<view class="current-period">
<view class="period-container">
<button class="period-btn" bindtap="prevPeriod">
<image src="/images/left-arrow.png" class="arrow-icon"></image>
</button>
<view class="period-text">{{currentPeriodText}}</view>
<button class="period-btn" bindtap="nextPeriod">
<image src="/images/right-arrow.png" class="arrow-icon"></image>
</button>
</view>
</view>
<!-- 总额统计卡片 -->
<view class="total-cards">
<view class="income-card card">
<view class="card-header">
<text class="card-label">收入总额</text>
<view class="card-icon income-icon">
<image src="/images/income-icon.png" class="icon-img"></image>
</view>
</view>
<view class="card-amount">{{incomeTotal}}</view>
<view class="card-change positive-change">
<text class="change-icon">↑</text>
<text class="change-value">12.5%</text>
<text class="change-desc">较上期</text>
</view>
</view>
<view class="expense-card card">
<view class="card-header">
<text class="card-label">支出总额</text>
<view class="card-icon expense-icon">
<image src="/images/expense-icon.png" class="icon-img"></image>
</view>
</view>
<view class="card-amount">{{expenseTotal}}</view>
<view class="card-change negative-change">
<text class="change-icon">↑</text>
<text class="change-value">8.2%</text>
<text class="change-desc">较上期</text>
</view>
</view>
</view>
<!-- 折线图区域 -->
<view class="chart-container trend-chart">
<view class="chart-header">
<text class="chart-title">收支趋势</text>
<view class="chart-legend">
<view class="legend-item">
<view class="legend-color income-color"></view>
<text class="legend-text">收入</text>
</view>
<view class="legend-item">
<view class="legend-color expense-color"></view>
<text class="legend-text">支出</text>
</view>
</view>
</view>
<canvas
canvas-id="trendChart"
class="chart-canvas"
bindtouchstart="touchTrendChart"
></canvas>
</view>
<!-- 饼图区域 -->
<view class="chart-container pie-chart">
<view class="chart-header">
<text class="chart-title">收支分类</text>
<view class="pie-type-switch">
<button
class="type-btn {{currentPieType === 'income' ? 'type-btn-active' : ''}}"
bindtap="changePieType"
data-type="income"
>
收入分类
</button>
<button
class="type-btn {{currentPieType === 'expense' ? 'type-btn-active' : ''}}"
bindtap="changePieType"
data-type="expense"
>
支出分类
</button>
</view>
</view>
<view class="pie-content">
<canvas
canvas-id="categoryChart"
class="pie-canvas"
bindtouchstart="touchPieChart"
></canvas>
<view class="pie-legend">
<view class="legend-list">
<view class="legend-item" wx:for="{{currentPieLegend}}" wx:key="index">
<view class="legend-color" style="background-color: {{item.color}};"></view>
<text class="legend-text">{{item.name}}</text>
<text class="legend-value">{{item.value}}</text>
<text class="legend-percent">{{item.percent}}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 底部导航 -->
<view class="tab-bar">
<navigator url="/pages/index/index" class="tab-bar-item">
<image class="tab-bar-icon" src="/images/home{{currentTab === 'home' ? '-active' : ''}}.png"></image>
<text class="tab-bar-text {{currentTab === 'home' ? 'tab-bar-text-active' : ''}}">首页</text>
</navigator>
<navigator url="/pages/statistics/statistics" class="tab-bar-item">
<image class="tab-bar-icon" src="/images/chart{{currentTab === 'statistics' ? '-active' : ''}}.png"></image>
<text class="tab-bar-text {{currentTab === 'statistics' ? 'tab-bar-text-active' : ''}}">统计</text>
</navigator>
<navigator url="/pages/add/add" class="tab-bar-item add-btn">
<image class="tab-bar-icon" src="/images/add.png"></image>
</navigator>
<navigator url="/pages/detail/detail" class="tab-bar-item">
<image class="tab-bar-icon" src="/images/list{{currentTab === 'detail' ? '-active' : ''}}.png"></image>
<text class="tab-bar-text {{currentTab === 'detail' ? 'tab-bar-text-active' : ''}}">明细</text>
</navigator>
<navigator url="/pages/mine/mine" class="tab-bar-item">
<image class="tab-bar-icon" src="/images/mine{{currentTab === 'mine' ? '-active' : ''}}.png"></image>
<text class="tab-bar-text {{currentTab === 'mine' ? 'tab-bar-text-active' : ''}}">我的</text>
</navigator>
</view>

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

6
pages/utils/request.js

@ -1,7 +1,7 @@
/**
* 封装微信小程序请求自动处理token
*/
const baseUrl='http://localhost:48080'
const baseUrl='http://192.168.250.38:48080'
const request = (url, options = {}) => {
// 返回Promise,方便使用async/await
@ -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,

560
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;

2
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,

Loading…
Cancel
Save