增加 dashboard 页面

This commit is contained in:
GeekMaster
2025-07-31 16:59:59 +08:00
parent 0ee230cf41
commit 4d048dbfa7
3 changed files with 765 additions and 201 deletions

View File

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

BIN
image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

View File

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