17 changed files with 1738 additions and 82 deletions
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.4 KiB |
@ -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}`; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}); |
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"navigationBarTitleText": "收支统计", |
||||
|
"navigationBarBackgroundColor": "#ffffff", |
||||
|
"navigationBarTextStyle": "black", |
||||
|
"enablePullDownRefresh": false |
||||
|
} |
@ -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> |
@ -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; |
||||
|
} |
@ -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; |
Loading…
Reference in new issue