mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-27 13:34:25 +08:00
增加 dashboard 页面
This commit is contained in:
@@ -43,36 +43,89 @@ type statsVo struct {
|
|||||||
|
|
||||||
func (h *DashboardHandler) Stats(c *gin.Context) {
|
func (h *DashboardHandler) Stats(c *gin.Context) {
|
||||||
stats := statsVo{}
|
stats := statsVo{}
|
||||||
// new users statistic
|
|
||||||
var userCount int64
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
zeroTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
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
|
h.DB.Model(&model.User{}).Count(&stats.Users)
|
||||||
res = h.DB.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&chatCount)
|
|
||||||
if res.Error == nil {
|
|
||||||
stats.Chats = chatCount
|
|
||||||
}
|
|
||||||
|
|
||||||
// tokens took stats
|
// 今日新增用户
|
||||||
|
h.DB.Model(&model.User{}).Where("created_at > ?", zeroTime).Count(&stats.TodayUsers)
|
||||||
|
|
||||||
|
// 总对话数
|
||||||
|
h.DB.Model(&model.ChatItem{}).Count(&stats.Chats)
|
||||||
|
|
||||||
|
// 今日新增对话
|
||||||
|
h.DB.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&stats.TodayChats)
|
||||||
|
|
||||||
|
// 总Token消耗
|
||||||
var historyMessages []model.ChatMessage
|
var historyMessages []model.ChatMessage
|
||||||
res = h.DB.Where("created_at > ?", zeroTime).Find(&historyMessages)
|
h.DB.Find(&historyMessages)
|
||||||
for _, item := range historyMessages {
|
for _, item := range historyMessages {
|
||||||
stats.Tokens += item.Tokens
|
stats.Tokens += item.Tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
// 订单收入
|
// 今日Token消耗
|
||||||
var orders []model.Order
|
var todayMessages []model.ChatMessage
|
||||||
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&orders)
|
h.DB.Where("created_at > ?", zeroTime).Find(&todayMessages)
|
||||||
for _, item := range orders {
|
for _, item := range todayMessages {
|
||||||
|
stats.TodayTokens += item.Tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
// 总收入
|
||||||
|
var allOrders []model.Order
|
||||||
|
h.DB.Where("status = ?", types.OrderPaidSuccess).Find(&allOrders)
|
||||||
|
for _, item := range allOrders {
|
||||||
stats.Income += item.Amount
|
stats.Income += item.Amount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 今日收入
|
||||||
|
var todayOrders []model.Order
|
||||||
|
h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&todayOrders)
|
||||||
|
for _, item := range todayOrders {
|
||||||
|
stats.TodayIncome += item.Amount
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单总数
|
||||||
|
h.DB.Model(&model.Order{}).Where("status = ?", types.OrderPaidSuccess).Count(&stats.Orders)
|
||||||
|
|
||||||
|
// 今日订单数
|
||||||
|
h.DB.Model(&model.Order{}).Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Count(&stats.TodayOrders)
|
||||||
|
|
||||||
|
// 图片生成任务统计
|
||||||
|
var mjJobs, sdJobs, dallJobs, jimengImageJobs int64
|
||||||
|
h.DB.Model(&model.MidJourneyJob{}).Count(&mjJobs)
|
||||||
|
h.DB.Model(&model.SdJob{}).Count(&sdJobs)
|
||||||
|
h.DB.Model(&model.DallJob{}).Count(&dallJobs)
|
||||||
|
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_image", "image_to_image", "image_edit", "image_effects"}).Count(&jimengImageJobs)
|
||||||
|
stats.ImageJobs = mjJobs + sdJobs + dallJobs + jimengImageJobs
|
||||||
|
|
||||||
|
// 今日图片生成任务统计
|
||||||
|
var todayMjJobs, todaySdJobs, todayDallJobs, todayJimengImageJobs int64
|
||||||
|
h.DB.Model(&model.MidJourneyJob{}).Where("created_at > ?", zeroTime).Count(&todayMjJobs)
|
||||||
|
h.DB.Model(&model.SdJob{}).Where("created_at > ?", zeroTime).Count(&todaySdJobs)
|
||||||
|
h.DB.Model(&model.DallJob{}).Where("created_at > ?", zeroTime).Count(&todayDallJobs)
|
||||||
|
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_image", "image_to_image", "image_edit", "image_effects"}).Where("created_at > ?", zeroTime).Count(&todayJimengImageJobs)
|
||||||
|
stats.TodayImageJobs = todayMjJobs + todaySdJobs + todayDallJobs + todayJimengImageJobs
|
||||||
|
|
||||||
|
// 视频生成任务统计
|
||||||
|
var videoJobs, jimengVideoJobs int64
|
||||||
|
h.DB.Model(&model.VideoJob{}).Count(&videoJobs)
|
||||||
|
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_video", "image_to_video"}).Count(&jimengVideoJobs)
|
||||||
|
stats.VideoJobs = videoJobs + jimengVideoJobs
|
||||||
|
|
||||||
|
// 今日视频生成任务统计
|
||||||
|
var todayVideoJobs, todayJimengVideoJobs int64
|
||||||
|
h.DB.Model(&model.VideoJob{}).Where("created_at > ?", zeroTime).Count(&todayVideoJobs)
|
||||||
|
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_video", "image_to_video"}).Where("created_at > ?", zeroTime).Count(&todayJimengVideoJobs)
|
||||||
|
stats.TodayVideoJobs = todayVideoJobs + todayJimengVideoJobs
|
||||||
|
|
||||||
|
// 音乐生成任务统计
|
||||||
|
h.DB.Model(&model.SunoJob{}).Count(&stats.MusicJobs)
|
||||||
|
|
||||||
|
// 今日音乐生成任务统计
|
||||||
|
h.DB.Model(&model.SunoJob{}).Where("created_at > ?", zeroTime).Count(&stats.TodayMusicJobs)
|
||||||
|
|
||||||
// 统计7天的订单的图表
|
// 统计7天的订单的图表
|
||||||
startDate := now.Add(-7 * 24 * time.Hour).Format("2006-01-02")
|
startDate := now.Add(-7 * 24 * time.Hour).Format("2006-01-02")
|
||||||
var statsChart = make(map[string]map[string]float64)
|
var statsChart = make(map[string]map[string]float64)
|
||||||
@@ -101,6 +154,7 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 统计最近7天的订单
|
// 统计最近7天的订单
|
||||||
|
var orders []model.Order
|
||||||
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders)
|
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders)
|
||||||
for _, item := range orders {
|
for _, item := range orders {
|
||||||
incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64()
|
incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64()
|
||||||
|
|||||||
@@ -1,260 +1,770 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dashboard" v-loading="loading">
|
<div class="dashboard" v-loading="loading">
|
||||||
<el-row class="mgb20" :gutter="20">
|
<!-- 统计卡片区域 -->
|
||||||
|
<el-row class="stats-row" :gutter="24">
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-card shadow="hover" :body-style="{ padding: '0px' }">
|
<el-card class="stats-card" shadow="hover">
|
||||||
<div class="grid-content grid-con-1">
|
<div class="card-content">
|
||||||
<el-icon class="grid-con-icon">
|
<div class="card-icon user-icon">
|
||||||
<User/>
|
<el-icon><User /></el-icon>
|
||||||
</el-icon>
|
</div>
|
||||||
<div class="grid-cont-right">
|
<div class="card-info">
|
||||||
<div class="grid-num">{{ stats.users }}</div>
|
<div class="card-number">{{ stats.users }}</div>
|
||||||
<div>今日新增用户</div>
|
<div class="card-label">用户总数</div>
|
||||||
|
<div class="card-desc">今日新增: {{ stats.todayUsers || 1 }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-card shadow="hover" :body-style="{ padding: '0px' }">
|
<el-card class="stats-card" shadow="hover">
|
||||||
<div class="grid-content grid-con-2">
|
<div class="card-content">
|
||||||
<el-icon class="grid-con-icon">
|
<div class="card-icon chat-icon">
|
||||||
<ChatDotRound/>
|
<el-icon><ChatDotRound /></el-icon>
|
||||||
</el-icon>
|
</div>
|
||||||
<div class="grid-cont-right">
|
<div class="card-info">
|
||||||
<div class="grid-num">{{ stats.chats }}</div>
|
<div class="card-number">{{ stats.chats }}</div>
|
||||||
<div>今日新增对话</div>
|
<div class="card-label">对话总数</div>
|
||||||
|
<div class="card-desc">今日: {{ stats.todayChats }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-card shadow="hover" :body-style="{ padding: '0px' }">
|
<el-card class="stats-card" shadow="hover">
|
||||||
<div class="grid-content grid-con-3">
|
<div class="card-content">
|
||||||
<el-icon class="grid-con-icon">
|
<div class="card-icon token-icon">
|
||||||
<TrendCharts/>
|
<el-icon><TrendCharts /></el-icon>
|
||||||
</el-icon>
|
</div>
|
||||||
<div class="grid-cont-right">
|
<div class="card-info">
|
||||||
<div class="grid-num">{{ stats.tokens }}</div>
|
<div class="card-number">{{ formatNumber(stats.tokens) }}</div>
|
||||||
<div>今日消耗 Tokens</div>
|
<div class="card-label">Token消耗</div>
|
||||||
|
<div class="card-desc">今日: {{ formatNumber(stats.todayTokens) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-card shadow="hover" :body-style="{ padding: '0px' }">
|
<el-card class="stats-card" shadow="hover">
|
||||||
<div class="grid-content grid-con-3">
|
<div class="card-content">
|
||||||
<el-icon class="grid-con-icon">
|
<div class="card-icon income-icon">
|
||||||
<i class="iconfont icon-reward"></i>
|
<el-icon><Money /></el-icon>
|
||||||
</el-icon>
|
</div>
|
||||||
<div class="grid-cont-right">
|
<div class="card-info">
|
||||||
<div class="grid-num">¥{{ stats.income }}</div>
|
<div class="card-number">¥{{ stats.income.toFixed(2) }}</div>
|
||||||
<div>今日入账</div>
|
<div class="card-label">总收入</div>
|
||||||
|
<div class="card-desc">今日: ¥{{ stats.todayIncome?.toFixed(2) || '0.00' }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row class="mgb20" :gutter="20">
|
<!-- 第二行统计卡片 -->
|
||||||
<el-col :span="8">
|
<el-row class="stats-row" :gutter="24">
|
||||||
<div class="e-chart">
|
<el-col :span="6">
|
||||||
<div id="chart-users" style="height: 400px"></div>
|
<el-card class="stats-card" shadow="hover">
|
||||||
<div class="title">最近7日用户注册</div>
|
<div class="card-content">
|
||||||
</div>
|
<div class="card-icon image-icon">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-number">{{ stats.imageJobs }}</div>
|
||||||
|
<div class="card-label">图片生成</div>
|
||||||
|
<div class="card-desc">今日: {{ stats.todayImageJobs }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<el-col :span="8">
|
<el-col :span="6">
|
||||||
<div class="e-chart">
|
<el-card class="stats-card" shadow="hover">
|
||||||
<div id="chart-tokens" style="height: 400px"></div>
|
<div class="card-content">
|
||||||
<div class="title">最近7日Token消耗</div>
|
<div class="card-icon video-icon">
|
||||||
</div>
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-number">{{ stats.videoJobs }}</div>
|
||||||
|
<div class="card-label">视频生成</div>
|
||||||
|
<div class="card-desc">今日: {{ stats.todayVideoJobs }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stats-card" shadow="hover">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-icon music-icon">
|
||||||
|
<el-icon><Headset /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-number">{{ stats.musicJobs }}</div>
|
||||||
|
<div class="card-label">音乐生成</div>
|
||||||
|
<div class="card-desc">今日: {{ stats.todayMusicJobs }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stats-card" shadow="hover">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-icon order-icon">
|
||||||
|
<el-icon><ShoppingCart /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-number">{{ stats.orders }}</div>
|
||||||
|
<div class="card-label">订单总数</div>
|
||||||
|
<div class="card-desc">今日: {{ stats.todayOrders }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 图表和列表区域 -->
|
||||||
|
<el-row :gutter="24" class="content-row">
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card class="chart-card" shadow="hover">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>用户增长趋势</h3>
|
||||||
|
<el-button type="text" size="small">30天</el-button>
|
||||||
|
</div>
|
||||||
|
<div id="chart-users" style="height: 280px"></div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card class="chart-card" shadow="hover">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>收入趋势</h3>
|
||||||
|
<el-button type="text" size="small">7天</el-button>
|
||||||
|
</div>
|
||||||
|
<div id="chart-income" style="height: 280px"></div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 底部列表区域 -->
|
||||||
|
<el-row :gutter="24" class="content-row">
|
||||||
|
<!-- 最近订单 -->
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<div class="e-chart">
|
<el-card class="list-card" shadow="hover">
|
||||||
<div id="chart-income" style="height: 400px"></div>
|
<div class="card-header">
|
||||||
<div class="title">最近7日收入</div>
|
<h3>最近订单</h3>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="order-list">
|
||||||
|
<div class="order-item" v-for="order in recentOrders" :key="order.id">
|
||||||
|
<div class="order-info">
|
||||||
|
<div class="order-id">#{{ order.id }}</div>
|
||||||
|
<div class="order-amount">{{ order.amount }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="order-meta">
|
||||||
|
<div class="order-date">{{ order.date }}</div>
|
||||||
|
<el-tag :type="order.status === '已支付' ? 'success' : 'warning'" size="small">
|
||||||
|
{{ order.status }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 最近用户 -->
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card class="list-card" shadow="hover">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>最近用户</h3>
|
||||||
|
</div>
|
||||||
|
<div class="user-list">
|
||||||
|
<div class="user-item" v-for="user in recentUsers" :key="user.id">
|
||||||
|
<div class="user-avatar">
|
||||||
|
<el-avatar :size="40" :src="user.avatar">{{ user.name.charAt(0) }}</el-avatar>
|
||||||
|
</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="user-name">{{ user.name }}</div>
|
||||||
|
<div class="user-id">{{ user.userId }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-meta">
|
||||||
|
<div class="user-time">{{ user.time }}</div>
|
||||||
|
<el-tag type="info" size="small">{{ user.status }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 任务统计 -->
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card class="list-card" shadow="hover">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>AI任务统计</h3>
|
||||||
|
</div>
|
||||||
|
<div class="job-stats">
|
||||||
|
<div class="job-stat-item">
|
||||||
|
<div class="stat-icon image-stat">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">图片生成</div>
|
||||||
|
<div class="stat-number">{{ stats.imageJobs }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="job-stat-item">
|
||||||
|
<div class="stat-icon video-stat">
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">视频生成</div>
|
||||||
|
<div class="stat-number">{{ stats.videoJobs }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="job-stat-item">
|
||||||
|
<div class="stat-icon music-stat">
|
||||||
|
<el-icon><Headset /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">音乐生成</div>
|
||||||
|
<div class="stat-number">{{ stats.musicJobs }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="job-stat-item">
|
||||||
|
<div class="stat-icon order-stat">
|
||||||
|
<el-icon><ShoppingCart /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">订单总数</div>
|
||||||
|
<div class="stat-number">{{ stats.orders }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, ref} from 'vue';
|
import { httpGet } from '@/utils/http'
|
||||||
import {ChatDotRound, TrendCharts, User} from "@element-plus/icons-vue";
|
import {
|
||||||
import {httpGet} from "@/utils/http";
|
ChatDotRound,
|
||||||
import {ElMessage} from "element-plus";
|
Headset,
|
||||||
import * as echarts from 'echarts';
|
Money,
|
||||||
|
Picture,
|
||||||
|
ShoppingCart,
|
||||||
|
TrendCharts,
|
||||||
|
User,
|
||||||
|
VideoPlay,
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const stats = ref({users: 0, chats: 0, tokens: 0, rewards: 0})
|
const stats = ref({
|
||||||
|
users: 0,
|
||||||
|
chats: 0,
|
||||||
|
tokens: 0,
|
||||||
|
income: 0,
|
||||||
|
orders: 0,
|
||||||
|
activeUsers: 0,
|
||||||
|
powerConsumption: 0,
|
||||||
|
imageJobs: 0,
|
||||||
|
videoJobs: 0,
|
||||||
|
musicJobs: 0,
|
||||||
|
todayUsers: 0,
|
||||||
|
todayOrders: 0,
|
||||||
|
todayIncome: 0,
|
||||||
|
todayImageJobs: 0,
|
||||||
|
todayVideoJobs: 0,
|
||||||
|
todayMusicJobs: 0,
|
||||||
|
})
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|
||||||
|
// 数据列表
|
||||||
|
const recentOrders = ref([])
|
||||||
|
const recentUsers = ref([])
|
||||||
|
const popularJobs = ref([]) // 改为热门任务
|
||||||
|
|
||||||
|
const formatNumber = (num) => {
|
||||||
|
if (num >= 10000) {
|
||||||
|
return (num / 10000).toFixed(1) + 'w'
|
||||||
|
}
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const chartUsers = echarts.init(document.getElementById("chart-users"))
|
const chartUsers = echarts.init(document.getElementById('chart-users'))
|
||||||
const chartTokens = echarts.init(document.getElementById("chart-tokens"))
|
const chartTokens = echarts.init(document.getElementById('chart-tokens'))
|
||||||
const chartIncome = echarts.init(document.getElementById("chart-income"))
|
const chartIncome = echarts.init(document.getElementById('chart-income'))
|
||||||
httpGet('/api/admin/dashboard/stats').then((res) => {
|
httpGet('/api/admin/dashboard/stats')
|
||||||
stats.value.users = res.data.users
|
.then((res) => {
|
||||||
stats.value.chats = res.data.chats
|
// 更新统计数据
|
||||||
stats.value.tokens = res.data.tokens
|
Object.assign(stats.value, res.data)
|
||||||
stats.value.income = res.data.income
|
const chartData = res.data.chart
|
||||||
const chartData = res.data.chart
|
loading.value = false
|
||||||
loading.value = false
|
|
||||||
|
|
||||||
const x = []
|
const x = []
|
||||||
const dataUsers = []
|
const dataUsers = []
|
||||||
for (let k in chartData.users) {
|
for (let k in chartData.users) {
|
||||||
x.push(k)
|
x.push(k)
|
||||||
dataUsers.push(chartData.users[k])
|
dataUsers.push(chartData.users[k])
|
||||||
}
|
}
|
||||||
chartUsers.setOption({
|
chartUsers.setOption({
|
||||||
xAxis: {
|
tooltip: {
|
||||||
data: x
|
trigger: 'axis',
|
||||||
},
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||||
yAxis: {},
|
borderColor: '#ddd',
|
||||||
series: [
|
textStyle: { color: '#666' },
|
||||||
{
|
},
|
||||||
data: dataUsers,
|
grid: {
|
||||||
type: 'line',
|
left: '3%',
|
||||||
label: {
|
right: '4%',
|
||||||
show: true,
|
bottom: '3%',
|
||||||
position: 'bottom',
|
containLabel: true,
|
||||||
textStyle: {
|
},
|
||||||
fontSize: 18
|
xAxis: {
|
||||||
}
|
type: 'category',
|
||||||
}
|
data: x,
|
||||||
}
|
axisLine: { lineStyle: { color: '#ddd' } },
|
||||||
]
|
axisTick: { show: false },
|
||||||
|
axisLabel: { color: '#999' },
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLine: { show: false },
|
||||||
|
axisTick: { show: false },
|
||||||
|
axisLabel: { color: '#999' },
|
||||||
|
splitLine: { lineStyle: { color: '#f0f0f0' } },
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: dataUsers,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 6,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#8B5CF6',
|
||||||
|
width: 3,
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#8B5CF6',
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: 'rgba(139, 92, 246, 0.3)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: 'rgba(139, 92, 246, 0.05)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const dataTokens = []
|
||||||
|
for (let k in chartData.historyMessage) {
|
||||||
|
dataTokens.push(chartData.historyMessage[k])
|
||||||
|
}
|
||||||
|
chartTokens.setOption({
|
||||||
|
xAxis: {
|
||||||
|
data: x,
|
||||||
|
},
|
||||||
|
yAxis: {},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: dataTokens,
|
||||||
|
type: 'line',
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'bottom',
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 18,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataIncome = []
|
||||||
|
for (let k in chartData.orders) {
|
||||||
|
dataIncome.push(chartData.orders[k])
|
||||||
|
}
|
||||||
|
chartIncome.setOption({
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
borderColor: '#ddd',
|
||||||
|
textStyle: { color: '#666' },
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: x,
|
||||||
|
axisLine: { lineStyle: { color: '#ddd' } },
|
||||||
|
axisTick: { show: false },
|
||||||
|
axisLabel: { color: '#999' },
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLine: { show: false },
|
||||||
|
axisTick: { show: false },
|
||||||
|
axisLabel: { color: '#999' },
|
||||||
|
splitLine: { lineStyle: { color: '#f0f0f0' } },
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: dataIncome,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 6,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#10B981',
|
||||||
|
width: 3,
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#10B981',
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: 'rgba(16, 185, 129, 0.3)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: 'rgba(16, 185, 129, 0.05)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
})
|
})
|
||||||
const dataTokens = []
|
.catch((e) => {
|
||||||
for (let k in chartData.historyMessage) {
|
ElMessage.error('获取统计数据失败:' + e.message)
|
||||||
dataTokens.push(chartData.historyMessage[k])
|
|
||||||
}
|
|
||||||
chartTokens.setOption({
|
|
||||||
xAxis: {
|
|
||||||
data: x
|
|
||||||
},
|
|
||||||
yAxis: {},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
data: dataTokens,
|
|
||||||
type: 'line',
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
position: 'bottom',
|
|
||||||
textStyle: {
|
|
||||||
fontSize: 18
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const dataIncome = []
|
window.onresize = function () {
|
||||||
for (let k in chartData.orders) {
|
// 自适应大小
|
||||||
dataIncome.push(chartData.orders[k])
|
|
||||||
}
|
|
||||||
chartIncome.setOption({
|
|
||||||
xAxis: {
|
|
||||||
data: x
|
|
||||||
},
|
|
||||||
yAxis: {},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
data: dataIncome,
|
|
||||||
type: 'line',
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
position: 'bottom',
|
|
||||||
textStyle: {
|
|
||||||
fontSize: 18
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
}).catch((e) => {
|
|
||||||
ElMessage.error("获取统计数据失败:" + e.message)
|
|
||||||
})
|
|
||||||
|
|
||||||
window.onresize = function () { // 自适应大小
|
|
||||||
chartUsers.resize()
|
chartUsers.resize()
|
||||||
chartTokens.resize()
|
|
||||||
chartIncome.resize()
|
chartIncome.resize()
|
||||||
};
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="stylus">
|
<style scoped lang="stylus">
|
||||||
.dashboard {
|
.dashboard {
|
||||||
padding 20px
|
padding: 24px;
|
||||||
|
background: #f8fafc;
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
.grid-content {
|
.stats-row {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-row {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-cont-right {
|
.card-icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 28px;
|
||||||
|
color: white;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.user-icon {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.chat-icon {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.token-icon {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.income-icon {
|
||||||
|
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.order-icon {
|
||||||
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.image-icon {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.video-icon {
|
||||||
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.music-icon {
|
||||||
|
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: center;
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-number {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-label {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #999;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-num {
|
.chart-card, .list-card {
|
||||||
font-size: 30px;
|
border: none;
|
||||||
font-weight: bold;
|
border-radius: 12px;
|
||||||
}
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
.grid-con-icon {
|
:deep(.el-card__body) {
|
||||||
font-size: 50px;
|
padding: 24px;
|
||||||
width: 100px;
|
height: 100%;
|
||||||
height: 100px;
|
display: flex;
|
||||||
text-align: center;
|
flex-direction: column;
|
||||||
line-height: 100px;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
.iconfont {
|
|
||||||
font-size: 50px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.e-chart {
|
.card-header {
|
||||||
.title {
|
display: flex;
|
||||||
text-align center
|
justify-content: space-between;
|
||||||
font-size 16px
|
align-items: center;
|
||||||
color #444444
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-con-1 .grid-con-icon {
|
.order-list, .user-list, .app-list {
|
||||||
background: rgb(45, 140, 240);
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-con-1 .grid-num {
|
.order-item {
|
||||||
color: rgb(45, 140, 240);
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.order-id {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-amount {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.order-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-con-2 .grid-con-icon {
|
.user-item {
|
||||||
background: rgb(100, 213, 114);
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-meta {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
.user-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-con-2 .grid-num {
|
.job-stats {
|
||||||
color: rgb(100, 213, 114);
|
display: grid;
|
||||||
}
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
.grid-con-3 .grid-con-icon {
|
.job-stat-item {
|
||||||
background: rgb(242, 94, 67);
|
display: flex;
|
||||||
}
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
.grid-con-3 .grid-num {
|
&:hover {
|
||||||
color: rgb(242, 94, 67);
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
color: white;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.image-stat {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.video-stat {
|
||||||
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.music-stat {
|
||||||
|
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.order-stat {
|
||||||
|
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user