|
@@ -1,231 +1,296 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div class="dashboard-container">
|
|
<div class="dashboard-container">
|
|
|
- <!-- 顶部统计卡片 -->
|
|
|
|
|
- <el-row :gutter="20" class="stat-row">
|
|
|
|
|
- <el-col :span="6" v-for="(stat, idx) in statCards" :key="idx">
|
|
|
|
|
- <el-card class="stat-card" :class="'stat-card-' + stat.type">
|
|
|
|
|
- <div class="stat-icon">{{ stat.icon }}</div>
|
|
|
|
|
- <div class="stat-content">
|
|
|
|
|
- <div class="stat-value">{{ stat.value }}</div>
|
|
|
|
|
- <div class="stat-label">{{ stat.label }}</div>
|
|
|
|
|
|
|
+ <el-card class="section-card" v-loading="statLoading">
|
|
|
|
|
+ <div slot="header" class="card-header">
|
|
|
|
|
+ <span><i class="el-icon-data-analysis"></i> 数据概览</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-row :gutter="16">
|
|
|
|
|
+ <el-col :xs="12" :sm="8" :md="4" v-for="(card, idx) in statCards" :key="idx">
|
|
|
|
|
+ <div class="stat-card" :class="'stat-' + card.color">
|
|
|
|
|
+ <div class="stat-icon-wrap">
|
|
|
|
|
+ <i :class="card.icon"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-value">{{ card.value }}</div>
|
|
|
|
|
+ <div class="stat-label">{{ card.label }}</div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </el-card>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- </el-row>
|
|
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
|
|
|
- <!-- 收入和会员概览 -->
|
|
|
|
|
- <el-row :gutter="20" class="chart-row">
|
|
|
|
|
- <el-col :span="16">
|
|
|
|
|
- <el-card class="chart-card">
|
|
|
|
|
|
|
+ <el-row :gutter="16" class="section-row">
|
|
|
|
|
+ <el-col :xs="24" :md="16">
|
|
|
|
|
+ <el-card class="chart-card" v-loading="trendLoading">
|
|
|
<div slot="header" class="card-header">
|
|
<div slot="header" class="card-header">
|
|
|
- <span>用户与家庭增长趋势(近30天)</span>
|
|
|
|
|
|
|
+ <span><i class="el-icon-trend"></i> 用户与家庭增长趋势(近30天)</span>
|
|
|
</div>
|
|
</div>
|
|
|
<div ref="trendChart" class="echarts-container"></div>
|
|
<div ref="trendChart" class="echarts-container"></div>
|
|
|
</el-card>
|
|
</el-card>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
- <el-col :span="8">
|
|
|
|
|
- <el-card class="chart-card">
|
|
|
|
|
|
|
+ <el-col :xs="24" :md="8">
|
|
|
|
|
+ <el-card class="chart-card" v-loading="memberLoading">
|
|
|
<div slot="header" class="card-header">
|
|
<div slot="header" class="card-header">
|
|
|
- <span>会员等级分布</span>
|
|
|
|
|
|
|
+ <span><i class="el-icon-pie-chart"></i> 会员等级分布</span>
|
|
|
</div>
|
|
</div>
|
|
|
<div ref="memberPieChart" class="echarts-container"></div>
|
|
<div ref="memberPieChart" class="echarts-container"></div>
|
|
|
</el-card>
|
|
</el-card>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
</el-row>
|
|
</el-row>
|
|
|
|
|
|
|
|
- <!-- 收入概览 -->
|
|
|
|
|
- <el-row :gutter="20" class="chart-row">
|
|
|
|
|
- <el-col :span="8">
|
|
|
|
|
- <el-card class="chart-card">
|
|
|
|
|
|
|
+ <el-row :gutter="16" class="section-row">
|
|
|
|
|
+ <el-col :xs="24" :md="8" v-for="(item, idx) in revenueCards" :key="idx">
|
|
|
|
|
+ <el-card class="revenue-card" v-loading="revenueLoading">
|
|
|
<div slot="header" class="card-header">
|
|
<div slot="header" class="card-header">
|
|
|
- <span>本月佣金</span>
|
|
|
|
|
|
|
+ <span><i :class="item.icon"></i> {{ item.label }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="revenue-value">¥{{ revenueData.monthCommission || 0 }}</div>
|
|
|
|
|
- <div class="revenue-sub">待结算: ¥{{ revenueData.pendingCommission || 0 }}</div>
|
|
|
|
|
- </el-card>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="8">
|
|
|
|
|
- <el-card class="chart-card">
|
|
|
|
|
- <div slot="header" class="card-header">
|
|
|
|
|
- <span>累计佣金</span>
|
|
|
|
|
|
|
+ <div class="revenue-body">
|
|
|
|
|
+ <div class="revenue-value" :class="'revenue-' + item.color">{{ item.value }}</div>
|
|
|
|
|
+ <div v-if="item.sub" class="revenue-sub">{{ item.sub }}</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="revenue-value">¥{{ revenueData.totalCommission || 0 }}</div>
|
|
|
|
|
- </el-card>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="8">
|
|
|
|
|
- <el-card class="chart-card">
|
|
|
|
|
- <div slot="header" class="card-header">
|
|
|
|
|
- <span>本月会员升级</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="revenue-value">{{ membershipData.monthUpgrades || 0 }}</div>
|
|
|
|
|
- <div class="revenue-sub">试用会员: {{ membershipData.trialCount || 0 }}</div>
|
|
|
|
|
</el-card>
|
|
</el-card>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
</el-row>
|
|
</el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <el-card class="section-card" v-loading="statLoading">
|
|
|
|
|
+ <div slot="header" class="card-header">
|
|
|
|
|
+ <span><i class="el-icon-s-order"></i> 最近任务</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-table :data="recentTasks" border stripe empty-text="暂无任务" size="small">
|
|
|
|
|
+ <el-table-column prop="id" label="ID" width="60" />
|
|
|
|
|
+ <el-table-column prop="title" label="任务名称" min-width="160" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column prop="type" label="类型" width="80">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <el-tag :type="taskTypeTag(row.type)" size="mini">{{ row.type }}</el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="points" label="积分" width="60" />
|
|
|
|
|
+ <el-table-column prop="status" label="状态" width="80">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <el-tag :type="taskStatusTag(row.status)" size="mini">{{ taskStatusLabel(row.status) }}</el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="创建时间" width="150">
|
|
|
|
|
+ <template slot-scope="{ row }">{{ row.createdAt || '-' }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </el-card>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
import * as echarts from 'echarts'
|
|
import * as echarts from 'echarts'
|
|
|
|
|
+import { getDashboardStats, getRevenueStats, getMembershipStats, getTrendStats } from '@/api/admin'
|
|
|
|
|
|
|
|
export default {
|
|
export default {
|
|
|
name: 'AdminDashboard',
|
|
name: 'AdminDashboard',
|
|
|
data() {
|
|
data() {
|
|
|
return {
|
|
return {
|
|
|
|
|
+ statLoading: false,
|
|
|
|
|
+ trendLoading: false,
|
|
|
|
|
+ memberLoading: false,
|
|
|
|
|
+ revenueLoading: false,
|
|
|
statCards: [
|
|
statCards: [
|
|
|
- { label: '家庭总数', value: 0, icon: '👨👩👧', type: 'orange' },
|
|
|
|
|
- { label: '家长总数', value: 0, icon: '👤', type: 'blue' },
|
|
|
|
|
- { label: '成长规划师', value: 0, icon: '🎓', type: 'green' },
|
|
|
|
|
- { label: '待审核规划师', value: 0, icon: '⏳', type: 'red' }
|
|
|
|
|
|
|
+ { label: '家庭总数', value: '-', icon: 'el-icon-s-home', color: 'orange' },
|
|
|
|
|
+ { label: '家长总数', value: '-', icon: 'el-icon-user', color: 'blue' },
|
|
|
|
|
+ { label: '孩子总数', value: '-', icon: 'el-icon-s-custom', color: 'cyan' },
|
|
|
|
|
+ { label: '规划师总数', value: '-', icon: 'el-icon-school', color: 'green' },
|
|
|
|
|
+ { label: '待审核规划师', value: '-', icon: 'el-icon-time', color: 'red' },
|
|
|
|
|
+ { label: '待审核套餐', value: '-', icon: 'el-icon-document', color: 'purple' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ revenueCards: [
|
|
|
|
|
+ { label: '本月佣金', value: '-', icon: 'el-icon-money', color: 'orange', sub: '' },
|
|
|
|
|
+ { label: '累计佣金', value: '-', icon: 'el-icon-coin', color: 'green', sub: '' },
|
|
|
|
|
+ { label: '本月会员升级', value: '-', icon: 'el-icon-top', color: 'blue', sub: '' }
|
|
|
],
|
|
],
|
|
|
- revenueData: {},
|
|
|
|
|
|
|
+ recentTasks: [],
|
|
|
|
|
+ trendData: { familyTrend: {}, userTrend: {} },
|
|
|
membershipData: {},
|
|
membershipData: {},
|
|
|
- trendData: { familyTrend: {}, userTrend: {} }
|
|
|
|
|
|
|
+ trendChart: null,
|
|
|
|
|
+ memberChart: null,
|
|
|
|
|
+ resizeHandler: null
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
mounted() {
|
|
mounted() {
|
|
|
- this.loadOverview()
|
|
|
|
|
|
|
+ this.loadDashboard()
|
|
|
this.loadRevenue()
|
|
this.loadRevenue()
|
|
|
this.loadMembership()
|
|
this.loadMembership()
|
|
|
this.loadTrend()
|
|
this.loadTrend()
|
|
|
|
|
+ this.resizeHandler = () => {
|
|
|
|
|
+ this.trendChart && this.trendChart.resize()
|
|
|
|
|
+ this.memberChart && this.memberChart.resize()
|
|
|
|
|
+ }
|
|
|
|
|
+ window.addEventListener('resize', this.resizeHandler)
|
|
|
|
|
+ },
|
|
|
|
|
+ beforeDestroy() {
|
|
|
|
|
+ this.trendChart && this.trendChart.dispose()
|
|
|
|
|
+ this.memberChart && this.memberChart.dispose()
|
|
|
|
|
+ this.resizeHandler && window.removeEventListener('resize', this.resizeHandler)
|
|
|
},
|
|
},
|
|
|
methods: {
|
|
methods: {
|
|
|
- async loadOverview() {
|
|
|
|
|
|
|
+ async loadDashboard() {
|
|
|
|
|
+ this.statLoading = true
|
|
|
try {
|
|
try {
|
|
|
- const res = await this.$http.post('/api/stats/overview')
|
|
|
|
|
- if (res.data.code === 200) {
|
|
|
|
|
- const d = res.data.data
|
|
|
|
|
|
|
+ const res = await getDashboardStats()
|
|
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
|
|
+ const d = res.data
|
|
|
this.statCards[0].value = d.totalFamilies || 0
|
|
this.statCards[0].value = d.totalFamilies || 0
|
|
|
this.statCards[1].value = d.totalParents || 0
|
|
this.statCards[1].value = d.totalParents || 0
|
|
|
- this.statCards[2].value = d.totalTeachers || 0
|
|
|
|
|
- this.statCards[3].value = d.pendingGuides || 0
|
|
|
|
|
|
|
+ this.statCards[2].value = d.totalChildren || 0
|
|
|
|
|
+ this.statCards[3].value = d.totalTeachers || 0
|
|
|
|
|
+ this.statCards[4].value = d.pendingGuideCount || 0
|
|
|
|
|
+ this.statCards[5].value = d.pendingPackageCount || 0
|
|
|
|
|
+ this.recentTasks = d.recentTasks || []
|
|
|
}
|
|
}
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
- console.error('加载概览失败', e)
|
|
|
|
|
|
|
+ this.$message.error('加载概览数据失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.statLoading = false
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
async loadRevenue() {
|
|
async loadRevenue() {
|
|
|
|
|
+ this.revenueLoading = true
|
|
|
try {
|
|
try {
|
|
|
- const res = await this.$http.post('/api/stats/revenue')
|
|
|
|
|
- if (res.data.code === 200) {
|
|
|
|
|
- this.revenueData = res.data.data
|
|
|
|
|
|
|
+ const res = await getRevenueStats()
|
|
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
|
|
+ const d = res.data
|
|
|
|
|
+ this.revenueCards[0].value = '\u00a5' + (d.monthCommission || 0)
|
|
|
|
|
+ this.revenueCards[0].sub = '\u5f85\u7ed3\u7b97: \u00a5' + (d.pendingCommission || 0)
|
|
|
|
|
+ this.revenueCards[1].value = '\u00a5' + (d.totalCommission || 0)
|
|
|
}
|
|
}
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
- console.error('加载收入统计失败', e)
|
|
|
|
|
|
|
+ this.$message.error('\u52a0\u8f7d\u6536\u5165\u6570\u636e\u5931\u8d25')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.revenueLoading = false
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
async loadMembership() {
|
|
async loadMembership() {
|
|
|
|
|
+ this.memberLoading = true
|
|
|
try {
|
|
try {
|
|
|
- const res = await this.$http.post('/api/stats/membership')
|
|
|
|
|
- if (res.data.code === 200) {
|
|
|
|
|
- this.membershipData = res.data.data
|
|
|
|
|
- this.initMemberPieChart()
|
|
|
|
|
|
|
+ const res = await getMembershipStats()
|
|
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
|
|
+ this.membershipData = res.data
|
|
|
|
|
+ this.revenueCards[2].value = res.data.monthUpgrades || 0
|
|
|
|
|
+ this.revenueCards[2].sub = '\u8bd5\u7528\u4f1a\u5458: ' + (res.data.trialCount || 0)
|
|
|
|
|
+ this.$nextTick(() => this.initMemberPieChart())
|
|
|
}
|
|
}
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
- console.error('加载会员统计失败', e)
|
|
|
|
|
|
|
+ this.$message.error('\u52a0\u8f7d\u4f1a\u5458\u6570\u636e\u5931\u8d25')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.memberLoading = false
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
async loadTrend() {
|
|
async loadTrend() {
|
|
|
|
|
+ this.trendLoading = true
|
|
|
try {
|
|
try {
|
|
|
- const res = await this.$http.post('/api/stats/trend')
|
|
|
|
|
- if (res.data.code === 200) {
|
|
|
|
|
- this.trendData = res.data.data
|
|
|
|
|
- this.initTrendChart()
|
|
|
|
|
|
|
+ const res = await getTrendStats()
|
|
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
|
|
+ this.trendData = res.data
|
|
|
|
|
+ this.$nextTick(() => this.initTrendChart())
|
|
|
}
|
|
}
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
- console.error('加载趋势失败', e)
|
|
|
|
|
|
|
+ this.$message.error('\u52a0\u8f7d\u8d8b\u52bf\u6570\u636e\u5931\u8d25')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.trendLoading = false
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
initTrendChart() {
|
|
initTrendChart() {
|
|
|
- const chart = echarts.init(this.$refs.trendChart)
|
|
|
|
|
|
|
+ if (this.trendChart) this.trendChart.dispose()
|
|
|
|
|
+ this.trendChart = echarts.init(this.$refs.trendChart)
|
|
|
const familyData = Object.values(this.trendData.familyTrend || {})
|
|
const familyData = Object.values(this.trendData.familyTrend || {})
|
|
|
const userData = Object.values(this.trendData.userTrend || {})
|
|
const userData = Object.values(this.trendData.userTrend || {})
|
|
|
const xAxis = Object.keys(this.trendData.familyTrend || {})
|
|
const xAxis = Object.keys(this.trendData.familyTrend || {})
|
|
|
-
|
|
|
|
|
- chart.setOption({
|
|
|
|
|
|
|
+ this.trendChart.setOption({
|
|
|
tooltip: { trigger: 'axis' },
|
|
tooltip: { trigger: 'axis' },
|
|
|
- legend: { data: ['新增家庭', '新增用户'] },
|
|
|
|
|
- xAxis: { type: 'category', data: xAxis },
|
|
|
|
|
- yAxis: { type: 'value' },
|
|
|
|
|
|
|
+ legend: { data: ['\u65b0\u589e\u5bb6\u5ead', '\u65b0\u589e\u7528\u6237'] },
|
|
|
|
|
+ grid: { left: 50, right: 20, bottom: 30, top: 40 },
|
|
|
|
|
+ xAxis: { type: 'category', data: xAxis, axisLabel: { rotate: xAxis.length > 15 ? 45 : 0 } },
|
|
|
|
|
+ yAxis: { type: 'value', minInterval: 1 },
|
|
|
series: [
|
|
series: [
|
|
|
- { name: '新增家庭', type: 'bar', data: familyData, itemStyle: { color: '#F97316' } },
|
|
|
|
|
- { name: '新增用户', type: 'line', data: userData, itemStyle: { color: '#0EA5E9' } }
|
|
|
|
|
|
|
+ { name: '\u65b0\u589e\u5bb6\u5ead', type: 'bar', barWidth: '40%', data: familyData, itemStyle: { color: '#F97316', borderRadius: [4, 4, 0, 0] } },
|
|
|
|
|
+ { name: '\u65b0\u589e\u7528\u6237', type: 'line', smooth: true, data: userData, lineStyle: { color: '#0EA5E9', width: 2 }, itemStyle: { color: '#0EA5E9' } }
|
|
|
]
|
|
]
|
|
|
})
|
|
})
|
|
|
},
|
|
},
|
|
|
initMemberPieChart() {
|
|
initMemberPieChart() {
|
|
|
- const chart = echarts.init(this.$refs.memberPieChart)
|
|
|
|
|
- chart.setOption({
|
|
|
|
|
- tooltip: { trigger: 'item' },
|
|
|
|
|
- legend: { orient: 'vertical', left: 'left' },
|
|
|
|
|
|
|
+ if (this.memberChart) this.memberChart.dispose()
|
|
|
|
|
+ this.memberChart = echarts.init(this.$refs.memberPieChart)
|
|
|
|
|
+ this.memberChart.setOption({
|
|
|
|
|
+ tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
|
|
|
|
+ legend: { orient: 'vertical', left: 'left', top: 'center' },
|
|
|
series: [{
|
|
series: [{
|
|
|
type: 'pie',
|
|
type: 'pie',
|
|
|
- radius: ['40%', '70%'],
|
|
|
|
|
|
|
+ radius: ['40%', '65%'],
|
|
|
|
|
+ center: ['55%', '50%'],
|
|
|
|
|
+ avoidLabelOverlap: true,
|
|
|
|
|
+ label: { show: false },
|
|
|
|
|
+ emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
|
|
|
data: [
|
|
data: [
|
|
|
- { value: this.membershipData.freeCount || 0, name: '免费用户' },
|
|
|
|
|
- { value: this.membershipData.familyCount || 0, name: '家庭会员' },
|
|
|
|
|
- { value: this.membershipData.providerCount || 0, name: '服务商' }
|
|
|
|
|
- ],
|
|
|
|
|
- itemStyle: {
|
|
|
|
|
- color: function(params) {
|
|
|
|
|
- var colors = ['#94A3B8', '#F97316', '#10B981']
|
|
|
|
|
- return colors[params.dataIndex]
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ { value: this.membershipData.freeCount || 0, name: '\u514d\u8d39\u7528\u6237', itemStyle: { color: '#94A3B8' } },
|
|
|
|
|
+ { value: this.membershipData.familyCount || 0, name: '\u5bb6\u5ead\u4f1a\u5458', itemStyle: { color: '#F97316' } },
|
|
|
|
|
+ { value: this.membershipData.providerCount || 0, name: '\u670d\u52a1\u5546', itemStyle: { color: '#10B981' } }
|
|
|
|
|
+ ]
|
|
|
}]
|
|
}]
|
|
|
})
|
|
})
|
|
|
|
|
+ },
|
|
|
|
|
+ taskTypeTag(type) {
|
|
|
|
|
+ const map = { daily: '', weekly: 'success', challenge: 'warning', custom: 'info' }
|
|
|
|
|
+ return map[type] || ''
|
|
|
|
|
+ },
|
|
|
|
|
+ taskStatusTag(status) {
|
|
|
|
|
+ const map = { pending: 'warning', approved: 'success', completed: '', rejected: 'danger' }
|
|
|
|
|
+ return map[status] || 'info'
|
|
|
|
|
+ },
|
|
|
|
|
+ taskStatusLabel(status) {
|
|
|
|
|
+ const map = { pending: '\u5f85\u5ba1\u6838', approved: '\u5df2\u901a\u8fc7', completed: '\u5df2\u5b8c\u6210', rejected: '\u5df2\u62d2\u7edd' }
|
|
|
|
|
+ return map[status] || status
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
<style scoped>
|
|
|
-.dashboard-container {
|
|
|
|
|
- padding: 20px;
|
|
|
|
|
-}
|
|
|
|
|
-.stat-row {
|
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+.dashboard-container { padding: 20px; }
|
|
|
|
|
+.section-row { margin-top: 16px; }
|
|
|
|
|
+.section-card { margin-bottom: 0; }
|
|
|
|
|
+.card-header { font-weight: 600; font-size: 15px; color: #1E293B; }
|
|
|
|
|
+.card-header i { margin-right: 6px; }
|
|
|
.stat-card {
|
|
.stat-card {
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- padding: 10px;
|
|
|
|
|
-}
|
|
|
|
|
-.stat-icon {
|
|
|
|
|
- font-size: 36px;
|
|
|
|
|
- margin-right: 16px;
|
|
|
|
|
-}
|
|
|
|
|
-.stat-value {
|
|
|
|
|
- font-size: 28px;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- color: #1E293B;
|
|
|
|
|
-}
|
|
|
|
|
-.stat-label {
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- color: #64748B;
|
|
|
|
|
- margin-top: 4px;
|
|
|
|
|
-}
|
|
|
|
|
-.chart-card {
|
|
|
|
|
- min-height: 300px;
|
|
|
|
|
-}
|
|
|
|
|
-.echarts-container {
|
|
|
|
|
- height: 260px;
|
|
|
|
|
|
|
+ display: flex; align-items: center; padding: 16px 12px;
|
|
|
|
|
+ background: #fff; border-radius: 8px; border: 1px solid #f0f0f0;
|
|
|
|
|
+ transition: all 0.25s ease; margin-bottom: 12px;
|
|
|
}
|
|
}
|
|
|
-.card-header {
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #1E293B;
|
|
|
|
|
|
|
+.stat-card:hover {
|
|
|
|
|
+ transform: translateY(-2px);
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
|
|
}
|
|
}
|
|
|
-.revenue-value {
|
|
|
|
|
- font-size: 32px;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- color: #F97316;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- margin-top: 20px;
|
|
|
|
|
|
|
+.stat-icon-wrap {
|
|
|
|
|
+ width: 44px; height: 44px; border-radius: 10px;
|
|
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
|
|
+ font-size: 22px; margin-right: 12px; flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
-.revenue-sub {
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- color: #64748B;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- margin-top: 8px;
|
|
|
|
|
|
|
+.stat-orange .stat-icon-wrap { background: #FEF3C7; color: #F97316; }
|
|
|
|
|
+.stat-blue .stat-icon-wrap { background: #DBEAFE; color: #3B82F6; }
|
|
|
|
|
+.stat-cyan .stat-icon-wrap { background: #CFFAFE; color: #06B6D4; }
|
|
|
|
|
+.stat-green .stat-icon-wrap { background: #D1FAE5; color: #10B981; }
|
|
|
|
|
+.stat-red .stat-icon-wrap { background: #FEE2E2; color: #EF4444; }
|
|
|
|
|
+.stat-purple .stat-icon-wrap { background: #EDE9FE; color: #8B5CF6; }
|
|
|
|
|
+.stat-body { flex: 1; min-width: 0; }
|
|
|
|
|
+.stat-value { font-size: 24px; font-weight: 700; color: #1E293B; line-height: 1.2; }
|
|
|
|
|
+.stat-label { font-size: 13px; color: #94A3B8; margin-top: 2px; }
|
|
|
|
|
+.chart-card { min-height: 320px; }
|
|
|
|
|
+.echarts-container { height: 260px; width: 100%; }
|
|
|
|
|
+.revenue-card { min-height: 140px; }
|
|
|
|
|
+.revenue-body { text-align: center; padding: 8px 0; }
|
|
|
|
|
+.revenue-value { font-size: 28px; font-weight: 700; line-height: 1.3; }
|
|
|
|
|
+.revenue-orange { color: #F97316; }
|
|
|
|
|
+.revenue-green { color: #10B981; }
|
|
|
|
|
+.revenue-blue { color: #3B82F6; }
|
|
|
|
|
+.revenue-sub { font-size: 13px; color: #94A3B8; margin-top: 6px; }
|
|
|
|
|
+@media (max-width: 768px) {
|
|
|
|
|
+ .dashboard-container { padding: 12px; }
|
|
|
|
|
+ .stat-card { padding: 12px 10px; }
|
|
|
|
|
+ .stat-value { font-size: 20px; }
|
|
|
|
|
+ .echarts-container { height: 200px; }
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|