Dashboard page is ready

This commit is contained in:
GeekMaster
2025-08-01 10:45:49 +08:00
parent 8168377e47
commit 068b5ddeef
2 changed files with 79 additions and 189 deletions

View File

@@ -55,18 +55,18 @@ type statsVo struct {
Tokens int `json:"tokens"` Tokens int `json:"tokens"`
Income float64 `json:"income"` Income float64 `json:"income"`
Chart map[string]map[string]float64 `json:"chart"` Chart map[string]map[string]float64 `json:"chart"`
TodayUsers int64 `json:"today_users"` TodayUsers int64 `json:"todayUsers"`
TodayChats int64 `json:"today_chats"` TodayChats int64 `json:"todayChats"`
TodayTokens int `json:"today_tokens"` TodayTokens int `json:"todayTokens"`
TodayIncome float64 `json:"today_income"` TodayIncome float64 `json:"todayIncome"`
TodayOrders int64 `json:"today_orders"` TodayOrders int64 `json:"todayOrders"`
TodayImageJobs int64 `json:"today_image_jobs"` TodayImageJobs int64 `json:"todayImageJobs"`
TodayVideoJobs int64 `json:"today_video_jobs"` TodayVideoJobs int64 `json:"todayVideoJobs"`
TodayMusicJobs int64 `json:"today_music_jobs"` TodayMusicJobs int64 `json:"todayMusicJobs"`
Orders int64 `json:"orders"` Orders int64 `json:"orders"`
ImageJobs int64 `json:"image_jobs"` ImageJobs int64 `json:"imageJobs"`
VideoJobs int64 `json:"video_jobs"` VideoJobs int64 `json:"videoJobs"`
MusicJobs int64 `json:"music_jobs"` MusicJobs int64 `json:"musicJobs"`
RecentOrders []OrderBrief `json:"recentOrders"` RecentOrders []OrderBrief `json:"recentOrders"`
RecentUsers []UserBrief `json:"recentUsers"` 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) h.DB.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&stats.TodayChats)
// 总Token消耗 // 总算力消耗
var historyMessages []model.ChatMessage var powerLogs []model.PowerLog
h.DB.Find(&historyMessages) h.DB.Where("mark = ?", types.PowerSub).Find(&powerLogs)
for _, item := range historyMessages { for _, item := range powerLogs {
stats.Tokens += item.Tokens stats.Tokens += item.Amount
} }
// 今日Token消耗 // 今日算力消耗
var todayMessages []model.ChatMessage var todayPowerLogs []model.PowerLog
h.DB.Where("created_at > ?", zeroTime).Find(&todayMessages) h.DB.Where("mark = ?", types.PowerSub).Where("created_at > ?", zeroTime).Find(&todayPowerLogs)
for _, item := range todayMessages { for _, item := range todayPowerLogs {
stats.TodayTokens += item.Tokens 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) 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 stats.ImageJobs = mjJobs + sdJobs + dallJobs + jimengImageJobs
logger.Info("stats.ImageJobs", stats.ImageJobs)
// 今日图片生成任务统计 // 今日图片生成任务统计
var todayMjJobs, todaySdJobs, todayDallJobs, todayJimengImageJobs int64 var todayMjJobs, todaySdJobs, todayDallJobs, todayJimengImageJobs int64
h.DB.Model(&model.MidJourneyJob{}).Where("created_at > ?", zeroTime).Count(&todayMjJobs) h.DB.Model(&model.MidJourneyJob{}).Where("created_at > ?", zeroTime).Count(&todayMjJobs)
@@ -202,11 +204,12 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
} }
} }
// 统计7天Token 消耗 // 统计7天算力消耗
err = h.DB.Where("created_at > ?", startDate).Find(&historyMessages).Error var chartPowerLogs []model.PowerLog
err = h.DB.Where("mark = ?", types.PowerSub).Where("created_at > ?", startDate).Find(&chartPowerLogs).Error
if err == nil { if err == nil {
for _, item := range historyMessages { for _, item := range chartPowerLogs {
historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Tokens) historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Amount)
} }
} }

View File

@@ -11,7 +11,7 @@
<div class="card-info"> <div class="card-info">
<div class="card-number">{{ stats.users }}</div> <div class="card-number">{{ stats.users }}</div>
<div class="card-label">用户总数</div> <div class="card-label">用户总数</div>
<div class="card-desc">今日新增: {{ stats.todayUsers || 1 }}</div> <div class="card-desc">今日新增: {{ stats.todayUsers || 0 }}</div>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -39,9 +39,9 @@
<el-icon><TrendCharts /></el-icon> <el-icon><TrendCharts /></el-icon>
</div> </div>
<div class="card-info"> <div class="card-info">
<div class="card-number">{{ formatNumber(stats.tokens) }}</div> <div class="card-number">{{ formatNumber(stats.power || stats.tokens) }}</div>
<div class="card-label">Token消耗</div> <div class="card-label">算力消耗</div>
<div class="card-desc">今日: {{ formatNumber(stats.todayTokens) }}</div> <div class="card-desc">今日: {{ formatNumber(stats.todayPower || stats.todayTokens) }}</div>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -153,7 +153,7 @@
<!-- 底部列表区域 --> <!-- 底部列表区域 -->
<el-row :gutter="24" class="content-row"> <el-row :gutter="24" class="content-row">
<!-- 最近订单 --> <!-- 最近订单 -->
<el-col :span="8"> <el-col :span="12">
<el-card class="list-card" shadow="hover"> <el-card class="list-card" shadow="hover">
<div class="card-header"> <div class="card-header">
<h3>最近订单</h3> <h3>最近订单</h3>
@@ -173,7 +173,7 @@
</el-col> </el-col>
<!-- 最近用户 --> <!-- 最近用户 -->
<el-col :span="8"> <el-col :span="12">
<el-card class="list-card" shadow="hover"> <el-card class="list-card" shadow="hover">
<div class="card-header"> <div class="card-header">
<h3>最近用户</h3> <h3>最近用户</h3>
@@ -193,53 +193,6 @@
</div> </div>
</el-card> </el-card>
</el-col> </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> </el-row>
</div> </div>
</template> </template>
@@ -264,6 +217,7 @@ const stats = ref({
users: 0, users: 0,
chats: 0, chats: 0,
tokens: 0, tokens: 0,
power: 0,
income: 0, income: 0,
orders: 0, orders: 0,
activeUsers: 0, activeUsers: 0,
@@ -274,6 +228,8 @@ const stats = ref({
todayUsers: 0, todayUsers: 0,
todayOrders: 0, todayOrders: 0,
todayIncome: 0, todayIncome: 0,
todayTokens: 0,
todayPower: 0,
todayImageJobs: 0, todayImageJobs: 0,
todayVideoJobs: 0, todayVideoJobs: 0,
todayMusicJobs: 0, todayMusicJobs: 0,
@@ -307,18 +263,31 @@ const formatTime = (dateStr) => {
} }
onMounted(() => { onMounted(() => {
const chartUsers = echarts.init(document.getElementById('chart-users')) const chartUsersEl = document.getElementById('chart-users')
const chartTokens = echarts.init(document.getElementById('chart-tokens')) const chartIncomeEl = document.getElementById('chart-income')
const chartIncome = echarts.init(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') httpGet('/api/admin/dashboard/stats')
.then((res) => { .then((res) => {
// 更新统计数据 // 更新统计数据
Object.assign(stats.value, res.data) Object.assign(stats.value, res.data)
recentOrders.value = res.data.recentOrders || [] recentOrders.value = res.data.recentOrders || []
recentUsers.value = res.data.recentUsers || [] recentUsers.value = res.data.recentUsers || []
const chartData = res.data.chart const chartData = res.data.chart || {}
loading.value = false loading.value = false
// 检查图表数据是否存在
if (!chartData.users || !chartData.orders) {
ElMessage.warning('图表数据不完整')
return
}
const x = [] const x = []
const dataUsers = [] const dataUsers = []
for (let k in chartData.users) { 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 = [] const dataIncome = []
for (let k in chartData.orders) { for (let k in chartData.orders) {
@@ -486,16 +432,16 @@ onMounted(() => {
window.onresize = function () { window.onresize = function () {
// 自适应大小 // 自适应大小
chartUsers.resize() if (chartUsers) chartUsers.resize()
chartIncome.resize() if (chartIncome) chartIncome.resize()
} }
}) })
</script> </script>
<style scoped lang="stylus"> <style scoped lang="scss">
.dashboard { .dashboard {
padding: 24px; padding: 24px;
background: #f8fafc; background: var(--theme-bg-color);
min-height: 100vh; min-height: 100vh;
.stats-row { .stats-row {
@@ -511,10 +457,11 @@ onMounted(() => {
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
transition: all 0.3s ease; transition: all 0.3s ease;
background: var(--card-bg);
&:hover { &:hover {
transform: translateY(-2px); 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) { :deep(.el-card__body) {
@@ -580,28 +527,30 @@ onMounted(() => {
.card-number { .card-number {
font-size: 32px; font-size: 32px;
font-weight: 700; font-weight: 700;
color: #1f2937; color: var(--theme-text-color-primary);
margin-bottom: 4px; margin-bottom: 4px;
line-height: 1; line-height: 1;
} }
.card-label { .card-label {
font-size: 16px; font-size: 16px;
color: #6b7280; color: var(--el-text-color-regular);
margin-bottom: 4px; margin-bottom: 4px;
font-weight: 500; font-weight: 500;
} }
.card-desc { .card-desc {
font-size: 14px; font-size: 14px;
color: #9ca3af; color: var(--theme-text-color-secondary);
} }
.chart-card, .list-card { .chart-card,
.list-card {
border: none; border: none;
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
background: var(--card-bg);
:deep(.el-card__body) { :deep(.el-card__body) {
padding: 24px; padding: 24px;
@@ -617,17 +566,19 @@ onMounted(() => {
align-items: center; align-items: center;
margin-bottom: 20px; margin-bottom: 20px;
padding-bottom: 16px; padding-bottom: 16px;
border-bottom: 1px solid #f3f4f6; border-bottom: 1px solid var(--el-border-color);
h3 { h3 {
margin: 0; margin: 0;
font-size: 18px; font-size: 18px;
font-weight: 600; 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; flex: 1;
overflow-y: auto; overflow-y: auto;
} }
@@ -637,7 +588,7 @@ onMounted(() => {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 12px 0; padding: 12px 0;
border-bottom: 1px solid #f3f4f6; border-bottom: 1px solid var(--el-border-color);
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
@@ -650,13 +601,13 @@ onMounted(() => {
.order-id { .order-id {
font-size: 14px; font-size: 14px;
color: #6b7280; color: var(--theme-text-color-secondary);
} }
.order-amount { .order-amount {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: #10b981; color: var(--el-color-success, #10b981);
} }
} }
@@ -667,7 +618,7 @@ onMounted(() => {
.order-date { .order-date {
font-size: 12px; font-size: 12px;
color: #9ca3af; color: var(--theme-text-color-secondary);
} }
} }
} }
@@ -677,7 +628,7 @@ onMounted(() => {
align-items: center; align-items: center;
gap: 12px; gap: 12px;
padding: 12px 0; padding: 12px 0;
border-bottom: 1px solid #f3f4f6; border-bottom: 1px solid var(--el-border-color);
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
@@ -690,7 +641,7 @@ onMounted(() => {
.user-name { .user-name {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: #1f2937; color: var(--theme-text-color-primary);
margin-bottom: 2px; margin-bottom: 2px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -699,7 +650,7 @@ onMounted(() => {
.user-id { .user-id {
font-size: 12px; font-size: 12px;
color: #9ca3af; color: var(--theme-text-color-secondary);
} }
} }
@@ -708,74 +659,10 @@ onMounted(() => {
.user-time { .user-time {
font-size: 12px; font-size: 12px;
color: #6b7280; color: var(--theme-text-color-secondary);
margin-bottom: 4px; 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> </style>