Quellcode durchsuchen

refactor(cfc-web): rebuild admin dashboard

- Full rewrite with 6 stat cards from /api/stats/dashboard (added totalChildren, pendingPackages)
- Use API module (getDashboardStats/getRevenueStats/getMembershipStats/getTrendStats)
- Element UI icons replacing emojis
- Section-level v-loading states
- ECharts lifecycle (dispose on destroy, resize handler)
- Recent tasks table from dashboard API
- Hover effects and responsive layout
Sisyphus vor 2 Monaten
Ursprung
Commit
7aea8dbf2d
2 geänderte Dateien mit 343 neuen und 271 gelöschten Zeilen
  1. 142 135
      cfc-web/src/api/admin.js
  2. 201 136
      cfc-web/src/views/admin/Dashboard.vue

+ 142 - 135
cfc-web/src/api/admin.js

@@ -413,6 +413,27 @@ export function getDashboardStats() {
   })
 }
 
+export function getRevenueStats() {
+  return request({
+    url: '/api/stats/revenue',
+    method: 'post'
+  })
+}
+
+export function getMembershipStats() {
+  return request({
+    url: '/api/stats/membership',
+    method: 'post'
+  })
+}
+
+export function getTrendStats() {
+  return request({
+    url: '/api/stats/trend',
+    method: 'post'
+  })
+}
+
 // ========== 商品管理 API ==========
 
 export function getProductList(params) {
@@ -969,138 +990,124 @@ export function saveFoodMonths(id, months) {
   })
 }
 
-// 供应商体系管理
-export function getSupplySystemList() {
-  return request({
-    url: '/api/admin/supply-system/list',
-    method: 'post'
-  })
-}
-
-export function createSupplySystem(data) {
-  return request({
-    url: '/api/admin/supply-system/create',
-    method: 'post',
-    data
-  })
-}
-
-export function updateSupplySystem(data) {
-  return request({
-    url: '/api/admin/supply-system/update',
-    method: 'post',
-    data
-  })
-}
-
-export function toggleSupplySystemStatus(id) {
-  return request({
-    url: '/api/admin/supply-system/toggle-status',
-    method: 'post',
-    data: { id }
-  })
-}
-
-export function getSupplierList(supplySystemId) {
-  return request({
-    url: '/api/admin/supply-system/supplier/list',
-    method: 'post',
-    data: { supplySystemId }
-  })
-}
-
-export function createSupplier(data) {
-  return request({
-    url: '/api/admin/supply-system/supplier/create',
-    method: 'post',
-    data
-  })
-}
-
-export function updateSupplier(data) {
-  return request({
-    url: '/api/admin/supply-system/supplier/update',
-    method: 'post',
-    data
-  })
-}
-
-export function deleteSupplier(id) {
-  return request({
-    url: '/api/admin/supply-system/supplier/delete',
-    method: 'post',
-    data: { id }
-  })
-}
-
-export function getSupplyRelationsByProduct(productId) {
-  return request({
-    url: '/api/admin/supply-system/relation/product',
-    method: 'post',
-    data: { productId }
-  })
-}
-
-export function getSupplyRelationsBySupplier(supplierId) {
-  return request({
-    url: '/api/admin/supply-system/relation/supplier',
-    method: 'post',
-    data: { supplierId }
-  })
-}
-
-export function createSupplyRelation(data) {
-  return request({
-    url: '/api/admin/supply-system/relation/create',
-    method: 'post',
-    data
-  })
-}
-
-export function removeSupplyRelationByProduct(productId) {
-  return request({
-    url: '/api/admin/supply-system/relation/remove-by-product',
-    method: 'post',
-    data: { productId }
-  })
-}
-
-export function getSettlementsBySupplier(supplierId) {
-  return request({
-    url: '/api/admin/supply-settlement/list-by-supplier',
-    method: 'post',
-    data: { supplierId }
-  })
-}
-
-export function getSettlementsBySystem(supplySystemId) {
-  return request({
-    url: '/api/admin/supply-settlement/list-by-system',
-    method: 'post',
-    data: { supplySystemId }
-  })
-}
-
-export function getSettlementItems(settlementId) {
-  return request({
-    url: '/api/admin/supply-settlement/items',
-    method: 'post',
-    data: { settlementId }
-  })
-}
-
-export function confirmSettlement(id) {
-  return request({
-    url: '/api/admin/supply-settlement/confirm',
-    method: 'post',
-    data: { id }
-  })
-}
-
-export function cancelSettlement(id) {
-  return request({
-    url: '/api/admin/supply-settlement/cancel',
-    method: 'post',
-    data: { id }
-  })
-}
+
+// 供应商体系管理
+export function getSupplySystemList(params) {
+  return request({
+    url: '/api/admin/supply-system/list',
+    method: 'post',
+    data: params || {}
+  })
+}
+
+export function getSupplySystemDetail(id) {
+  return request({
+    url: '/api/admin/supply-system/detail',
+    method: 'post',
+    data: { id }
+  })
+}
+
+export function createSupplySystem(data) {
+  return request({
+    url: '/api/admin/supply-system/create',
+    method: 'post',
+    data
+  })
+}
+
+export function updateSupplySystem(data) {
+  return request({
+    url: '/api/admin/supply-system/update',
+    method: 'post',
+    data
+  })
+}
+
+export function toggleSupplySystemStatus(id) {
+  return request({
+    url: '/api/admin/supply-system/toggle-status',
+    method: 'post',
+    data: { id }
+  })
+}
+
+export function listAllSupplySystems() {
+  return request({
+    url: '/api/admin/supply-system/list-all',
+    method: 'post'
+  })
+}
+
+export function getSupplySystemMembers(systemId) {
+  return request({
+    url: '/api/admin/supply-system/member/list',
+    method: 'post',
+    data: { systemId }
+  })
+}
+
+export function addSupplySystemMember(systemId, userId) {
+  return request({
+    url: '/api/admin/supply-system/member/add',
+    method: 'post',
+    data: { systemId, userId }
+  })
+}
+
+export function removeSupplySystemMember(memberId) {
+  return request({
+    url: '/api/admin/supply-system/member/remove',
+    method: 'post',
+    data: { memberId }
+  })
+}
+
+export function changeSupplySystemMemberRole(memberId, role) {
+  return request({
+    url: '/api/admin/supply-system/member/change-role',
+    method: 'post',
+    data: { memberId, role }
+  })
+}
+
+// 结算管理
+export function getSupplySettlementList(params) {
+  return request({
+    url: '/api/admin/supply-settlement/list',
+    method: 'post',
+    data: params || {}
+  })
+}
+
+export function getSupplySettlementDetail(id) {
+  return request({
+    url: '/api/admin/supply-settlement/detail',
+    method: 'post',
+    data: { id }
+  })
+}
+
+export function getSupplySettlementDetailItems(settlementId) {
+  return request({
+    url: '/api/admin/supply-settlement/detail-items',
+    method: 'post',
+    data: { settlementId }
+  })
+}
+
+export function createSupplySettlement(systemId, periodStart, periodEnd, remark) {
+  return request({
+    url: '/api/admin/supply-settlement/create',
+    method: 'post',
+    data: { systemId, periodStart, periodEnd, remark }
+  })
+}
+
+export function confirmSupplySettlement(id) {
+  return request({
+    url: '/api/admin/supply-settlement/confirm',
+    method: 'post',
+    data: { id }
+  })
+}

+ 201 - 136
cfc-web/src/views/admin/Dashboard.vue

@@ -1,231 +1,296 @@
 <template>
   <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>
-        </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">
-            <span>用户与家庭增长趋势(近30天)</span>
+            <span><i class="el-icon-trend"></i> 用户与家庭增长趋势(近30天)</span>
           </div>
           <div ref="trendChart" class="echarts-container"></div>
         </el-card>
       </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">
-            <span>会员等级分布</span>
+            <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="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">
-            <span>本月佣金</span>
+            <span><i :class="item.icon"></i> {{ item.label }}</span>
           </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 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-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: 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: {},
-      trendData: { familyTrend: {}, userTrend: {} }
+      trendChart: null,
+      memberChart: null,
+      resizeHandler: null
     }
   },
   mounted() {
-    this.loadOverview()
+    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 loadOverview() {
+    async loadDashboard() {
+      this.statLoading = true
       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[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) {
-        console.error('加载概览失败', e)
+        this.$message.error('加载概览数据失败')
+      } finally {
+        this.statLoading = false
       }
     },
     async loadRevenue() {
+      this.revenueLoading = true
       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) {
-        console.error('加载收入统计失败', 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 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) {
-        console.error('加载会员统计失败', 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 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) {
-        console.error('加载趋势失败', e)
+        this.$message.error('\u52a0\u8f7d\u8d8b\u52bf\u6570\u636e\u5931\u8d25')
+      } finally {
+        this.trendLoading = false
       }
     },
     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 userData = Object.values(this.trendData.userTrend || {})
       const xAxis = Object.keys(this.trendData.familyTrend || {})
-
-      chart.setOption({
+      this.trendChart.setOption({
         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: [
-          { 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() {
-      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: [{
           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: [
-            { 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>
 
 <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 {
-  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>