mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-11 05:34:25 +08:00
增加 dashboard 页面
This commit is contained in:
@@ -43,36 +43,89 @@ type statsVo struct {
|
||||
|
||||
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
|
||||
}
|
||||
// 总用户数
|
||||
h.DB.Model(&model.User{}).Count(&stats.Users)
|
||||
|
||||
// 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
|
||||
res = h.DB.Where("created_at > ?", zeroTime).Find(&historyMessages)
|
||||
h.DB.Find(&historyMessages)
|
||||
for _, item := range historyMessages {
|
||||
stats.Tokens += item.Tokens
|
||||
}
|
||||
|
||||
// 订单收入
|
||||
var orders []model.Order
|
||||
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&orders)
|
||||
for _, item := range orders {
|
||||
// 今日Token消耗
|
||||
var todayMessages []model.ChatMessage
|
||||
h.DB.Where("created_at > ?", zeroTime).Find(&todayMessages)
|
||||
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
|
||||
}
|
||||
|
||||
// 今日收入
|
||||
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天的订单的图表
|
||||
startDate := now.Add(-7 * 24 * time.Hour).Format("2006-01-02")
|
||||
var statsChart = make(map[string]map[string]float64)
|
||||
@@ -101,6 +154,7 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 统计最近7天的订单
|
||||
var orders []model.Order
|
||||
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&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()
|
||||
|
||||
@@ -1,260 +1,770 @@
|
||||
<template>
|
||||
<div class="dashboard" v-loading="loading">
|
||||
<el-row class="mgb20" :gutter="20">
|
||||
<!-- 统计卡片区域 -->
|
||||
<el-row class="stats-row" :gutter="24">
|
||||
<el-col :span="6">
|
||||
<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>
|
||||
<el-card class="stats-card" shadow="hover">
|
||||
<div class="card-content">
|
||||
<div class="card-icon user-icon">
|
||||
<el-icon><User /></el-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-number">{{ stats.users }}</div>
|
||||
<div class="card-label">用户总数</div>
|
||||
<div class="card-desc">今日新增: {{ stats.todayUsers || 1 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<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>
|
||||
<el-card class="stats-card" shadow="hover">
|
||||
<div class="card-content">
|
||||
<div class="card-icon chat-icon">
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-number">{{ stats.chats }}</div>
|
||||
<div class="card-label">对话总数</div>
|
||||
<div class="card-desc">今日: {{ stats.todayChats }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<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>
|
||||
<el-card class="stats-card" shadow="hover">
|
||||
<div class="card-content">
|
||||
<div class="card-icon token-icon">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-number">{{ formatNumber(stats.tokens) }}</div>
|
||||
<div class="card-label">Token消耗</div>
|
||||
<div class="card-desc">今日: {{ formatNumber(stats.todayTokens) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" :body-style="{ padding: '0px' }">
|
||||
<div class="grid-content grid-con-3">
|
||||
<el-icon class="grid-con-icon">
|
||||
<i class="iconfont icon-reward"></i>
|
||||
</el-icon>
|
||||
<div class="grid-cont-right">
|
||||
<div class="grid-num">¥{{ stats.income }}</div>
|
||||
<div>今日入账</div>
|
||||
<el-card class="stats-card" shadow="hover">
|
||||
<div class="card-content">
|
||||
<div class="card-icon income-icon">
|
||||
<el-icon><Money /></el-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-number">¥{{ stats.income.toFixed(2) }}</div>
|
||||
<div class="card-label">总收入</div>
|
||||
<div class="card-desc">今日: ¥{{ stats.todayIncome?.toFixed(2) || '0.00' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row class="mgb20" :gutter="20">
|
||||
<el-col :span="8">
|
||||
<div class="e-chart">
|
||||
<div id="chart-users" style="height: 400px"></div>
|
||||
<div class="title">最近7日用户注册</div>
|
||||
</div>
|
||||
<!-- 第二行统计卡片 -->
|
||||
<el-row class="stats-row" :gutter="24">
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card" shadow="hover">
|
||||
<div class="card-content">
|
||||
<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 :span="8">
|
||||
<div class="e-chart">
|
||||
<div id="chart-tokens" style="height: 400px"></div>
|
||||
<div class="title">最近7日Token消耗</div>
|
||||
</div>
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card" shadow="hover">
|
||||
<div class="card-content">
|
||||
<div class="card-icon video-icon">
|
||||
<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 :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">
|
||||
<div class="e-chart">
|
||||
<div id="chart-income" style="height: 400px"></div>
|
||||
<div class="title">最近7日收入</div>
|
||||
</div>
|
||||
<el-card class="list-card" shadow="hover">
|
||||
<div class="card-header">
|
||||
<h3>最近订单</h3>
|
||||
</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-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from 'vue';
|
||||
import {ChatDotRound, TrendCharts, User} from "@element-plus/icons-vue";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import * as echarts from 'echarts';
|
||||
import { httpGet } from '@/utils/http'
|
||||
import {
|
||||
ChatDotRound,
|
||||
Headset,
|
||||
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 recentOrders = ref([])
|
||||
const recentUsers = ref([])
|
||||
const popularJobs = ref([]) // 改为热门任务
|
||||
|
||||
const formatNumber = (num) => {
|
||||
if (num >= 10000) {
|
||||
return (num / 10000).toFixed(1) + 'w'
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const chartUsers = echarts.init(document.getElementById("chart-users"))
|
||||
const chartTokens = echarts.init(document.getElementById("chart-tokens"))
|
||||
const chartIncome = echarts.init(document.getElementById("chart-income"))
|
||||
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
|
||||
stats.value.income = res.data.income
|
||||
const chartData = res.data.chart
|
||||
loading.value = false
|
||||
const chartUsers = echarts.init(document.getElementById('chart-users'))
|
||||
const chartTokens = echarts.init(document.getElementById('chart-tokens'))
|
||||
const chartIncome = echarts.init(document.getElementById('chart-income'))
|
||||
httpGet('/api/admin/dashboard/stats')
|
||||
.then((res) => {
|
||||
// 更新统计数据
|
||||
Object.assign(stats.value, res.data)
|
||||
const chartData = res.data.chart
|
||||
loading.value = false
|
||||
|
||||
const x = []
|
||||
const dataUsers = []
|
||||
for (let k in chartData.users) {
|
||||
x.push(k)
|
||||
dataUsers.push(chartData.users[k])
|
||||
}
|
||||
chartUsers.setOption({
|
||||
xAxis: {
|
||||
data: x
|
||||
},
|
||||
yAxis: {},
|
||||
series: [
|
||||
{
|
||||
data: dataUsers,
|
||||
type: 'line',
|
||||
label: {
|
||||
show: true,
|
||||
position: 'bottom',
|
||||
textStyle: {
|
||||
fontSize: 18
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
const x = []
|
||||
const dataUsers = []
|
||||
for (let k in chartData.users) {
|
||||
x.push(k)
|
||||
dataUsers.push(chartData.users[k])
|
||||
}
|
||||
chartUsers.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: 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 = []
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
.catch((e) => {
|
||||
ElMessage.error('获取统计数据失败:' + e.message)
|
||||
})
|
||||
|
||||
const dataIncome = []
|
||||
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 () { // 自适应大小
|
||||
window.onresize = function () {
|
||||
// 自适应大小
|
||||
chartUsers.resize()
|
||||
chartTokens.resize()
|
||||
chartIncome.resize()
|
||||
};
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
.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;
|
||||
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;
|
||||
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;
|
||||
color: #999;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.grid-num {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.chart-card, .list-card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
.grid-con-icon {
|
||||
font-size: 50px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
text-align: center;
|
||||
line-height: 100px;
|
||||
color: #fff;
|
||||
|
||||
.iconfont {
|
||||
font-size: 50px;
|
||||
:deep(.el-card__body) {
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.e-chart {
|
||||
.title {
|
||||
text-align center
|
||||
font-size 16px
|
||||
color #444444
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
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 {
|
||||
background: rgb(45, 140, 240);
|
||||
.order-list, .user-list, .app-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.grid-con-1 .grid-num {
|
||||
color: rgb(45, 140, 240);
|
||||
.order-item {
|
||||
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 {
|
||||
background: rgb(100, 213, 114);
|
||||
.user-item {
|
||||
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 {
|
||||
color: rgb(100, 213, 114);
|
||||
}
|
||||
.job-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
|
||||
.grid-con-3 .grid-con-icon {
|
||||
background: rgb(242, 94, 67);
|
||||
}
|
||||
.job-stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.grid-con-3 .grid-num {
|
||||
color: rgb(242, 94, 67);
|
||||
&:hover {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user