<template>
|
<div class="data-dashboard">
|
<!-- 顶部统计卡片 -->
|
<div class="stat-cards">
|
<div class="stat-card" v-for="(card, index) in statCards" :key="index">
|
<div class="stat-label">{{ card.label }}</div>
|
<div class="stat-value" :class="{ 'has-prefix': card.prefix }">
|
<span v-if="card.prefix" class="stat-prefix">{{ card.prefix }}</span>
|
{{ card.value }}
|
</div>
|
<div class="stat-sub" v-if="card.label !== '车辆总数'">
|
<span>昨日 {{ card.label === '本月收益' ? '¥' : ''}}{{ card.yesterday }}</span>
|
<span>今日 {{ card.label === '本月收益' ? '¥' : ''}}{{ card.today }}</span>
|
</div>
|
</div>
|
</div>
|
|
<!-- 图表区域 -->
|
<div class="charts-row">
|
<!-- 近30天收益分析 -->
|
<div class="chart-panel chart-bar">
|
<div class="chart-title">近30天收益分析</div>
|
<div ref="barChart" class="chart-container"></div>
|
</div>
|
|
<!-- 车辆实时使用情况 -->
|
<div class="chart-panel chart-donut">
|
<div class="chart-header">
|
<div class="chart-title">车辆实时使用情况</div>
|
<div class="chart-toggle">
|
<el-button-group>
|
<el-button
|
:type="vehicleType === 'bicycle' ? 'primary' : 'default'"
|
size="mini"
|
@click="switchVehicleType('bicycle')"
|
>自行车</el-button>
|
<el-button
|
:type="vehicleType === 'electric' ? 'primary' : 'default'"
|
size="mini"
|
@click="switchVehicleType('electric')"
|
>电动车</el-button>
|
</el-button-group>
|
</div>
|
</div>
|
<div class="chart-subtitle">总计:{{ vehicleTotal }}辆</div>
|
<div ref="donutChart" class="chart-container"></div>
|
</div>
|
|
<!-- 套餐销售来源分析 -->
|
<div class="chart-panel chart-pie">
|
<div class="chart-header">
|
<div class="chart-title">套餐销售来源分析</div>
|
<div class="chart-toggle">
|
<el-button-group>
|
<el-button
|
:type="salesPeriod === 'month' ? 'primary' : 'default'"
|
size="mini"
|
@click="switchSalesPeriod('month')"
|
>本月</el-button>
|
<el-button
|
:type="salesPeriod === 'year' ? 'primary' : 'default'"
|
size="mini"
|
@click="switchSalesPeriod('year')"
|
>本年</el-button>
|
</el-button-group>
|
</div>
|
</div>
|
<div ref="pieChart" class="chart-container"></div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import * as echarts from 'echarts'
|
import { dashboard, packageSourceStat, bikeUsageStat, incomeStat30 } from '@/api/business/report.js'
|
export default {
|
data () {
|
return {
|
statCards: [],
|
vehicleType: 'bicycle',
|
vehicleTotal: 0,
|
bikeUsageData: null,
|
salesPeriod: 'month',
|
packageSourceData: null,
|
barChart: null,
|
donutChart: null,
|
pieChart: null
|
}
|
},
|
mounted () {
|
this.$nextTick(() => {
|
this.initBarChart()
|
this.initDonutChart()
|
this.initPieChart()
|
})
|
this.loadDashboard()
|
window.addEventListener('resize', this.handleResize)
|
},
|
beforeDestroy () {
|
window.removeEventListener('resize', this.handleResize)
|
if (this.barChart) this.barChart.dispose()
|
if (this.donutChart) this.donutChart.dispose()
|
if (this.pieChart) this.pieChart.dispose()
|
},
|
methods: {
|
initBarChart () {
|
const chart = echarts.init(this.$refs.barChart)
|
incomeStat30({}).then(res => {
|
const data = res.dailyList || []
|
const dates = data.map(item => {
|
const d = item.date.split('-')
|
return `${d[1]}-${d[2]}`
|
})
|
const incomes = data.map(item => item.income)
|
chart.setOption({
|
tooltip: { trigger: 'axis' },
|
legend: {
|
data: ['收入'],
|
bottom: 50
|
},
|
grid: {
|
left: 10,
|
right: 10,
|
top: 20,
|
bottom: 80,
|
containLabel: true
|
},
|
dataZoom: [
|
{
|
type: 'inside',
|
xAxisIndex: 0,
|
start: 0,
|
end: 100
|
},
|
{
|
type: 'slider',
|
xAxisIndex: 0,
|
start: 0,
|
end: 100,
|
bottom: 20,
|
height: 20
|
}
|
],
|
xAxis: {
|
type: 'category',
|
data: dates,
|
axisLine: { lineStyle: { color: '#e0e0e0' } },
|
axisTick: { show: false }
|
},
|
yAxis: {
|
type: 'value',
|
axisLine: { show: false },
|
axisTick: { show: false },
|
splitLine: { lineStyle: { color: '#f0f0f0' } }
|
},
|
series: [{
|
name: '收入',
|
type: 'bar',
|
barWidth: '60%',
|
itemStyle: { color: '#5B8FF9' },
|
data: incomes
|
}]
|
})
|
}).catch(e => {
|
this.$tip.apiFailed(e)
|
})
|
this.barChart = chart
|
},
|
initDonutChart () {
|
const chart = echarts.init(this.$refs.donutChart)
|
this.donutChart = chart
|
bikeUsageStat({}).then(res => {
|
this.bikeUsageData = res
|
this.updateDonutChart()
|
}).catch(e => {
|
this.$tip.apiFailed(e)
|
})
|
},
|
updateDonutChart () {
|
if (!this.bikeUsageData || !this.donutChart) return
|
const isBicycle = this.vehicleType === 'bicycle'
|
const idle = isBicycle ? this.bikeUsageData.bikeIdle || 0 : this.bikeUsageData.eleBikeIdle || 0
|
const inUse = isBicycle ? this.bikeUsageData.bikeInUse || 0 : this.bikeUsageData.eleBikeInUse || 0
|
this.vehicleTotal = idle + inUse
|
this.donutChart.setOption({
|
tooltip: { trigger: 'item' },
|
legend: {
|
orient: 'horizontal',
|
bottom: 0,
|
data: ['空闲', '使用中']
|
},
|
series: [{
|
type: 'pie',
|
radius: ['50%', '75%'],
|
center: ['50%', '45%'],
|
avoidLabelOverlap: true,
|
itemStyle: {
|
borderColor: '#fff',
|
borderWidth: 2
|
},
|
label: {
|
show: true,
|
formatter: '{b}\n{c}',
|
fontSize: 12
|
},
|
data: [
|
{ value: idle, name: '空闲', itemStyle: { color: '#73C0DE' } },
|
{ value: inUse, name: '使用中', itemStyle: { color: '#5B8FF9' } }
|
]
|
}]
|
})
|
},
|
switchVehicleType (type) {
|
this.vehicleType = type
|
this.updateDonutChart()
|
},
|
initPieChart () {
|
const chart = echarts.init(this.$refs.pieChart)
|
this.pieChart = chart
|
packageSourceStat({}).then(res => {
|
this.packageSourceData = res
|
this.updatePieChart()
|
}).catch(e => {
|
this.$tip.apiFailed(e)
|
})
|
},
|
updatePieChart () {
|
if (!this.packageSourceData || !this.pieChart) return
|
const data = this.salesPeriod === 'month' ? this.packageSourceData.month : this.packageSourceData.year
|
const douyinCount = data ? (data.douyinCount || 0) : 0
|
const miniCount = data ? (data.miniCount || 0) : 0
|
this.pieChart.setOption({
|
tooltip: { trigger: 'item' },
|
legend: {
|
orient: 'horizontal',
|
top: 0,
|
data: ['抖音团购', '小程序销售']
|
},
|
series: [{
|
type: 'pie',
|
radius: '70%',
|
center: ['50%', '55%'],
|
avoidLabelOverlap: true,
|
itemStyle: {
|
borderColor: '#fff',
|
borderWidth: 2
|
},
|
label: {
|
show: true,
|
formatter: '{b}\n{c}',
|
fontSize: 12
|
},
|
data: [
|
{ value: douyinCount, name: '抖音团购', itemStyle: { color: '#73C0DE' } },
|
{ value: miniCount, name: '小程序销售', itemStyle: { color: '#5B8FF9' } }
|
]
|
}]
|
})
|
},
|
switchSalesPeriod (period) {
|
this.salesPeriod = period
|
this.updatePieChart()
|
},
|
handleResize () {
|
if (this.barChart) this.barChart.resize()
|
if (this.donutChart) this.donutChart.resize()
|
if (this.pieChart) this.pieChart.resize()
|
},
|
loadDashboard () {
|
dashboard({}).then(res => {
|
if (res) {
|
this.statCards = [
|
{ label: '本月收益', prefix: '¥', value: res.monthIncome || 0, yesterday: res.yesterdayIncome || 0, today: res.todayIncome || 0 },
|
{ label: '本月订单', value: res.monthOrderCount || 0, yesterday: res.yesterdayOrderCount || 0, today: res.todayOrderCount || 0 },
|
{ label: '在客户数', value: res.totalMemberCount || 0, yesterday: res.yesterdayNewMember || 0, today: res.todayNewMember || 0 },
|
{ label: '车辆总数', value: res.totalBikeCount || 0, yesterday: '', today: '' }
|
]
|
}
|
}).catch(e => {
|
this.$tip.apiFailed(e)
|
})
|
}
|
}
|
}
|
</script>
|
|
<style lang="scss" scoped>
|
.data-dashboard {
|
padding: 16px;
|
background: #f0f2f5;
|
min-height: 100%;
|
box-sizing: border-box;
|
}
|
|
// 统计卡片
|
.stat-cards {
|
display: flex;
|
gap: 16px;
|
margin-bottom: 16px;
|
}
|
|
.stat-card {
|
flex: 1;
|
background: #eff6ff;
|
border-radius: 8px;
|
padding: 20px 24px;
|
}
|
|
.stat-label {
|
font-size: 14px;
|
color: #666;
|
margin-bottom: 12px;
|
}
|
|
.stat-value {
|
font-size: 28px;
|
font-weight: bold;
|
color: #1a1a1a;
|
margin-bottom: 10px;
|
|
&.has-prefix {
|
display: flex;
|
align-items: baseline;
|
}
|
}
|
|
.stat-prefix {
|
font-size: 20px;
|
margin-right: 2px;
|
}
|
|
.stat-sub {
|
font-size: 12px;
|
color: #999;
|
display: flex;
|
gap: 16px;
|
}
|
|
// 图表行
|
.charts-row {
|
display: flex;
|
gap: 16px;
|
}
|
|
.chart-panel {
|
background: #fff;
|
border-radius: 8px;
|
padding: 20px;
|
box-sizing: border-box;
|
}
|
|
.chart-bar {
|
flex: 1.2;
|
}
|
|
.chart-donut,
|
.chart-pie {
|
flex: 1;
|
}
|
|
.chart-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 4px;
|
}
|
|
.chart-title {
|
font-size: 16px;
|
font-weight: bold;
|
color: #1a1a1a;
|
}
|
|
.chart-toggle {
|
::v-deep .el-button--primary {
|
background: #216EEE;
|
border-color: #216EEE;
|
}
|
}
|
|
.chart-subtitle {
|
font-size: 12px;
|
color: #999;
|
margin-bottom: 10px;
|
}
|
|
.chart-container {
|
width: 100%;
|
height: 320px;
|
}
|
</style>
|