| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- <template>
- <div class="dashboard-container">
- <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>
- </el-col>
- </el-row>
- </el-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">
- <span><i class="el-icon-trend"></i> 用户与家庭增长趋势(近30天)</span>
- </div>
- <div ref="trendChart" class="echarts-container"></div>
- </el-card>
- </el-col>
- <el-col :xs="24" :md="8">
- <el-card class="chart-card" v-loading="memberLoading">
- <div slot="header" class="card-header">
- <span><i class="el-icon-pie-chart"></i> 会员等级分布</span>
- </div>
- <div ref="memberPieChart" class="echarts-container"></div>
- </el-card>
- </el-col>
- </el-row>
- <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">
- <span><i :class="item.icon"></i> {{ item.label }}</span>
- </div>
- <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>
- </el-card>
- </el-col>
- </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>
- </template>
- <script>
- import * as echarts from 'echarts'
- import { getDashboardStats, getRevenueStats, getMembershipStats, getTrendStats } from '@/api/admin'
- export default {
- name: 'AdminDashboard',
- data() {
- return {
- statLoading: false,
- trendLoading: false,
- memberLoading: false,
- revenueLoading: false,
- statCards: [
- { 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: '' }
- ],
- recentTasks: [],
- trendData: { familyTrend: {}, userTrend: {} },
- membershipData: {},
- trendChart: null,
- memberChart: null,
- resizeHandler: null
- }
- },
- mounted() {
- this.loadDashboard()
- this.loadRevenue()
- this.loadMembership()
- 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: {
- async loadDashboard() {
- this.statLoading = true
- try {
- const res = await getDashboardStats()
- if (res.code === 200 && res.data) {
- const d = res.data
- this.statCards[0].value = d.totalFamilies || 0
- this.statCards[1].value = d.totalParents || 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) {
- this.$message.error('加载概览数据失败')
- } finally {
- this.statLoading = false
- }
- },
- async loadRevenue() {
- this.revenueLoading = true
- try {
- 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) {
- this.$message.error('\u52a0\u8f7d\u6536\u5165\u6570\u636e\u5931\u8d25')
- } finally {
- this.revenueLoading = false
- }
- },
- async loadMembership() {
- this.memberLoading = true
- try {
- 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) {
- this.$message.error('\u52a0\u8f7d\u4f1a\u5458\u6570\u636e\u5931\u8d25')
- } finally {
- this.memberLoading = false
- }
- },
- async loadTrend() {
- this.trendLoading = true
- try {
- const res = await getTrendStats()
- if (res.code === 200 && res.data) {
- this.trendData = res.data
- this.$nextTick(() => this.initTrendChart())
- }
- } catch (e) {
- this.$message.error('\u52a0\u8f7d\u8d8b\u52bf\u6570\u636e\u5931\u8d25')
- } finally {
- this.trendLoading = false
- }
- },
- initTrendChart() {
- if (this.trendChart) this.trendChart.dispose()
- this.trendChart = echarts.init(this.$refs.trendChart)
- const familyData = Object.values(this.trendData.familyTrend || {})
- const userData = Object.values(this.trendData.userTrend || {})
- const xAxis = Object.keys(this.trendData.familyTrend || {})
- this.trendChart.setOption({
- tooltip: { trigger: 'axis' },
- 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: [
- { 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() {
- 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: [{
- type: 'pie',
- radius: ['40%', '65%'],
- center: ['55%', '50%'],
- avoidLabelOverlap: true,
- label: { show: false },
- emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
- data: [
- { 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>
- <style scoped>
- .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 {
- 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;
- }
- .stat-card:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0,0,0,0.08);
- }
- .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;
- }
- .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>
|