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"`
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)
}
}

View File

@@ -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>