feat: the dashboard page is ready for admin console

This commit is contained in:
RockYang 2023-08-02 16:37:47 +08:00
parent 3529649ba9
commit b09d23f97f
7 changed files with 214 additions and 32 deletions

View File

@ -0,0 +1,55 @@
package admin
import (
"chatplus/core"
"chatplus/handler"
"chatplus/store/model"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"time"
)
type DashboardHandler struct {
handler.BaseHandler
db *gorm.DB
}
func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler {
h := DashboardHandler{db: db}
h.App = app
return &h
}
type statsVo struct {
Users int64 `json:"users"`
Chats int64 `json:"chats"`
Tokens int64 `json:"tokens"`
}
func (h *DashboardHandler) Stats(c *gin.Context) {
stats := statsVo{}
// new users statistic
var userCount int64
now := time.Now()
zeroTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
res := h.db.Model(&model.User{}).Where("created_at > ?", zeroTime).Count(&userCount)
if res.Error == nil {
stats.Users = userCount
}
// new chats statistic
var chatCount int64
res = h.db.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&chatCount)
if res.Error == nil {
stats.Chats = chatCount
}
// tokens took stats
var tokenCount int64
res = h.db.Model(&model.HistoryMessage{}).Select("sum(tokens) as tokens_total").Where("created_at > ?", zeroTime).Scan(&tokenCount)
if res.Error == nil {
stats.Tokens = tokenCount
}
resp.SUCCESS(c, stats)
}

View File

@ -141,6 +141,7 @@ func main() {
fx.Provide(admin.NewUserHandler),
fx.Provide(admin.NewChatRoleHandler),
fx.Provide(admin.NewRewardHandler),
fx.Provide(admin.NewDashboardHandler),
// 创建服务
fx.Provide(service.NewAliYunSmsService),
@ -231,6 +232,10 @@ func main() {
group := s.Engine.Group("/api/admin/reward/")
group.GET("list", h.List)
}),
fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
group := s.Engine.Group("/api/admin/dashboard/")
group.GET("stats", h.Stats)
}),
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
err := s.Run(db)

29
web/package-lock.json generated
View File

@ -25,7 +25,8 @@
"sortablejs": "^1.15.0",
"vant": "^4.5.0",
"vue": "^3.2.13",
"vue-router": "^4.0.15"
"vue-router": "^4.0.15",
"vue-schart": "^2.0.0"
},
"devDependencies": {
"@babel/core": "7.18.6",
@ -9366,6 +9367,11 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
"node_modules/schart.js": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/schart.js/-/schart.js-3.0.4.tgz",
"integrity": "sha512-uylb2u9rrHX1jyAuSAJUQON8XTfyDKI9kWj1J3fUlCQCkLVZ4HG4+IiV8qm//Z71dqvLI78QZ/fCBw0reB22Zw=="
},
"node_modules/schema-utils": {
"version": "2.7.1",
"resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-2.7.1.tgz",
@ -10694,6 +10700,14 @@
"vue": "^3.2.0"
}
},
"node_modules/vue-schart": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vue-schart/-/vue-schart-2.0.0.tgz",
"integrity": "sha512-qAu3e5wfMcq26wK1xeHExEWfGpnjfoN1R/9QXblNi+AsU/p52X7tTwhi+Fw7H/otfEufhEY2X7z7emaoF4QO+g==",
"dependencies": {
"schart.js": "^3.0.0"
}
},
"node_modules/vue-style-loader": {
"version": "4.1.3",
"resolved": "https://registry.npmmirror.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
@ -18621,6 +18635,11 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
"schart.js": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/schart.js/-/schart.js-3.0.4.tgz",
"integrity": "sha512-uylb2u9rrHX1jyAuSAJUQON8XTfyDKI9kWj1J3fUlCQCkLVZ4HG4+IiV8qm//Z71dqvLI78QZ/fCBw0reB22Zw=="
},
"schema-utils": {
"version": "2.7.1",
"resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-2.7.1.tgz",
@ -19686,6 +19705,14 @@
"@vue/devtools-api": "^6.0.0"
}
},
"vue-schart": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vue-schart/-/vue-schart-2.0.0.tgz",
"integrity": "sha512-qAu3e5wfMcq26wK1xeHExEWfGpnjfoN1R/9QXblNi+AsU/p52X7tTwhi+Fw7H/otfEufhEY2X7z7emaoF4QO+g==",
"requires": {
"schart.js": "^3.0.0"
}
},
"vue-style-loader": {
"version": "4.1.3",
"resolved": "https://registry.npmmirror.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz",

View File

@ -71,7 +71,7 @@ httpGet('/api/admin/config/get?key=system').then(res => {
const items = [
{
icon: 'home',
index: '/admin/welcome',
index: '/admin/dashboard',
title: '仪表盘',
},
{

View File

@ -35,15 +35,15 @@ const routes = [
{
name: 'admin',
path: '/admin',
redirect: '/admin/welcome',
redirect: '/admin/dashboard',
component: () => import("@/views/admin/Home.vue"),
meta: {title: 'ChatGPT-Plus 管理后台'},
children: [
{
path: '/admin/welcome',
name: 'admin-home',
meta: {title: '系统首页'},
component: () => import('@/views/admin/Welcome.vue'),
path: '/admin/dashboard',
name: 'admin-dashboard',
meta: {title: '仪表盘'},
component: () => import('@/views/admin/Dashboard.vue'),
},
{
path: '/admin/system',

View File

@ -0,0 +1,120 @@
<template>
<div class="dashboard">
<el-row class="mgb20" :gutter="20">
<el-col :span="8">
<el-card shadow="hover" :body-style="{ padding: '0px' }">
<div class="grid-content grid-con-1">
<el-icon class="grid-con-icon">
<User/>
</el-icon>
<div class="grid-cont-right">
<div class="grid-num">{{ stats.users }}</div>
<div>今日新增用户</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" :body-style="{ padding: '0px' }">
<div class="grid-content grid-con-2">
<el-icon class="grid-con-icon">
<ChatDotRound/>
</el-icon>
<div class="grid-cont-right">
<div class="grid-num">{{ stats.chats }}</div>
<div>今日新增对话</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" :body-style="{ padding: '0px' }">
<div class="grid-content grid-con-3">
<el-icon class="grid-con-icon">
<TrendCharts/>
</el-icon>
<div class="grid-cont-right">
<div class="grid-num">{{ stats.tokens }}</div>
<div>今日消耗 Tokens</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import {ref} from 'vue';
import {ChatDotRound, TrendCharts, User} from "@element-plus/icons-vue";
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
const stats = ref({users: 0, chats: 0, tokens: 0})
httpGet('/api/admin/dashboard/stats').then((res) => {
stats.value.users = res.data.users
stats.value.chats = res.data.chats
stats.value.tokens = res.data.tokens
}).catch((e) => {
ElMessage.error("获取统计数据失败:" + e.message)
})
</script>
<style scoped lang="stylus">
.dashboard {
.grid-content {
display: flex;
align-items: center;
height: 100px;
}
.grid-cont-right {
flex: 1;
text-align: center;
font-size: 14px;
color: #999;
}
.grid-num {
font-size: 30px;
font-weight: bold;
}
.grid-con-icon {
font-size: 50px;
width: 100px;
height: 100px;
text-align: center;
line-height: 100px;
color: #fff;
}
.grid-con-1 .grid-con-icon {
background: rgb(45, 140, 240);
}
.grid-con-1 .grid-num {
color: rgb(45, 140, 240);
}
.grid-con-2 .grid-con-icon {
background: rgb(100, 213, 114);
}
.grid-con-2 .grid-num {
color: rgb(100, 213, 114);
}
.grid-con-3 .grid-con-icon {
background: rgb(242, 94, 67);
}
.grid-con-3 .grid-num {
color: rgb(242, 94, 67);
}
}
</style>

View File

@ -1,25 +0,0 @@
<template>
<div class="welcome">
<h1>ChatGPT-PLUS 控制台</h1>
</div>
</template>
<script setup></script>
<style lang="stylus" scoped>
.welcome {
display: flex;
justify-content: center;
align-items: center;
color: #202020;
background-color: #282c34;
height 100%;
h1 {
font-size: 300%;
font-weight: bold;
letter-spacing: 0.1em;
text-shadow: -1px -1px 1px #111111, 2px 2px 1px #363636;
}
}
</style>