mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-21 02:24:32 +08:00
Dashboard page is ready
This commit is contained in:
@@ -55,18 +55,18 @@ type statsVo struct {
|
||||
Tokens int `json:"tokens"`
|
||||
Income float64 `json:"income"`
|
||||
Chart map[string]map[string]float64 `json:"chart"`
|
||||
TodayUsers int64 `json:"today_users"`
|
||||
TodayChats int64 `json:"today_chats"`
|
||||
TodayTokens int `json:"today_tokens"`
|
||||
TodayIncome float64 `json:"today_income"`
|
||||
TodayOrders int64 `json:"today_orders"`
|
||||
TodayImageJobs int64 `json:"today_image_jobs"`
|
||||
TodayVideoJobs int64 `json:"today_video_jobs"`
|
||||
TodayMusicJobs int64 `json:"today_music_jobs"`
|
||||
TodayUsers int64 `json:"todayUsers"`
|
||||
TodayChats int64 `json:"todayChats"`
|
||||
TodayTokens int `json:"todayTokens"`
|
||||
TodayIncome float64 `json:"todayIncome"`
|
||||
TodayOrders int64 `json:"todayOrders"`
|
||||
TodayImageJobs int64 `json:"todayImageJobs"`
|
||||
TodayVideoJobs int64 `json:"todayVideoJobs"`
|
||||
TodayMusicJobs int64 `json:"todayMusicJobs"`
|
||||
Orders int64 `json:"orders"`
|
||||
ImageJobs int64 `json:"image_jobs"`
|
||||
VideoJobs int64 `json:"video_jobs"`
|
||||
MusicJobs int64 `json:"music_jobs"`
|
||||
ImageJobs int64 `json:"imageJobs"`
|
||||
VideoJobs int64 `json:"videoJobs"`
|
||||
MusicJobs int64 `json:"musicJobs"`
|
||||
RecentOrders []OrderBrief `json:"recentOrders"`
|
||||
RecentUsers []UserBrief `json:"recentUsers"`
|
||||
}
|
||||
@@ -88,18 +88,18 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
|
||||
// 今日新增对话
|
||||
h.DB.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&stats.TodayChats)
|
||||
|
||||
// 总Token消耗
|
||||
var historyMessages []model.ChatMessage
|
||||
h.DB.Find(&historyMessages)
|
||||
for _, item := range historyMessages {
|
||||
stats.Tokens += item.Tokens
|
||||
// 总算力消耗
|
||||
var powerLogs []model.PowerLog
|
||||
h.DB.Where("mark = ?", types.PowerSub).Find(&powerLogs)
|
||||
for _, item := range powerLogs {
|
||||
stats.Tokens += item.Amount
|
||||
}
|
||||
|
||||
// 今日Token消耗
|
||||
var todayMessages []model.ChatMessage
|
||||
h.DB.Where("created_at > ?", zeroTime).Find(&todayMessages)
|
||||
for _, item := range todayMessages {
|
||||
stats.TodayTokens += item.Tokens
|
||||
// 今日算力消耗
|
||||
var todayPowerLogs []model.PowerLog
|
||||
h.DB.Where("mark = ?", types.PowerSub).Where("created_at > ?", zeroTime).Find(&todayPowerLogs)
|
||||
for _, item := range todayPowerLogs {
|
||||
stats.TodayTokens += item.Amount
|
||||
}
|
||||
|
||||
// 总收入
|
||||
@@ -130,6 +130,8 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
|
||||
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
|
||||
|
||||
logger.Info("stats.ImageJobs", stats.ImageJobs)
|
||||
|
||||
// 今日图片生成任务统计
|
||||
var todayMjJobs, todaySdJobs, todayDallJobs, todayJimengImageJobs int64
|
||||
h.DB.Model(&model.MidJourneyJob{}).Where("created_at > ?", zeroTime).Count(&todayMjJobs)
|
||||
@@ -202,11 +204,12 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 统计7天Token 消耗
|
||||
err = h.DB.Where("created_at > ?", startDate).Find(&historyMessages).Error
|
||||
// 统计7天算力消耗
|
||||
var chartPowerLogs []model.PowerLog
|
||||
err = h.DB.Where("mark = ?", types.PowerSub).Where("created_at > ?", startDate).Find(&chartPowerLogs).Error
|
||||
if err == nil {
|
||||
for _, item := range historyMessages {
|
||||
historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Tokens)
|
||||
for _, item := range chartPowerLogs {
|
||||
historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Amount)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<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 class="card-desc">今日新增: {{ stats.todayUsers || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -39,9 +39,9 @@
|
||||
<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 class="card-number">{{ formatNumber(stats.power || stats.tokens) }}</div>
|
||||
<div class="card-label">算力消耗</div>
|
||||
<div class="card-desc">今日: {{ formatNumber(stats.todayPower || stats.todayTokens) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -153,7 +153,7 @@
|
||||
<!-- 底部列表区域 -->
|
||||
<el-row :gutter="24" class="content-row">
|
||||
<!-- 最近订单 -->
|
||||
<el-col :span="8">
|
||||
<el-col :span="12">
|
||||
<el-card class="list-card" shadow="hover">
|
||||
<div class="card-header">
|
||||
<h3>最近订单</h3>
|
||||
@@ -173,7 +173,7 @@
|
||||
</el-col>
|
||||
|
||||
<!-- 最近用户 -->
|
||||
<el-col :span="8">
|
||||
<el-col :span="12">
|
||||
<el-card class="list-card" shadow="hover">
|
||||
<div class="card-header">
|
||||
<h3>最近用户</h3>
|
||||
@@ -193,53 +193,6 @@
|
||||
</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>
|
||||
@@ -264,6 +217,7 @@ const stats = ref({
|
||||
users: 0,
|
||||
chats: 0,
|
||||
tokens: 0,
|
||||
power: 0,
|
||||
income: 0,
|
||||
orders: 0,
|
||||
activeUsers: 0,
|
||||
@@ -274,6 +228,8 @@ const stats = ref({
|
||||
todayUsers: 0,
|
||||
todayOrders: 0,
|
||||
todayIncome: 0,
|
||||
todayTokens: 0,
|
||||
todayPower: 0,
|
||||
todayImageJobs: 0,
|
||||
todayVideoJobs: 0,
|
||||
todayMusicJobs: 0,
|
||||
@@ -307,18 +263,31 @@ const formatTime = (dateStr) => {
|
||||
}
|
||||
|
||||
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'))
|
||||
const chartUsersEl = document.getElementById('chart-users')
|
||||
const chartIncomeEl = document.getElementById('chart-income')
|
||||
|
||||
if (!chartUsersEl || !chartIncomeEl) {
|
||||
ElMessage.error('图表容器未找到')
|
||||
return
|
||||
}
|
||||
|
||||
const chartUsers = echarts.init(chartUsersEl)
|
||||
const chartIncome = echarts.init(chartIncomeEl)
|
||||
httpGet('/api/admin/dashboard/stats')
|
||||
.then((res) => {
|
||||
// 更新统计数据
|
||||
Object.assign(stats.value, res.data)
|
||||
recentOrders.value = res.data.recentOrders || []
|
||||
recentUsers.value = res.data.recentUsers || []
|
||||
const chartData = res.data.chart
|
||||
const chartData = res.data.chart || {}
|
||||
loading.value = false
|
||||
|
||||
// 检查图表数据是否存在
|
||||
if (!chartData.users || !chartData.orders) {
|
||||
ElMessage.warning('图表数据不完整')
|
||||
return
|
||||
}
|
||||
|
||||
const x = []
|
||||
const dataUsers = []
|
||||
for (let k in chartData.users) {
|
||||
@@ -388,29 +357,6 @@ onMounted(() => {
|
||||
},
|
||||
],
|
||||
})
|
||||
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) {
|
||||
@@ -486,16 +432,16 @@ onMounted(() => {
|
||||
|
||||
window.onresize = function () {
|
||||
// 自适应大小
|
||||
chartUsers.resize()
|
||||
chartIncome.resize()
|
||||
if (chartUsers) chartUsers.resize()
|
||||
if (chartIncome) chartIncome.resize()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
<style scoped lang="scss">
|
||||
.dashboard {
|
||||
padding: 24px;
|
||||
background: #f8fafc;
|
||||
background: var(--theme-bg-color);
|
||||
min-height: 100vh;
|
||||
|
||||
.stats-row {
|
||||
@@ -511,10 +457,11 @@ onMounted(() => {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
background: var(--card-bg);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: var(--el-box-shadow, 0 8px 25px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
@@ -580,28 +527,30 @@ onMounted(() => {
|
||||
.card-number {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
color: var(--theme-text-color-primary);
|
||||
margin-bottom: 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 16px;
|
||||
color: #6b7280;
|
||||
color: var(--el-text-color-regular);
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
color: var(--theme-text-color-secondary);
|
||||
}
|
||||
|
||||
.chart-card, .list-card {
|
||||
.chart-card,
|
||||
.list-card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
background: var(--card-bg);
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 24px;
|
||||
@@ -617,17 +566,19 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
color: var(--theme-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.order-list, .user-list, .app-list {
|
||||
.order-list,
|
||||
.user-list,
|
||||
.app-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -637,7 +588,7 @@ onMounted(() => {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
@@ -650,13 +601,13 @@ onMounted(() => {
|
||||
|
||||
.order-id {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
color: var(--theme-text-color-secondary);
|
||||
}
|
||||
|
||||
.order-amount {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #10b981;
|
||||
color: var(--el-color-success, #10b981);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,7 +618,7 @@ onMounted(() => {
|
||||
|
||||
.order-date {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
color: var(--theme-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -677,7 +628,7 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
@@ -690,7 +641,7 @@ onMounted(() => {
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
color: var(--theme-text-color-primary);
|
||||
margin-bottom: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -699,7 +650,7 @@ onMounted(() => {
|
||||
|
||||
.user-id {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
color: var(--theme-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,74 +659,10 @@ onMounted(() => {
|
||||
|
||||
.user-time {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
color: var(--theme-text-color-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.job-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
|
||||
.job-stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&: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