feat: add data statistic chart

This commit is contained in:
engigu
2024-01-24 21:33:51 +08:00
parent 4ded53c2ce
commit 7440c377fb
10 changed files with 382 additions and 5 deletions
+63
View File
@@ -101,3 +101,66 @@ func DeleteOutDateLogs(keepNum int) (int, error) {
affectedRows = int(result.RowsAffected)
return affectedRows, nil
}
type StatisticData struct {
TodaySuccNum int `json:"today_succ_num"`
TodayFailedNum int `json:"today_failed_num"`
TodayTotalNum int `json:"today_total_num"`
LatestSendData []LatestSendData `json:"latest_send_data"`
WayCateData []WayCateData `json:"way_cate_data"`
}
type LatestSendData struct {
Day string `json:"day"`
Num int `json:"num"`
}
type WayCateData struct {
WayName string `json:"way_name"`
CountNum int `json:"count_num"`
}
// GetStatisticData 获取统计数据
func GetStatisticData() (StatisticData, error) {
var statistic StatisticData
var latestData []LatestSendData
var wayCateData []WayCateData
logt := db.NewScope(SendTasksLogs{}).TableName()
inst := db.NewScope(SendTasksIns{}).TableName()
wayst := db.NewScope(SendWays{}).TableName()
// 今日统计数据
query := db.
Table(logt).
Select(`
COUNT(*) AS today_total_num,
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS today_succ_num,
SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) AS today_failed_num`).
Where("DATE(created_on) = CURDATE()")
query.First(&statistic)
// 最近30天数据
days := 30
queryData := db.
Table(logt).
Select(`
CAST(DATE(created_on) AS CHAR) AS day,
COUNT(*) AS num`).
Where(" created_on >= CURDATE() - INTERVAL ? DAY", days).
Group("DATE(created_on)").
Order("created_on")
queryData.Scan(&latestData)
// 消息实例分类数目
db.
Table(inst).
Select(fmt.Sprintf("%s.name as way_name, count(%s.id) as count_num", wayst, wayst)).
Joins(fmt.Sprintf("JOIN %s ON %s.way_id = %s.id", wayst, inst, wayst)).
Group(fmt.Sprintf("%s.id", wayst)).
Scan(&wayCateData)
statistic.LatestSendData = latestData
statistic.WayCateData = wayCateData
return statistic, nil
}
+25
View File
@@ -0,0 +1,25 @@
package v1
import (
"fmt"
"github.com/gin-gonic/gin"
"message-nest/pkg/app"
"message-nest/service/statistic_service"
"net/http"
)
// GetStatisticData 获取发送统计数据
func GetStatisticData(c *gin.Context) {
var (
appG = app.Gin{C: c}
)
msgService := statistic_service.StatisticService{}
data, err := msgService.GetStatisticData()
if err != nil {
appG.CResponse(http.StatusInternalServerError, fmt.Sprintf("获取失败!原因:%s", err), nil)
return
}
appG.CResponse(http.StatusOK, "获取成功", data)
}
+3
View File
@@ -81,6 +81,9 @@ func InitRouter(f embed.FS) *gin.Engine {
apiV1.POST("/settings/reset", v1.RestDefaultSettings)
apiV1.GET("/settings/getsetting", v1.GetUserSetting)
// statistic
apiV1.GET("/statistic", v1.GetStatisticData)
}
return app
+12
View File
@@ -0,0 +1,12 @@
package statistic_service
import (
"message-nest/models"
)
type StatisticService struct {
}
func (sw *StatisticService) GetStatisticData() (models.StatisticData, error) {
return models.GetStatisticData()
}
+59
View File
@@ -10,6 +10,7 @@
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"axios": "^1.6.2",
"echarts": "^5.4.3",
"element-plus": "^2.4.2",
"lodash": "^4.17.21",
"pinia": "^2.1.7",
@@ -1772,6 +1773,20 @@
"node": ">=6.0.0"
}
},
"node_modules/echarts": {
"version": "5.4.3",
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.4.3.tgz",
"integrity": "sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==",
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.4.4"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
},
"node_modules/electron-to-chromium": {
"version": "1.4.616",
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz",
@@ -3745,6 +3760,19 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zrender": {
"version": "5.4.4",
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.4.4.tgz",
"integrity": "sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==",
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
}
},
"dependencies": {
@@ -4960,6 +4988,22 @@
"esutils": "^2.0.2"
}
},
"echarts": {
"version": "5.4.3",
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.4.3.tgz",
"integrity": "sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==",
"requires": {
"tslib": "2.3.0",
"zrender": "5.4.4"
},
"dependencies": {
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
}
}
},
"electron-to-chromium": {
"version": "1.4.616",
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz",
@@ -6277,6 +6321,21 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
},
"zrender": {
"version": "5.4.4",
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.4.4.tgz",
"integrity": "sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==",
"requires": {
"tslib": "2.3.0"
},
"dependencies": {
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
}
}
}
}
}
+1
View File
@@ -14,6 +14,7 @@
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"axios": "^1.6.2",
"echarts": "^5.4.3",
"element-plus": "^2.4.2",
"lodash": "^4.17.21",
"pinia": "^2.1.7",
+6 -1
View File
@@ -11,6 +11,12 @@ const router = createRouter({
name: 'login',
component: LoginInex
},
{
path: '/statistic',
name: 'statistic',
alias: '/',
component: () => import('../views/tabsTools/statistic/statistic.vue')
},
{
path: '/sendways',
name: 'sendWays',
@@ -23,7 +29,6 @@ const router = createRouter({
},
{
path: '/sendlogs',
alias: '/',
name: 'sendlogs',
component: () => import('../views/tabsTools/sendLogs/sendLogs.vue')
},
+3 -2
View File
@@ -2,7 +2,7 @@
<div class="inex-title-bar" v-if="pageState.isLogin">
<el-menu :collapse="isCollapse" breakpoint="768px" mode="horizontal" @select="handleSelect()"
:default-active="currActivate()" :ellipsis="false" :menu-width="'auto'">
<el-menu-item index="0" :disabled="false">
<el-menu-item index="" :disabled="false">
<img style="width: 60px" class="title-logo" :src="titleLogo" alt="Message logo" />
</el-menu-item>
@@ -33,6 +33,7 @@ export default {
titleLogo: '',
});
const menuData = reactive([
{ id: '0', title: '数据统计', path: '/statistic' },
{ id: '1', title: '发信日志', path: '/sendlogs' },
{ id: '2', title: '发信任务', path: '/sendtasks' },
{ id: '3', title: '发信渠道', path: '/sendways' },
@@ -54,7 +55,7 @@ export default {
const currActivate = () => {
const cur_path = useRoute().path;
let result = '1';
let result = '0';
menuData.forEach(element => {
if (element.path == cur_path) {
result = element.id;
@@ -42,9 +42,7 @@
import { defineComponent, reactive, toRefs, onMounted } from 'vue';
import { ElMessage } from 'element-plus'
import { QuestionFilled } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router';
import { request } from '@/api/api'
import { CONSTANT } from '@/constant'
import { LocalStieConfigUtils } from '@/util/localSiteConfig'
@@ -0,0 +1,210 @@
<template>
<div class="main-center-body">
<div class="container">
<el-row :gutter="16">
<el-col :span="8">
<div class="statistic-card">
<el-statistic :value="data.today_total_num">
<template #title>
<div style="display: inline-flex; align-items: center">
今日发送总数
</div>
</template>
</el-statistic>
</div>
</el-col>
<el-col :span="8">
<div class="statistic-card">
<el-statistic :value="data.today_succ_num">
<template #title>
<div style="display: inline-flex; align-items: center">
今日发送成功数
</div>
</template>
</el-statistic>
</div>
</el-col>
<el-col :span="8">
<div class="statistic-card">
<el-statistic :value="data.today_failed_num">
<template #title>
<div style="display: inline-flex; align-items: center">
今日发送失败数
</div>
</template>
</el-statistic>
</div>
</el-col>
</el-row>
<el-divider />
<div class="echarts-box">
<div id="daily-chart" style="width: 65%; height: 350px;"></div>
<div id="send-cate-chart" style="width: 35%; height: 300px;"></div>
</div>
</div>
</div>
</template>
<script >
// import {
// ArrowRight,
// CaretBottom,
// CaretTop,
// Warning,
// } from '@element-plus/icons-vue'
import { reactive, toRefs, onMounted, onUnmounted } from 'vue'
import { request } from '@/api/api'
import * as echarts from "echarts"
export default {
setup() {
const state = reactive({
data: {},
dailyChart: {},
sendCateChart: {},
});
let echart = echarts;
const getStatisticData = async () => {
const rsp = await request.get('/statistic');
if (await rsp.data.code == 200) {
let data = await rsp.data.data;
state.data = data;
}
}
onMounted(async () => {
await getStatisticData();
initDailyChart();
initSendCateChart();
});
onUnmounted(() => {
state.dailyChart.dispose();
state.sendCateChart.dispose();
});
// 最近30天数据图
function initDailyChart() {
state.dailyChart = echart.init(document.getElementById("daily-chart"));
let xAxisdata = [];
state.data.latest_send_data.forEach(element => {
xAxisdata.push(element.day);
});
let yAxisdata = [];
state.data.latest_send_data.forEach(element => {
yAxisdata.push(element.num);
});
state.dailyChart.setOption({
title: {
subtext: '最近消息30天发送数据',
top: 0,
textStyle: {
color: '#333',
fontSize: 18,
},
},
xAxis: {
type: "category",
data: xAxisdata
},
tooltip: {
trigger: "axis"
},
yAxis: {
type: "value"
},
series: [
{
data: yAxisdata,
type: "line",
smooth: true
}
]
});
window.onresize = function () {
state.dailyChart.resize();
};
}
// 发送消息实例类别图
function initSendCateChart() {
state.sendCateChart = echart.init(document.getElementById("send-cate-chart"));
let data = [];
state.data.way_cate_data.forEach(element => {
data.push({ name: element.way_name, value: element.count_num });
});
state.sendCateChart.setOption({
grid: {
width: '60%',
height: '60%',
},
title: {
subtext: '发送消息实例渠道',
top: 0,
textStyle: {
color: '#333',
fontSize: 18,
},
},
series: [
{
type: 'pie',
data: data,
radius: '40%',
// roseType: 'area'
}
]
});
window.onresize = function () {
state.sendCateChart.resize();
};
}
return {
...toRefs(state)
};
}
}
</script>
<style scoped>
.container {
max-width: 1000px;
height: 450px;
}
.el-statistic {
--el-statistic-content-font-size: 28px;
text-align: center;
}
.statistic-card {
height: 100%;
padding: 20px;
border-radius: 4px;
background-color: var(--el-bg-color-overlay);
}
.echarts-box {
display: flex;
}
.green {
color: var(--el-color-success);
}
.red {
color: var(--el-color-error);
}
</style>