You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
623 lines
16 KiB
623 lines
16 KiB
<template> |
|
<view class="chart-container"> |
|
<text class="title">价格走势图</text> |
|
<!-- 悬浮筛选按钮 --> |
|
<view class="floating-filter-btn" @click="showFilter = true"> |
|
<text>筛选</text> |
|
</view> |
|
|
|
<!-- 筛选面板 --> |
|
<view class="filter-panel" v-if="showFilter"> |
|
<view class="filter-header"> |
|
<text class="filter-title">选择关注的区服和阵营</text> |
|
<view class="close-btn" @click="showFilter = false">关闭</view> |
|
</view> |
|
<view class="filter-content"> |
|
<checkbox-group @change="handleCheckboxChange"> |
|
<view class="server-group" v-for="server in allServers" :key="server.id"> |
|
<view class="server-title">{{ server.name }}</view> |
|
<view class="faction-items"> |
|
<view class="filter-item" v-for="faction in server.factions" :key="faction.id"> |
|
<checkbox :value="faction.id" :checked="selectedFactions.includes(faction.id)"> |
|
{{ faction.name }} |
|
</checkbox> |
|
</view> |
|
</view> |
|
</view> |
|
</checkbox-group> |
|
</view> |
|
<view class="filter-footer"> |
|
<view class="cancel-btn" @click="showFilter = false">取消</view> |
|
<view class="save-btn" @click="saveFilter">保存</view> |
|
</view> |
|
</view> |
|
|
|
<view class="chart-header"> |
|
<!-- 时间筛选 --> |
|
<view class="time-filter"> |
|
<view class="filter-item" :class="{ active: timeFilter === 'day' }" @click="timeFilter = 'day'"> |
|
<text>天</text> |
|
</view> |
|
<view class="filter-item" :class="{ active: timeFilter === 'hour' }" @click="timeFilter = 'hour'"> |
|
<text>时</text> |
|
</view> |
|
<view class="filter-item" :class="{ active: timeFilter === 'week' }" @click="timeFilter = 'week'"> |
|
<text>周</text> |
|
</view> |
|
</view> |
|
</view> |
|
<view class="chart-content"> |
|
<!-- 价格走势图区域 --> |
|
<!-- 铁血1区 --> |
|
<view v-if="isServerSelected('tx1')"> |
|
<view class="chart-item" v-if="isFactionSelected('tx1-alliance')"> |
|
<text class="chart-title">铁血1区 - 联盟</text> |
|
<canvas canvas-id="tx1AllianceChart" class="chart-canvas"></canvas> |
|
</view> |
|
|
|
<view class="chart-item" v-if="isFactionSelected('tx1-horde')"> |
|
<text class="chart-title">铁血1区 - 部落</text> |
|
<canvas canvas-id="tx1HordeChart" class="chart-canvas"></canvas> |
|
</view> |
|
</view> |
|
|
|
<!-- 铁血2区 --> |
|
<view v-if="isServerSelected('tx2')"> |
|
<view class="chart-item" v-if="isFactionSelected('tx2-alliance')"> |
|
<text class="chart-title">铁血2区 - 联盟</text> |
|
<canvas canvas-id="tx2AllianceChart" class="chart-canvas"></canvas> |
|
</view> |
|
|
|
<view class="chart-item" v-if="isFactionSelected('tx2-horde')"> |
|
<text class="chart-title">铁血2区 - 部落</text> |
|
<canvas canvas-id="tx2HordeChart" class="chart-canvas"></canvas> |
|
</view> |
|
</view> |
|
|
|
<!-- 时光1区 --> |
|
<view v-if="isServerSelected('sg1')"> |
|
<view class="chart-item" v-if="isFactionSelected('sg1-alliance')"> |
|
<text class="chart-title">时光1区 - 联盟</text> |
|
<canvas canvas-id="sg1AllianceChart" class="chart-canvas"></canvas> |
|
</view> |
|
|
|
<view class="chart-item" v-if="isFactionSelected('sg1-horde')"> |
|
<text class="chart-title">时光1区 - 部落</text> |
|
<canvas canvas-id="sg1HordeChart" class="chart-canvas"></canvas> |
|
</view> |
|
</view> |
|
</view> |
|
</view> |
|
</template> |
|
|
|
<script> |
|
export default { |
|
data() { |
|
return { |
|
timeFilter: 'day', // day, hour, week |
|
showFilter: false, |
|
// 修改数据结构,支持服务器和阵营的两级筛选 |
|
allServers: [ |
|
{ |
|
id: 'tx1', |
|
name: '铁血1区', |
|
factions: [ |
|
{ id: 'tx1-alliance', name: '联盟', serverId: 'tx1' }, |
|
{ id: 'tx1-horde', name: '部落', serverId: 'tx1' } |
|
] |
|
}, |
|
{ |
|
id: 'tx2', |
|
name: '铁血2区', |
|
factions: [ |
|
{ id: 'tx2-alliance', name: '联盟', serverId: 'tx2' }, |
|
{ id: 'tx2-horde', name: '部落', serverId: 'tx2' } |
|
] |
|
}, |
|
{ |
|
id: 'sg1', |
|
name: '时光1区', |
|
factions: [ |
|
{ id: 'sg1-alliance', name: '联盟', serverId: 'sg1' }, |
|
{ id: 'sg1-horde', name: '部落', serverId: 'sg1' } |
|
] |
|
} |
|
], |
|
selectedFactions: [], |
|
// 模拟价格历史数据(元/1000金) |
|
chartData: { |
|
tx1Alliance: [], |
|
tx1Horde: [], |
|
tx2Alliance: [], |
|
tx2Horde: [], |
|
sg1Alliance: [], |
|
sg1Horde: [] |
|
} |
|
}; |
|
}, |
|
onLoad() { |
|
// 页面加载时从缓存中读取用户选择的阵营 |
|
this.loadSelectedFactions(); |
|
// 加载走势图数据 |
|
this.generateMockData(); |
|
// 初始绘制图表 |
|
// 注意:需要等待DOM渲染完成后才能获取canvas尺寸 |
|
}, |
|
onReady() { |
|
// 确保DOM渲染完成后绘制图表 |
|
this.$nextTick(() => { |
|
this.drawCharts(); |
|
}); |
|
}, |
|
watch: { |
|
// 监听时间筛选变化,重新绘制图表 |
|
timeFilter: function() { |
|
this.generateMockData(); |
|
this.$nextTick(() => { |
|
this.drawCharts(); |
|
}); |
|
} |
|
}, |
|
methods: { |
|
// 加载用户选择的阵营 |
|
loadSelectedFactions() { |
|
const selected = uni.getStorageSync('selectedFactions'); |
|
if (selected && selected.length > 0) { |
|
this.selectedFactions = selected; |
|
} else { |
|
// 默认全部选中 |
|
this.selectedFactions = []; |
|
this.allServers.forEach(server => { |
|
server.factions.forEach(faction => { |
|
this.selectedFactions.push(faction.id); |
|
}); |
|
}); |
|
// 保存到缓存 |
|
uni.setStorageSync('selectedFactions', this.selectedFactions); |
|
} |
|
}, |
|
|
|
// 处理checkbox变化 |
|
handleCheckboxChange(e) { |
|
this.selectedFactions = e.detail.value; |
|
}, |
|
|
|
// 保存筛选结果 |
|
saveFilter() { |
|
// 确保至少选择一个阵营 |
|
if (this.selectedFactions.length === 0) { |
|
uni.showToast({ |
|
title: '请至少选择一个阵营', |
|
icon: 'none' |
|
}); |
|
return; |
|
} |
|
|
|
// 保存到缓存 |
|
uni.setStorageSync('selectedFactions', this.selectedFactions); |
|
|
|
uni.showToast({ |
|
title: '保存成功', |
|
icon: 'success' |
|
}); |
|
this.showFilter = false; |
|
// 重新绘制图表 |
|
this.drawCharts(); |
|
}, |
|
|
|
// 检查阵营是否被选中 |
|
isFactionSelected(factionId) { |
|
return this.selectedFactions.includes(factionId); |
|
}, |
|
|
|
// 检查服务器是否有任何阵营被选中 |
|
isServerSelected(serverId) { |
|
return this.allServers.some(server => |
|
server.id === serverId && |
|
server.factions.some(faction => |
|
this.selectedFactions.includes(faction.id) |
|
) |
|
); |
|
}, |
|
|
|
// 生成模拟数据 |
|
generateMockData() { |
|
const basePrice = 10; |
|
const fluctuation = 2; |
|
let dataCount = 24; // 默认按小时 |
|
let timeStep = 1; // 默认按小时 |
|
|
|
if (this.timeFilter === 'day') { |
|
dataCount = 30; // 30天 |
|
timeStep = 24; // 按天 |
|
} else if (this.timeFilter === 'hour') { |
|
dataCount = 24; // 24小时 |
|
timeStep = 1; // 按小时 |
|
} else if (this.timeFilter === 'week') { |
|
dataCount = 12; // 12周 |
|
timeStep = 168; // 按周 |
|
} |
|
|
|
// 生成所有区服的模拟数据 |
|
this.chartData = { |
|
tx1Alliance: this.generatePriceData(basePrice + 1, fluctuation, dataCount, timeStep), |
|
tx1Horde: this.generatePriceData(basePrice + 0.5, fluctuation, dataCount, timeStep), |
|
tx2Alliance: this.generatePriceData(basePrice + 2, fluctuation, dataCount, timeStep), |
|
tx2Horde: this.generatePriceData(basePrice + 1.5, fluctuation, dataCount, timeStep), |
|
sg1Alliance: this.generatePriceData(basePrice + 3, fluctuation, dataCount, timeStep), |
|
sg1Horde: this.generatePriceData(basePrice + 2.5, fluctuation, dataCount, timeStep) |
|
}; |
|
|
|
console.log('Generated mock data:', this.chartData); |
|
}, |
|
|
|
// 生成单个区服的价格数据 |
|
generatePriceData(base, fluctuation, count, timeStep) { |
|
const data = []; |
|
const now = new Date(); |
|
|
|
for (let i = count - 1; i >= 0; i--) { |
|
const time = new Date(now.getTime() - i * timeStep * 60 * 60 * 1000); |
|
// 生成有波动的价格 |
|
const price = base + (Math.random() - 0.5) * fluctuation; |
|
data.push({ |
|
time: time, |
|
price: parseFloat(price.toFixed(2)) |
|
}); |
|
} |
|
|
|
return data; |
|
}, |
|
|
|
// 绘制所有图表 |
|
drawCharts() { |
|
// 只绘制用户选择的阵营的图表 |
|
if (this.isFactionSelected('tx1-alliance')) { |
|
this.drawChart('tx1AllianceChart', this.chartData.tx1Alliance); |
|
} |
|
if (this.isFactionSelected('tx1-horde')) { |
|
this.drawChart('tx1HordeChart', this.chartData.tx1Horde); |
|
} |
|
if (this.isFactionSelected('tx2-alliance')) { |
|
this.drawChart('tx2AllianceChart', this.chartData.tx2Alliance); |
|
} |
|
if (this.isFactionSelected('tx2-horde')) { |
|
this.drawChart('tx2HordeChart', this.chartData.tx2Horde); |
|
} |
|
if (this.isFactionSelected('sg1-alliance')) { |
|
this.drawChart('sg1AllianceChart', this.chartData.sg1Alliance); |
|
} |
|
if (this.isFactionSelected('sg1-horde')) { |
|
this.drawChart('sg1HordeChart', this.chartData.sg1Horde); |
|
} |
|
}, |
|
|
|
// 绘制单个图表 |
|
drawChart(canvasId, data) { |
|
if (!data || data.length === 0) return; |
|
|
|
const ctx = uni.createCanvasContext(canvasId, this); |
|
|
|
// 使用像素单位而不是rpx |
|
const canvasWidth = 350; // 画布宽度(像素) |
|
const canvasHeight = 150; // 画布高度(像素) |
|
const padding = 25; // 边距(像素) |
|
|
|
// 计算图表区域 |
|
const chartWidth = canvasWidth - 2 * padding; |
|
const chartHeight = canvasHeight - 2 * padding; |
|
|
|
// 计算价格范围 |
|
const prices = data.map(item => item.price); |
|
const minPrice = Math.min(...prices) - 0.5; |
|
const maxPrice = Math.max(...prices) + 0.5; |
|
const priceRange = maxPrice - minPrice; |
|
|
|
// 计算时间范围 |
|
const times = data.map(item => item.time); |
|
const minTime = times[0]; |
|
const maxTime = times[times.length - 1]; |
|
const timeRange = maxTime - minTime; |
|
|
|
// 绘制坐标轴 |
|
this.drawAxes(ctx, padding, chartWidth, chartHeight, minPrice, maxPrice, data); |
|
|
|
// 绘制价格曲线 |
|
this.drawPriceLine(ctx, padding, chartWidth, chartHeight, data, minPrice, maxPrice, minTime, maxTime); |
|
|
|
// 绘制价格点 |
|
this.drawPricePoints(ctx, padding, chartWidth, chartHeight, data, minPrice, maxPrice, minTime, maxTime); |
|
|
|
// 绘制完成 |
|
ctx.draw(); |
|
}, |
|
|
|
// 绘制坐标轴 |
|
drawAxes(ctx, padding, chartWidth, chartHeight, minPrice, maxPrice, data) { |
|
ctx.setStrokeStyle('#ccc'); |
|
ctx.setLineWidth(1); |
|
|
|
// X轴 |
|
ctx.beginPath(); |
|
ctx.moveTo(padding, padding + chartHeight); |
|
ctx.lineTo(padding + chartWidth, padding + chartHeight); |
|
ctx.stroke(); |
|
|
|
// Y轴 |
|
ctx.beginPath(); |
|
ctx.moveTo(padding, padding); |
|
ctx.lineTo(padding, padding + chartHeight); |
|
ctx.stroke(); |
|
|
|
// 绘制Y轴刻度和标签 |
|
const yTicks = 5; |
|
for (let i = 0; i <= yTicks; i++) { |
|
const y = padding + (chartHeight / yTicks) * i; |
|
const price = maxPrice - (maxPrice - minPrice) * (i / yTicks); |
|
|
|
// 绘制刻度线 |
|
ctx.beginPath(); |
|
ctx.moveTo(padding - 3, y); |
|
ctx.lineTo(padding, y); |
|
ctx.stroke(); |
|
|
|
// 绘制价格标签 |
|
ctx.setFontSize(10); |
|
ctx.setFillStyle('#666'); |
|
ctx.setTextAlign('right'); |
|
ctx.fillText(price.toFixed(1), padding - 5, y + 3); |
|
} |
|
|
|
// 绘制X轴刻度和标签 |
|
const xTicks = this.timeFilter === 'day' ? 6 : 5; |
|
for (let i = 0; i <= xTicks; i++) { |
|
const x = padding + (chartWidth / xTicks) * i; |
|
const index = Math.round((data.length - 1) * (i / xTicks)); |
|
const time = data[index].time; |
|
|
|
// 绘制刻度线 |
|
ctx.beginPath(); |
|
ctx.moveTo(x, padding + chartHeight); |
|
ctx.lineTo(x, padding + chartHeight + 3); |
|
ctx.stroke(); |
|
|
|
// 格式化时间标签 |
|
let timeLabel = ''; |
|
if (this.timeFilter === 'day') { |
|
timeLabel = time.getDate() + '日'; |
|
} else if (this.timeFilter === 'hour') { |
|
timeLabel = time.getHours() + '时'; |
|
} else if (this.timeFilter === 'week') { |
|
timeLabel = '第' + Math.ceil((time.getDate() + time.getDay()) / 7) + '周'; |
|
} |
|
|
|
// 绘制时间标签 |
|
ctx.setFontSize(10); |
|
ctx.setFillStyle('#666'); |
|
ctx.setTextAlign('center'); |
|
ctx.fillText(timeLabel, x, padding + chartHeight + 15); |
|
} |
|
|
|
// 绘制单位标签 |
|
ctx.setFontSize(12); |
|
ctx.setFillStyle('#333'); |
|
ctx.setTextAlign('center'); |
|
ctx.fillText('时间', padding + chartWidth / 2, padding + chartHeight + 30); |
|
ctx.setTextAlign('right'); |
|
ctx.fillText('价格 (元/1000金)', padding - 20, padding + chartHeight / 2); |
|
}, |
|
|
|
// 绘制价格曲线 |
|
drawPriceLine(ctx, padding, chartWidth, chartHeight, data, minPrice, maxPrice, minTime, maxTime) { |
|
ctx.setStrokeStyle('#1989fa'); |
|
ctx.setLineWidth(2); |
|
|
|
ctx.beginPath(); |
|
data.forEach((item, index) => { |
|
// 计算坐标 |
|
const x = padding + (chartWidth * (item.time - minTime)) / (maxTime - minTime); |
|
const y = padding + chartHeight - (chartHeight * (item.price - minPrice)) / (maxPrice - minPrice); |
|
|
|
if (index === 0) { |
|
ctx.moveTo(x, y); |
|
} else { |
|
ctx.lineTo(x, y); |
|
} |
|
}); |
|
ctx.stroke(); |
|
}, |
|
|
|
// 绘制价格点 |
|
drawPricePoints(ctx, padding, chartWidth, chartHeight, data, minPrice, maxPrice, minTime, maxTime) { |
|
ctx.setFillStyle('#1989fa'); |
|
|
|
data.forEach(item => { |
|
// 计算坐标 |
|
const x = padding + (chartWidth * (item.time - minTime)) / (maxTime - minTime); |
|
const y = padding + chartHeight - (chartHeight * (item.price - minPrice)) / (maxPrice - minPrice); |
|
|
|
// 绘制圆点 |
|
ctx.beginPath(); |
|
ctx.arc(x, y, 3, 0, 2 * Math.PI); |
|
ctx.fill(); |
|
}); |
|
} |
|
} |
|
}; |
|
</script> |
|
|
|
<style scoped> |
|
.chart-container { |
|
min-height: 100vh; |
|
background-color: #f5f5f5; |
|
padding: 20rpx; |
|
} |
|
|
|
.chart-header { |
|
text-align: center; |
|
padding: 20rpx 0; |
|
} |
|
|
|
.title { |
|
font-size: 36rpx; |
|
font-weight: bold; |
|
color: #333; |
|
text-align: center; |
|
margin-bottom: 20rpx; |
|
display: block; |
|
} |
|
|
|
/* 悬浮筛选按钮样式 */ |
|
.floating-filter-btn { |
|
position: fixed; |
|
right: 30rpx; |
|
top: 30rpx; |
|
width: 80rpx; |
|
height: 80rpx; |
|
background-color: #1989fa; |
|
color: #fff; |
|
border-radius: 50%; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
font-size: 24rpx; |
|
box-shadow: 0 4rpx 10rpx rgba(25, 137, 250, 0.3); |
|
z-index: 998; |
|
} |
|
|
|
/* 筛选面板样式 */ |
|
.filter-panel { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background-color: rgba(0, 0, 0, 0.5); |
|
z-index: 999; |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
.filter-header { |
|
background-color: #fff; |
|
padding: 20rpx; |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
border-bottom: 1rpx solid #eee; |
|
} |
|
|
|
.filter-title { |
|
font-size: 32rpx; |
|
font-weight: bold; |
|
} |
|
|
|
.close-btn { |
|
background-color: #f0f0f0; |
|
color: #333; |
|
font-size: 24rpx; |
|
padding: 10rpx 20rpx; |
|
border-radius: 20rpx; |
|
} |
|
|
|
.filter-content { |
|
background-color: #fff; |
|
flex: 1; |
|
padding: 20rpx; |
|
overflow-y: auto; |
|
} |
|
|
|
.server-group { |
|
margin-bottom: 30rpx; |
|
} |
|
|
|
.server-title { |
|
font-weight: bold; |
|
font-size: 30rpx; |
|
margin-bottom: 15rpx; |
|
padding-left: 20rpx; |
|
} |
|
|
|
.faction-items { |
|
padding-left: 40rpx; |
|
} |
|
|
|
.filter-item { |
|
margin-bottom: 15rpx; |
|
font-size: 28rpx; |
|
} |
|
|
|
.filter-footer { |
|
background-color: #fff; |
|
padding: 20rpx; |
|
display: flex; |
|
justify-content: space-around; |
|
border-top: 1rpx solid #eee; |
|
} |
|
|
|
.cancel-btn { |
|
background-color: #f0f0f0; |
|
color: #333; |
|
font-size: 24rpx; |
|
padding: 10rpx 40rpx; |
|
border-radius: 20rpx; |
|
} |
|
|
|
.save-btn { |
|
background-color: #1989fa; |
|
color: #fff; |
|
font-size: 24rpx; |
|
padding: 10rpx 40rpx; |
|
border-radius: 20rpx; |
|
} |
|
|
|
/* 时间筛选样式 */ |
|
.time-filter { |
|
display: flex; |
|
justify-content: center; |
|
margin: 20rpx 0; |
|
gap: 20rpx; |
|
} |
|
|
|
.filter-item { |
|
padding: 10rpx 30rpx; |
|
border-radius: 20rpx; |
|
background-color: #f5f5f5; |
|
font-size: 28rpx; |
|
color: #666; |
|
} |
|
|
|
.filter-item.active { |
|
background-color: #1989fa; |
|
color: #fff; |
|
} |
|
|
|
.chart-content { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 30rpx; |
|
} |
|
|
|
.chart-item { |
|
background-color: #fff; |
|
border-radius: 10rpx; |
|
padding: 20rpx; |
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
.chart-title { |
|
font-size: 32rpx; |
|
font-weight: bold; |
|
color: #333; |
|
margin-bottom: 20rpx; |
|
display: block; |
|
} |
|
|
|
/* 图表画布样式 */ |
|
.chart-canvas { |
|
width: 700rpx; |
|
height: 300rpx; |
|
background-color: #fafafa; |
|
border-radius: 8rpx; |
|
} |
|
</style>
|
|
|