统计数据

This commit is contained in:
磷叶 2025-04-06 18:20:25 +08:00
parent 58d2e40f23
commit c53155e371
10 changed files with 439 additions and 198 deletions

View File

@ -0,0 +1,14 @@
import request from '@/utils/request'
/**
* 获取统计数据
* @param {Object} query 查询参数
* @returns {Promise} 返回统计数据
*/
export function getOrderDailyAmount(query) {
return request({
url: '/dashboard/order/dailyAmount',
method: 'get',
params: query
})
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="12" height="12" viewBox="0 0 12 12"><defs><clipPath id="master_svg0_0_7134"><rect x="0" y="12" width="12" height="12" rx="0"/></clipPath></defs><g transform="matrix(1,0,0,-1,0,24)" clip-path="url(#master_svg0_0_7134)"><g><path d="M6.016670023841858,12.2998046875C6.016670023841858,12.2998046875,10.683330023841858,18.3227646875,10.683330023841858,18.3227646875C10.683330023841858,18.3227646875,7.620220023841858,17.6377646875,7.620220023841858,17.6377646875C7.620220023841858,17.6377646875,7.571000023841858,17.6587346875,7.571000023841858,17.6587346875C7.571000023841858,17.6587346875,7.532860023841858,17.9395546875,7.532860023841858,17.9395546875C7.091520023841858,20.761944687499998,5.262260023841858,23.0130046875,2.908160023841858,23.6776046875C3.657010023841858,22.6032046875,3.9502300238418577,22.3181046875,4.222270023841858,20.790864687499997C4.403620023841858,19.7726946875,4.486390023841858,18.7275246875,4.470570023841858,17.6553546875C4.470570023841858,17.6553546875,4.553000023841857,17.6544746875,4.553000023841857,17.6544746875C4.553000023841857,17.6544746875,4.515530023841858,17.6377646875,4.515530023841858,17.6377646875C4.515530023841858,17.6377646875,1.350000023841858,18.3227646875,1.350000023841858,18.3227646875C1.350000023841858,18.3227646875,6.016670023841858,12.2998046875,6.016670023841858,12.2998046875C6.016670023841858,12.2998046875,6.016670023841858,12.2998046875,6.016670023841858,12.2998046875Z" fill-rule="evenodd" fill="#00B42A" fill-opacity="1"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="12" height="12" viewBox="0 0 12 12"><defs><clipPath id="master_svg0_0_7191"><rect x="0" y="0" width="12" height="12" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_0_7191)"><g><path d="M6.016670023841858,0.2998046875C6.016670023841858,0.2998046875,10.683330023841858,6.3227646875,10.683330023841858,6.3227646875C10.683330023841858,6.3227646875,7.620220023841858,5.6377646875,7.620220023841858,5.6377646875C7.620220023841858,5.6377646875,7.571000023841858,5.6587346875,7.571000023841858,5.6587346875C7.571000023841858,5.6587346875,7.532860023841858,5.9395546875,7.532860023841858,5.9395546875C7.091520023841858,8.7619446875,5.262260023841858,11.0130046875,2.908160023841858,11.6776046875C3.657010023841858,10.6032046875,3.9502300238418577,10.3181046875,4.222270023841858,8.7908646875C4.403620023841858,7.7726946875,4.486390023841858,6.7275246875,4.470570023841858,5.6553546875C4.470570023841858,5.6553546875,4.553000023841857,5.6544746875,4.553000023841857,5.6544746875C4.553000023841857,5.6544746875,4.515530023841858,5.6377646875,4.515530023841858,5.6377646875C4.515530023841858,5.6377646875,1.350000023841858,6.3227646875,1.350000023841858,6.3227646875C1.350000023841858,6.3227646875,6.016670023841858,0.2998046875,6.016670023841858,0.2998046875C6.016670023841858,0.2998046875,6.016670023841858,0.2998046875,6.016670023841858,0.2998046875Z" fill-rule="evenodd" fill="#F53F3F" fill-opacity="1"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -2,9 +2,18 @@
<div class="statistics-card" :style="cardStyle">
<div class="card-content">
<div class="info">
<div class="value" :style="valueStyle">{{ value }}</div>
<div class="label" :style="labelStyle">{{ label }}</div>
<div v-if="subtitle" class="subtitle" :style="subtitleStyle">{{ subtitle }}</div>
<div class="label" :style="labelStyle">{{ label | dv}}</div>
<div class="value" :style="valueStyle">
<count-to :start-val="0" :end-val="value" :duration="3000" :decimals="precision"/>
</div>
<div v-if="subLabel" class="subtitle" :style="subtitleStyle">
<span class="sub-label">{{ subLabel | dv}}</span>
<span class="sub-value" :class="{ 'up': subValue >= 0, 'down': subValue < 0 }">
<count-to :start-val="0" :end-val="subValue" :duration="3000" :decimals="precision"/>
<svg-icon v-if="showValueChange && subValue >= 0" icon-class="up" />
<svg-icon v-else-if="showValueChange && subValue < 0" icon-class="down" />
</span>
</div>
</div>
<div class="icon-wrapper">
<i :class="icon" :style="iconStyle"></i>
@ -14,20 +23,28 @@
</template>
<script>
import CountTo from 'vue-count-to'
export default {
name: 'StatisticsCard',
components: {
CountTo
},
props: {
value: {
type: [Number, String],
type: Number,
default: null,
},
label: {
type: String,
required: true
},
subtitle: {
subLabel: {
type: String,
default: ''
default: null
},
subValue: {
type: Number,
default: null
},
icon: {
type: String,
@ -48,6 +65,16 @@ export default {
height: {
type: [Number, String],
default: 120
},
//
showValueChange: {
type: Boolean,
default: true
},
//
precision: {
type: Number,
default: 0
}
},
computed: {
@ -65,7 +92,7 @@ export default {
return {
value: Math.max(16, Math.round(28 * scale)),
label: Math.max(12, Math.round(16 * scale)),
subtitle: Math.max(10, Math.round(12 * scale)),
subtitle: Math.max(10, Math.round(14 * scale)),
icon: Math.max(32, Math.round(64 * scale))
};
},
@ -113,10 +140,10 @@ export default {
transition: all 0.3s ease;
backdrop-filter: blur(5px);
box-sizing: border-box;
cursor: pointer;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px 0 rgba(0,0,0,.15);
transform: translateY(-4px);
}
.card-content {
@ -144,8 +171,24 @@ export default {
}
.subtitle {
color: #909399;
opacity: 0.8;
display: flex;
align-items: flex-end;
.sub-label {
color: #333;
opacity: 0.8;
margin-right: 4px;
}
.sub-value {
color: #333;
opacity: 0.8;
&.up {
color: #dc3545;
}
&.down {
color: #198754;
}
}
}
}
@ -165,7 +208,7 @@ export default {
&:hover {
.icon-wrapper i {
opacity: 0.25;
transform: scale(1.05) rotate(5deg);
transform: scale(1.2) rotate(10deg);
}
}
}

View File

@ -196,16 +196,11 @@ export const StatKeys = {
ORDER_USER_COUNT: "order_user_count", // 累计订单用户
ORDER_COUNT: "order_count", // 订单数量
ORDER_PAY_AMOUNT: "order_pay_amount", // 订单支付金额
ORDER_REFUNDED_AMOUNT: "order_refunded_amount", // 订单已退款金额
ORDER_REFUNDING_AMOUNT: "order_refunding_amount", // 订单退款中金额
ORDER_TODAY_PAY_AMOUNT: "order_today_pay_amount", // 订单今日支付金额
ORDER_TODAY_REFUND_AMOUNT: "order_today_refund_amount", // 订单今日退款金额
ORDER_TODAY_REFUNDING_AMOUNT: "order_today_refunding_amount", // 订单今日退款中金额
ORDER_REFUND_AMOUNT: "order_refund_amount", // 订单退款金额
BONUS_COUNT: "bonus_count", // 分成数量
BONUS_AMOUNT: "bonus_amount", // 分成总金额
BONUS_REFUND_AMOUNT: "bonus_refund_amount", // 分成总退款
USER_COUNT: "user_count", // 用户数量
USER_TODAY_COUNT: "user_today_count", // 今日新增用户数量
USER_BALANCE: "user_balance", // 用户余额
DEVICE_COUNT: "device_count", // 设备数量
DEVICE_STATUS_COUNT: "device_status_count", // 设备状态数量

View File

@ -0,0 +1,102 @@
<template>
<el-row :gutter="20">
<el-col :span="24">
<el-card class="box-card" shadow="never">
<el-row :gutter="20">
<el-col :span="3" v-for="dict in dict.type.device_status" :key="dict.value">
<el-statistic
class="statistic"
group-separator=","
:value="stat.device.statusCount[dict.value] || 0"
:title="getDeviceStatusLabel(dict.value)"
suffix="辆"
>
<template #prefix>
<el-icon :class="statusMap[dict.value].icon" :style="{ color: statusMap[dict.value].color }" font-size="20px"/>
</template>
</el-statistic>
</el-col>
<el-col :span="3" v-for="status in ['1', '0']" :key="`online-${status}`">
<el-statistic
class="statistic"
group-separator=","
:value="stat.device.onlineStatusCount[status] || 0"
:title="status == 1 ? '在线' : '离线'"
suffix="辆"
>
<template #prefix>
<el-icon :class="onlineMap[status].icon" :style="{ color: onlineMap[status].color }" font-size="20px"/>
</template>
</el-statistic>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
</template>
<script>
import { dictLabel } from '@/utils'
import { DeviceStatus } from '@/utils/enums'
export default {
name: 'DeviceStat',
dicts: ['device_status'],
props: {
stat: {
type: Object,
required: true
}
},
data() {
return {
statusMap: {
[DeviceStatus.STORAGE]: {
icon: 'el-icon-box',
color: '#13C2C2' // ,
},
[DeviceStatus.AVAILABLE]: {
icon: 'el-icon-bicycle',
color: '#52C41A' // 绿,
},
[DeviceStatus.RESERVED]: {
icon: 'el-icon-timer',
color: '#722ED1' // ,
},
[DeviceStatus.IN_USE]: {
icon: 'el-icon-user-solid',
color: '#73D13D' // 绿,使
},
[DeviceStatus.TEMP_LOCKED]: {
icon: 'el-icon-lock',
color: '#EB2F96' // ,
},
[DeviceStatus.DISPATCHING]: {
icon: 'el-icon-position',
color: '#FF7A45' // ,
},
[DeviceStatus.DISABLED]: {
icon: 'el-icon-warning-outline',
color: '#FF4D4F' // ,
}
},
onlineMap: {
'1': {
icon: 'el-icon-success',
color: '#52C41A' // 绿,线
},
'0': {
icon: 'el-icon-error',
color: '#FF4D4F' // ,线
}
}
}
},
methods: {
getDeviceStatusLabel(value) {
return dictLabel(this.dict.type.device_status, value)
}
}
}
</script>

View File

@ -0,0 +1,35 @@
<template>
<el-card header="订单统计" shadow="never">
</el-card>
</template>
<script>
import { getOrderDailyAmount } from '@/api/dashboard/dashboardOrder'
export default {
name: "OrderDailyStat",
data() {
return {
loading: false,
orderDailyAmount: [],
queryParams: {},
}
},
created() {
this.getOrderDailyAmount();
},
methods: {
getOrderDailyAmount() {
this.loading = true;
getOrderDailyAmount(this.orderQueryParams).then(res => {
this.orderDailyAmount = res.data;
})
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,80 @@
<template>
<el-row :gutter="10">
<el-col :span="span">
<statistics-card
:value="stat.order.payAmount - stat.orderRefund.amount"
label="订单实收"
icon="el-icon-money"
start-color="#FF4D4F"
end-color="#FF7A45"
sub-label="今日订单实收"
:sub-value="todayStat.order.payAmount - todayStat.orderRefund.amount"
:precision="2"
/>
</el-col>
<el-col :span="span">
<statistics-card
:value="stat.device.count"
label="车辆总数"
icon="el-icon-bicycle"
start-color="#52C41A"
end-color="#73D13D"
sub-label="车型总数"
:sub-value="stat.model.count"
:show-value-change="false"
/>
</el-col>
<el-col :span="span">
<statistics-card
:value="stat.user.count"
label="用户总数"
icon="el-icon-user"
start-color="#722ED1"
end-color="#EB2F96"
sub-label="今日新增"
:sub-value="todayStat.user.count"
/>
</el-col>
<el-col :span="span">
<statistics-card
:value="stat.area.count"
label="运营区数量"
icon="el-icon-location"
start-color="#13C2C2"
end-color="#52C41A"
sub-label="商户余额"
:sub-value="stat.user.balance"
:show-value-change="false"
/>
</el-col>
</el-row>
</template>
<script>
import StatisticsCard from '@/components/StatisticsCard'
export default {
name: 'Stat',
components: {
StatisticsCard
},
props: {
stat: {
type: Object,
required: true
},
todayStat: {
type: Object,
required: true
}
},
data() {
return {
span: 6
}
},
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,101 @@
<template>
<el-card class="todo-list" v-loading="loading" shadow="never" header="待办事项">
<div class="todo-item" @click="$router.push('/money/withdraw?status=11')">
<div class="label"><i class="el-icon-wallet"/> 提现申请</div>
<div class="value">
<count-to :start-val="0" :end-val="stat.withdrawCount" :duration="3000"/>
</div>
<div class="unit"></div>
</div>
<div class="todo-item" @click="$router.push('/money/withdraw?status=11')">
<div class="label"><i class="el-icon-refresh"/> 还车审核</div>
<div class="value">
<count-to :start-val="0" :end-val="stat.withdrawCount" :duration="3000"/>
</div>
<div class="unit"></div>
</div>
<div class="todo-item" @click="$router.push('/complaint/mchApply?status=0')">
<div class="label"><i class="el-icon-office-building"/> 商家加盟</div>
<div class="value">
<count-to :start-val="0" :end-val="stat.mchApplyCount" :duration="3000"/>
</div>
<div class="unit"></div>
</div>
<div class="todo-item" @click="$router.push('/complaint/abnormal?status=1')">
<div class="label"><i class="el-icon-warning-outline"/> 待处理故障信息</div>
<div class="value">
<count-to :start-val="0" :end-val="stat.abnormalCount" :duration="3000"/>
</div>
<div class="unit"></div>
</div>
</el-card>
</template>
<script>
import CountTo from 'vue-count-to'
export default {
name: 'TodoList',
components: {
CountTo
},
props: {
stat: {
type: Object,
default: () => ({})
}
},
data() {
return {
loading: false,
}
},
}
</script>
<style scoped lang="scss">
.todo-list {
position: relative;
width: 100%;
}
.todo-item {
position: relative;
width: 100%;
display: flex;
transition: .25s;
padding: 0.55em 1em;
cursor: pointer;
border-radius: 16px;
vertical-align: bottom;
background: #fff;
.value {
display: inline-block;
width: fit-content;
margin-left: 1em;
color: #165DFF;
font-size: 20px;
}
.label {
display: flex;
align-items: center;
color: #1D252F;
flex: 1;
i {
color: #165DFF;
font-size: 20px;
margin-right: 0.3em;
}
}
.unit {
display: inline-block;
margin-left: 0.5em;
font-size: 14px;
}
}
.todo-item:hover {
background: linear-gradient(180deg, #F2F9FE -3%, #E6F4FE 100%);
}
.todo-item:nth-child(n + 2) {
margin-top: 8px;
}
</style>

View File

@ -1,127 +1,32 @@
<template>
<div class="app-container" style="min-height: 600px;" v-loading="loading">
<el-row :gutter="20" v-if="stat">
<el-row :gutter="gutter" v-if="stat">
<el-col :span="18">
<el-row :gutter="10">
<!-- 用户相关统计 -->
<el-col :span="span">
<statistics-card
:value="stat.user.balance"
label="用户总余额"
icon="el-icon-wallet"
start-color="#1890FF"
end-color="#36CBCB"
/>
</el-col>
<el-col :span="span">
<statistics-card
:value="stat.user.count"
label="用户总数"
icon="el-icon-user"
start-color="#722ED1"
end-color="#EB2F96"
/>
</el-col>
<el-col :span="span">
<statistics-card
:value="stat.user.todayCount"
label="今日新增用户"
icon="el-icon-user-plus"
start-color="#FA541C"
end-color="#FAAD14"
/>
</el-col>
<el-col :span="span">
<statistics-card
:value="stat.area.count"
label="运营区数量"
icon="el-icon-location"
start-color="#13C2C2"
end-color="#52C41A"
/>
</el-col>
</el-row>
<el-row :gutter="10" style="margin-top: 10px;">
<!-- 订单相关统计 -->
<el-col :span="span">
<statistics-card
:value="formatAmount(stat.order.payAmount - (stat.order.refundingAmount) - (stat.order.refundedAmount))"
label="订单总收入"
icon="el-icon-money"
start-color="#FF4D4F"
end-color="#FF7A45"
/>
</el-col>
<el-col :span="span">
<statistics-card
:value="formatAmount(stat.order.todayPayAmount - (stat.order.todayRefundingAmount) - (stat.order.todayRefundedAmount))"
label="今日订单收入"
icon="el-icon-date"
start-color="#2F54EB"
end-color="#597EF7"
/>
</el-col>
<el-col :span="span">
<statistics-card
:value="stat.device.count"
label="车辆总数"
icon="el-icon-bicycle"
start-color="#52C41A"
end-color="#73D13D"
/>
</el-col>
<el-col :span="span">
<statistics-card
:value="stat.model.count"
label="车型总数"
icon="el-icon-menu"
start-color="#722ED1"
end-color="#85A5FF"
/>
</el-col>
</el-row>
<!-- 车辆状态统计 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="24">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>车辆状态统计</span>
</div>
<el-row :gutter="20">
<el-col :span="4" v-for="status in deviceStatusList" :key="status">
<div class="status-item">
<div class="status-count">{{ stat.device.statusCount[status] || 0 }}</div>
<div class="status-label">{{ getDeviceStatusLabel(status) }}</div>
</div>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
<!-- 统计信息 -->
<stat :stat="stat" :today-stat="todayStat" />
<!-- 设备统计信息 -->
<device-stat :stat="stat" style="margin-top: 12px;"/>
<!-- 车辆在线状态统计 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="24">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>车辆在线状态统计</span>
</div>
<el-row :gutter="20">
<el-col :span="12" v-for="status in ['1', '0']" :key="status">
<div class="status-item">
<div class="status-count">{{ stat.device.onlineStatusCount[status] || 0 }}</div>
<div class="status-label">{{ status === '1' ? '在线' : '离线' }}</div>
</div>
</el-col>
</el-row>
<el-row :gutter="gutter" style="margin-top:12px">
<el-col :span="16">
<order-daily-stat/>
</el-col>
<el-col :span="8">
<el-card header="提现" shadow="never">
待实现
</el-card>
</el-col>
</el-row>
</el-col>
<el-col :span="6">
<!-- 待办事项 -->
<todo-list :stat="stat"/>
<el-card style="margin-top: 12px;" header="订单排行" shadow="never">
待实现
</el-card>
</el-col>
</el-row>
@ -132,42 +37,36 @@
<script>
import { getStat } from '@/api/dashboard/dashboard'
import { StatKeys } from '@/utils/enums'
import { DeviceStatus } from '@/utils/enums'
import StatisticsCard from '@/components/StatisticsCard'
import Stat from '@/views/bst/index/components/Stat'
import DeviceStat from '@/views/bst/index/components/DeviceStat'
import TodoList from '@/views/bst/index/components/TodoList'
import { getLastDateStr } from '@/utils'
import OrderDailyStat from './components/OrderDailyStat.vue'
export default {
name: 'Index',
components: {
StatisticsCard
Stat,
DeviceStat,
TodoList,
OrderDailyStat,
},
data() {
return {
span: 6,
gutter: 12,
loading: false,
stat: null,
deviceStatusList: [
DeviceStatus.STORAGE, //
DeviceStatus.AVAILABLE, //
DeviceStatus.RESERVED, //
DeviceStatus.IN_USE, //
DeviceStatus.TEMP_LOCKED, //
DeviceStatus.DISPATCHING, //
DeviceStatus.DISABLED //
],
todayStat: null,
queryParams: {
keys: [
StatKeys.ORDER_COUNT,
StatKeys.ORDER_PAY_AMOUNT,
StatKeys.ORDER_REFUNDED_AMOUNT,
StatKeys.ORDER_REFUNDING_AMOUNT,
StatKeys.ORDER_TODAY_PAY_AMOUNT,
StatKeys.ORDER_TODAY_REFUND_AMOUNT,
StatKeys.ORDER_TODAY_REFUNDING_AMOUNT,
StatKeys.ORDER_REFUND_AMOUNT,
StatKeys.BONUS_COUNT,
StatKeys.BONUS_AMOUNT,
StatKeys.BONUS_REFUND_AMOUNT,
StatKeys.USER_COUNT,
StatKeys.USER_TODAY_COUNT,
StatKeys.USER_BALANCE,
StatKeys.DEVICE_COUNT,
StatKeys.DEVICE_STATUS_COUNT,
@ -179,7 +78,8 @@ export default {
}
},
created() {
this.getStat()
this.getStat();
this.getTodayStat();
},
methods: {
getStat() {
@ -190,52 +90,21 @@ export default {
this.loading = false;
})
},
formatAmount(amount) {
return Number(amount).toFixed(2)
},
getDeviceStatusLabel(status) {
const statusMap = {
[DeviceStatus.STORAGE]: '仓库中',
[DeviceStatus.AVAILABLE]: '待骑行',
[DeviceStatus.RESERVED]: '预约中',
[DeviceStatus.IN_USE]: '骑行中',
[DeviceStatus.TEMP_LOCKED]: '临时锁车',
[DeviceStatus.DISPATCHING]: '调度中',
[DeviceStatus.DISABLED]: '禁用'
}
return statusMap[status] || '未知状态'
getTodayStat() {
getStat({
keys:[
StatKeys.USER_COUNT,
StatKeys.ORDER_PAY_AMOUNT,
StatKeys.ORDER_REFUND_AMOUNT,
],
dateRange: [
getLastDateStr(0),
getLastDateStr(0)
]
}).then(res => {
this.todayStat = res.data
})
}
}
}
</script>
<style lang="scss" scoped>
.el-row {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
.box-card {
.status-item {
text-align: center;
padding: 15px;
background: #f5f7fa;
border-radius: 4px;
margin-bottom: 10px;
.status-count {
font-size: 24px;
font-weight: bold;
color: #303133;
margin-bottom: 8px;
}
.status-label {
font-size: 14px;
color: #606266;
}
}
}
</style>
</script>