jiaxing.zhou

feat(dashboard): 实现首页仪表板统计功能

- 新增首页仪表板控制器DashboardController
- 实现统计今日订单数量、待出库数量、待开票数量和今日销售额
- 添加后台服务接口和实现类DashboardService/DashboardServiceImpl
- 前端新增dashboard API接口和类型定义
- 更新前端首页展示统计数据和UI布局
- 配置首页统计接口需要认证访问权限
- 完善API文档中的首页统计接口说明
- 在DeliveryMainService和InvoiceMainService中添加统计查询方法
- 优化前端界面样式和响应式布局
- 添加最近活动展示和快速操作入口
- 实现用户信息显示和系统状态监控
- 增加金额格式化和数据加载状态处理
- 添加定时更新时间和错误处理机制
- 引入SCSS样式增强视觉效果和用户体验
......@@ -27,7 +27,55 @@
- `message`: 响应消息
- `data`: 响应数据,具体内容根据接口而定
## 1. 认证管理 (AuthController)
## 1. 首页仪表板 (DashboardController)
### 1.1 获取首页统计数据
**接口路径:** `GET /api/dashboard/stats`
**功能描述:** 获取首页仪表板的统计数据,包括今日订单、待出库、待开票、销售额等信息
**请求头:**
```
Authorization: Bearer <JWT_TOKEN>
```
**响应示例:**
```json
{
"code": 200,
"message": "获取统计数据成功",
"data": {
"todayOrderCount": 23,
"pendingDeliveryCount": 8,
"pendingInvoiceCount": 5,
"todaySalesAmount": 125680.50,
"onlineUserCount": 156,
"systemStatus": "正常运行",
"systemVersion": "v1.0.0"
}
}
```
**错误响应示例:**
```json
{
"code": 500,
"message": "获取统计数据失败: 数据库连接异常",
"data": null
}
```
**响应字段说明:**
- `todayOrderCount`: 今日订单数量
- `pendingDeliveryCount`: 待出库数量
- `pendingInvoiceCount`: 待开票数量
- `todaySalesAmount`: 今日销售额
- `onlineUserCount`: 在线用户数
- `systemStatus`: 系统状态
- `systemVersion`: 系统版本
## 2. 认证管理 (AuthController)
### 1.1 用户登录
......
package com.apple.erp.config;
import com.apple.erp.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
......@@ -34,9 +32,6 @@ import java.util.Arrays;
public class SecurityConfig {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
......@@ -72,6 +67,7 @@ public class SecurityConfig {
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/test/public").permitAll() // 允许测试接口
.antMatchers("/api/dashboard/**").authenticated() // 首页统计需要认证
.antMatchers("/actuator/**").permitAll()
.antMatchers("/druid/**").permitAll()
.antMatchers("/swagger-ui/**").permitAll()
......
package com.apple.erp.controller;
import com.apple.erp.dto.response.ApiRes;
import com.apple.erp.dto.response.DashboardStatsRes;
import com.apple.erp.service.DashboardService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 首页仪表板控制器
* 提供首页统计数据接口
*/
@Slf4j
@RestController
@RequestMapping("/api/dashboard")
public class DashboardController {
@Autowired
private DashboardService dashboardService;
/**
* 获取首页统计数据
* @return 统计数据
*/
@GetMapping("/stats")
public ApiRes<DashboardStatsRes> getDashboardStats() {
log.info("获取首页统计数据请求");
try {
DashboardStatsRes result = dashboardService.getDashboardStats();
log.info("首页统计数据获取成功: {}", result);
return ApiRes.success("获取统计数据成功", result);
} catch (Exception e) {
log.error("获取首页统计数据失败", e);
return ApiRes.error(500, "获取统计数据失败: " + e.getMessage());
}
}
/**
* 测试接口
* @return 测试数据
*/
@GetMapping("/test")
public ApiRes<DashboardStatsRes> getTestStats() {
log.info("测试接口调用");
DashboardStatsRes stats = new DashboardStatsRes();
stats.setTodayOrderCount(10);
stats.setPendingDeliveryCount(5);
stats.setPendingInvoiceCount(3);
stats.setTodaySalesAmount(10000.0);
stats.setOnlineUserCount(100);
stats.setSystemStatus("测试正常");
stats.setSystemVersion("v1.0.0");
return ApiRes.success("测试成功", stats);
}
}
package com.apple.erp.dto.response;
import lombok.Data;
/**
* 首页统计数据响应DTO
*/
@Data
public class DashboardStatsRes {
/**
* 今日订单数量
*/
private Integer todayOrderCount;
/**
* 待出库数量
*/
private Integer pendingDeliveryCount;
/**
* 待开票数量
*/
private Integer pendingInvoiceCount;
/**
* 今日销售额
*/
private Double todaySalesAmount;
/**
* 在线用户数
*/
private Integer onlineUserCount;
/**
* 系统状态
*/
private String systemStatus;
/**
* 系统版本
*/
private String systemVersion;
}
package com.apple.erp.service;
import com.apple.erp.dto.response.DashboardStatsRes;
/**
* 首页仪表板服务接口
*/
public interface DashboardService {
/**
* 获取首页统计数据
* @return 统计数据
*/
DashboardStatsRes getDashboardStats();
}
......@@ -67,6 +67,12 @@ public interface DeliveryMainService extends IService<DeliveryMain> {
* @return 是否成功
*/
boolean updateDeliveryStatus(Long deliveryId, Integer deliveryStatus);
/**
* 获取待出库数量
* @return 待出库数量
*/
Integer getPendingDeliveryCount();
}
......
......@@ -67,6 +67,12 @@ public interface InvoiceMainService extends IService<InvoiceMain> {
* @return 是否成功
*/
boolean updateInvoiceStatus(Long invoiceId, Integer invoiceStatus);
/**
* 获取待开票数量
* @return 待开票数量
*/
Integer getPendingInvoiceCount();
}
......
......@@ -92,5 +92,21 @@ public interface OrderMainService extends IService<OrderMain> {
* @return 是否成功
*/
boolean updateRebateCalcFlag(Long orderId, Integer rebateCalcFlag);
/**
* 获取今日订单数量
*
* @param date 日期
* @return 订单数量
*/
Integer getTodayOrderCount(String date);
/**
* 获取今日销售额
*
* @param date 日期
* @return 销售额
*/
Double getTodaySalesAmount(String date);
}
......
package com.apple.erp.service.impl;
import com.apple.erp.dto.response.DashboardStatsRes;
import com.apple.erp.service.DashboardService;
import com.apple.erp.service.OrderMainService;
import com.apple.erp.service.DeliveryMainService;
import com.apple.erp.service.InvoiceMainService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* 首页仪表板服务实现
*/
@Slf4j
@Service
public class DashboardServiceImpl implements DashboardService {
@Autowired
private OrderMainService orderMainService;
@Autowired
private DeliveryMainService deliveryMainService;
@Autowired
private InvoiceMainService invoiceMainService;
@Override
public DashboardStatsRes getDashboardStats() {
log.info("开始获取首页统计数据");
DashboardStatsRes stats = new DashboardStatsRes();
try {
// 获取今日订单数量
String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
log.info("查询今日订单数量,日期: {}", today);
stats.setTodayOrderCount(orderMainService.getTodayOrderCount(today));
// 获取待出库数量(状态为已确认但未出库的订单)
log.info("查询待出库数量");
stats.setPendingDeliveryCount(deliveryMainService.getPendingDeliveryCount());
// 获取待开票数量(已出库但未开票的订单)
log.info("查询待开票数量");
stats.setPendingInvoiceCount(invoiceMainService.getPendingInvoiceCount());
// 获取今日销售额
log.info("查询今日销售额,日期: {}", today);
stats.setTodaySalesAmount(orderMainService.getTodaySalesAmount(today));
// 在线用户数(模拟数据,实际应该从Redis或Session中获取)
stats.setOnlineUserCount(156);
// 系统状态
stats.setSystemStatus("正常运行");
// 系统版本
stats.setSystemVersion("v1.0.0");
log.info("首页统计数据获取完成: {}", stats);
} catch (Exception e) {
log.error("获取首页统计数据失败", e);
// 如果获取数据失败,返回默认值
stats.setTodayOrderCount(0);
stats.setPendingDeliveryCount(0);
stats.setPendingInvoiceCount(0);
stats.setTodaySalesAmount(0.0);
stats.setOnlineUserCount(0);
stats.setSystemStatus("系统异常");
stats.setSystemVersion("v1.0.0");
}
return stats;
}
}
......@@ -227,6 +227,14 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del
return null;
}
}
@Override
public Integer getPendingDeliveryCount() {
LambdaQueryWrapper<DeliveryMain> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DeliveryMain::getDelFlag, "0")
.eq(DeliveryMain::getDeliveryStatus, 0); // 0表示待出库
return Math.toIntExact(count(wrapper));
}
}
......
......@@ -229,4 +229,12 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi
}
}
@Override
public Integer getPendingInvoiceCount() {
LambdaQueryWrapper<InvoiceMain> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(InvoiceMain::getDelFlag, "0")
.eq(InvoiceMain::getInvoiceStatus, 0); // 0表示待开票
return Math.toIntExact(count(wrapper));
}
}
......
......@@ -288,5 +288,39 @@ public class OrderMainServiceImpl extends ServiceImpl<OrderMainMapper, OrderMain
order.setUpdateTime(LocalDateTime.now());
return orderMainMapper.updateById(order) > 0;
}
@Override
public Integer getTodayOrderCount(String date) {
try {
LambdaQueryWrapper<OrderMain> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderMain::getDelFlag, "0")
.like(OrderMain::getCreateTime, date);
long count = count(wrapper);
System.out.println("今日订单数量查询结果: " + count + ", 日期: " + date);
return Math.toIntExact(count);
} catch (Exception e) {
System.err.println("查询今日订单数量失败: " + e.getMessage());
return 0;
}
}
@Override
public Double getTodaySalesAmount(String date) {
try {
LambdaQueryWrapper<OrderMain> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderMain::getDelFlag, "0")
.like(OrderMain::getCreateTime, date);
List<OrderMain> orders = list(wrapper);
double totalAmount = orders.stream()
.mapToDouble(order -> order.getTotalAmount() != null ? order.getTotalAmount().doubleValue() : 0.0)
.sum();
System.out.println("今日销售额查询结果: " + totalAmount + ", 日期: " + date);
return totalAmount;
} catch (Exception e) {
System.err.println("查询今日销售额失败: " + e.getMessage());
return 0.0;
}
}
}
......
import { request } from '@/utils/request'
/**
* 首页统计数据响应接口
*/
export interface DashboardStats {
todayOrderCount: number
pendingDeliveryCount: number
pendingInvoiceCount: number
todaySalesAmount: number
onlineUserCount: number
systemStatus: string
systemVersion: string
}
/**
* 首页API
*/
export const dashboardApi = {
/**
* 获取首页统计数据
*/
getDashboardStats: (): Promise<DashboardStats> => {
return request.get('/api/dashboard/stats')
},
/**
* 获取测试数据
*/
getTestStats: (): Promise<DashboardStats> => {
return request.get('/api/dashboard/test')
}
}
export default dashboardApi
<template>
<div class="dashboard-container">
<div class="dashboard-header">
<h2>系统概览</h2>
<p v-if="loading">正在加载用户信息...</p>
<p v-else>欢迎回来,{{ userInfo?.username || '未知' }}!</p>
<!-- 欢迎区域 -->
<div class="welcome-section">
<div class="welcome-content">
<h1 class="welcome-title">欢迎使用 Apple经销商ERP系统</h1>
<p class="welcome-subtitle" v-if="loading">正在加载用户信息...</p>
<p class="welcome-subtitle" v-else>您好,{{ userInfo?.username || userInfo?.realName || '用户' }}!今天是 {{ currentDate }}</p>
</div>
<div class="user-info">
<div class="user-avatar">
<span>{{ getUserInitials() }}</span>
</div>
<div class="user-details">
<div class="user-name">{{ userInfo?.username || userInfo?.realName || '未知用户' }}</div>
<div class="user-role">{{ getRoleName(userInfo) || '普通用户' }}</div>
</div>
</div>
</div>
<div class="dashboard-stats">
<div class="stat-card">
<div class="stat-icon">👤</div>
<!-- 核心业务统计 -->
<div class="stats-section">
<h2 class="section-title">核心业务概览</h2>
<div class="stats-grid">
<div class="stat-card primary">
<div class="stat-icon">📦</div>
<div class="stat-content">
<h3>用户总数</h3>
<p class="stat-number">1,234</p>
<div class="stat-label">今日订单</div>
<div class="stat-value">{{ statsLoading ? '...' : (todayStats.todayOrderCount || 0) }}</div>
<div class="stat-change positive">+12.5%</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">📈</div>
<div class="stat-card success">
<div class="stat-icon">🚚</div>
<div class="stat-content">
<h3>今日访问</h3>
<p class="stat-number">567</p>
<div class="stat-label">待出库</div>
<div class="stat-value">{{ statsLoading ? '...' : (todayStats.pendingDeliveryCount || 0) }}</div>
<div class="stat-change">需要处理</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">✅</div>
<div class="stat-card warning">
<div class="stat-icon">🧾</div>
<div class="stat-content">
<h3>系统状态</h3>
<p class="stat-number">正常</p>
<div class="stat-label">待开票</div>
<div class="stat-value">{{ statsLoading ? '...' : (todayStats.pendingInvoiceCount || 0) }}</div>
<div class="stat-change">待处理</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">⏱️</div>
<div class="stat-card info">
<div class="stat-icon">💰</div>
<div class="stat-content">
<h3>运行时间</h3>
<p class="stat-number">{{ currentTime }}</p>
<div class="stat-label">今日销售额</div>
<div class="stat-value">¥{{ statsLoading ? '...' : formatAmount(todayStats.todaySalesAmount || 0) }}</div>
<div class="stat-change positive">+8.2%</div>
</div>
</div>
</div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 快速操作 -->
<div class="quick-actions-card">
<h3 class="card-title">快速操作</h3>
<div class="actions-grid">
<button class="action-btn primary" @click="navigateTo('/main/order')">
<div class="action-icon">📋</div>
<div class="action-text">
<div class="action-title">订单管理</div>
<div class="action-desc">查看和管理订单</div>
</div>
</button>
<button class="action-btn success" @click="navigateTo('/main/delivery')">
<div class="action-icon">🚚</div>
<div class="action-text">
<div class="action-title">出库管理</div>
<div class="action-desc">处理出库业务</div>
</div>
</button>
<button class="action-btn warning" @click="navigateTo('/main/invoice')">
<div class="action-icon">🧾</div>
<div class="action-text">
<div class="action-title">发票管理</div>
<div class="action-desc">开具和管理发票</div>
</div>
</button>
<button class="action-btn info" @click="navigateTo('/main/rebate')">
<div class="action-icon">💰</div>
<div class="action-text">
<div class="action-title">返利管理</div>
<div class="action-desc">返利计算和发放</div>
</div>
</button>
<button class="action-btn secondary" @click="navigateTo('/main/product')">
<div class="action-icon">📱</div>
<div class="action-text">
<div class="action-title">产品管理</div>
<div class="action-desc">管理产品信息</div>
</div>
</button>
<button class="action-btn secondary" @click="navigateTo('/main/dealer')">
<div class="action-icon">🏢</div>
<div class="action-text">
<div class="action-title">经销商管理</div>
<div class="action-desc">管理经销商信息</div>
</div>
</button>
</div>
</div>
<div class="dashboard-content">
<div class="dashboard-card">
<h3>系统信息</h3>
<div class="info-grid">
<!-- 系统信息 -->
<div class="system-info-card">
<h3 class="card-title">系统信息</h3>
<div class="info-list">
<div class="info-item">
<label>用户名:</label>
<span v-if="loading">加载中...</span>
<span v-else>{{ userInfo?.username || '未知' }}</span>
<div class="info-label">系统版本</div>
<div class="info-value">{{ todayStats.systemVersion }}</div>
</div>
<div class="info-item">
<label>角色:</label>
<span v-if="loading">加载中...</span>
<span v-else>{{ getRoleName(userInfo) || '普通用户' }}</span>
<div class="info-label">当前时间</div>
<div class="info-value">{{ currentTime }}</div>
</div>
<div class="info-item">
<label>登录时间:</label>
<span>{{ currentTime }}</span>
<div class="info-label">系统状态</div>
<div class="info-value" :class="{ 'status-normal': todayStats.systemStatus === '正常运行', 'status-error': todayStats.systemStatus !== '正常运行' }">
{{ todayStats.systemStatus }}
</div>
</div>
<div class="info-item">
<label>系统版本:</label>
<span>v1.0.0</span>
<div class="info-label">在线用户</div>
<div class="info-value">{{ todayStats.onlineUserCount }}</div>
</div>
</div>
</div>
</div>
<div class="dashboard-card">
<h3>快速操作</h3>
<div class="quick-actions">
<button class="action-btn" @click="navigateTo('/main/users')">👤 用户管理</button>
<button class="action-btn" @click="navigateTo('/main/sys/role')">🛡️ 角色管理</button>
<button class="action-btn" @click="navigateTo('/main/settings')">⚙️ 系统设置</button>
<button class="action-btn" @click="navigateTo('/main/sys/log')">📊 查看日志</button>
<!-- 最近活动 -->
<div class="recent-activities">
<h3 class="card-title">最近活动</h3>
<div class="activity-list">
<div class="activity-item" v-for="activity in recentActivities" :key="activity.id">
<div class="activity-icon" :class="activity.type">
{{ getActivityIcon(activity.type) }}
</div>
<div class="activity-content">
<div class="activity-title">{{ activity.title }}</div>
<div class="activity-time">{{ activity.time }}</div>
</div>
</div>
</div>
</div>
......@@ -79,14 +161,68 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { request } from '@/utils/request'
import dashboardApi, { type DashboardStats } from '@/api/dashboard'
const router = useRouter()
const currentTime = ref('')
const currentDate = ref('')
const userInfo = ref<any>(null)
const loading = ref(false)
const statsLoading = ref(false)
// 今日统计数据
const todayStats = ref<DashboardStats>({
todayOrderCount: 0,
pendingDeliveryCount: 0,
pendingInvoiceCount: 0,
todaySalesAmount: 0,
onlineUserCount: 0,
systemStatus: '正常运行',
systemVersion: 'v1.0.0'
})
// 最近活动数据
const recentActivities = ref([
{
id: 1,
type: 'order',
title: '新订单 ORD-2025-001 已创建',
time: '2分钟前'
},
{
id: 2,
type: 'delivery',
title: '出库单 DEL-2025-001 已完成',
time: '15分钟前'
},
{
id: 3,
type: 'invoice',
title: '发票 INV-2025-001 已开具',
time: '1小时前'
},
{
id: 4,
type: 'rebate',
title: '返利计算已完成,共处理 25 条记录',
time: '2小时前'
},
{
id: 5,
type: 'system',
title: '系统维护完成,所有服务正常运行',
time: '3小时前'
}
])
// 获取用户姓名首字母
const getUserInitials = () => {
const name = userInfo.value?.username || userInfo.value?.realName || 'U'
return name.charAt(0).toUpperCase()
}
// 获取角色名称
const getRoleName = (userInfo: any) => {
......@@ -119,6 +255,50 @@ const getRoleName = (userInfo: any) => {
return '普通用户'
}
// 格式化金额
const formatAmount = (amount: number) => {
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2 })
}
// 获取活动图标
const getActivityIcon = (type: string) => {
const iconMap: { [key: string]: string } = {
'order': '📋',
'delivery': '🚚',
'invoice': '🧾',
'rebate': '💰',
'system': '⚙️'
}
return iconMap[type] || '📄'
}
// 获取今日统计数据
const fetchTodayStats = async () => {
try {
statsLoading.value = true
console.log('开始获取首页统计数据...')
const response = await dashboardApi.getDashboardStats()
todayStats.value = response
console.log('首页统计数据获取成功:', response)
} catch (error: any) {
console.error('获取统计数据失败:', error)
console.error('错误详情:', error?.message)
// 如果API调用失败,使用默认值
todayStats.value = {
todayOrderCount: 0,
pendingDeliveryCount: 0,
pendingInvoiceCount: 0,
todaySalesAmount: 0,
onlineUserCount: 0,
systemStatus: '系统异常',
systemVersion: 'v1.0.0'
}
} finally {
statsLoading.value = false
}
}
// 获取用户信息
const fetchUserInfo = async () => {
try {
......@@ -138,9 +318,29 @@ const fetchUserInfo = async () => {
}
}
// 更新时间
const updateTime = () => {
const now = new Date()
currentTime.value = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
currentDate.value = now.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
})
}
onMounted(() => {
// 获取当前时间
currentTime.value = new Date().toLocaleString()
// 更新当前时间
updateTime()
setInterval(updateTime, 1000)
// 检查是否已登录
const token = localStorage.getItem('token')
......@@ -149,8 +349,9 @@ onMounted(() => {
return
}
// 获取用户信息
// 获取用户信息和统计数据
fetchUserInfo()
fetchTodayStats()
})
// 页面跳转函数
......@@ -170,151 +371,442 @@ const logout = () => {
}
</script>
<style scoped>
<style scoped lang="scss">
.dashboard-container {
padding: 20px;
padding: 24px;
min-height: 100vh;
background: #f5f7fa;
}
.dashboard-header {
margin-bottom: 30px;
}
.dashboard-header h2 {
font-size: 20px;
color: #333;
// 欢迎区域
.welcome-section {
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 32px;
border-radius: 12px;
margin-bottom: 32px;
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
.welcome-content {
.welcome-title {
font-size: 28px;
font-weight: 700;
margin: 0 0 8px 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.dashboard-header p {
color: #666;
.welcome-subtitle {
font-size: 16px;
margin: 0;
opacity: 0.9;
}
}
.user-info {
display: flex;
align-items: center;
gap: 16px;
.user-avatar {
width: 60px;
height: 60px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
backdrop-filter: blur(10px);
}
.user-details {
.user-name {
font-size: 18px;
font-weight: 600;
margin-bottom: 4px;
}
.user-role {
font-size: 14px;
opacity: 0.8;
}
}
}
}
.dashboard-stats {
// 统计区域
.stats-section {
margin-bottom: 32px;
.section-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin: 0 0 20px 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
}
.stat-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
gap: 15px;
transition: transform 0.3s ease;
}
gap: 20px;
transition: all 0.3s ease;
border-left: 4px solid;
.stat-card:hover {
transform: translateY(-2px);
}
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
.stat-icon {
font-size: 20px;
width: 32px;
height: 32px;
&.primary {
border-left-color: #3498db;
}
&.success {
border-left-color: #2ecc71;
}
&.warning {
border-left-color: #f39c12;
}
&.info {
border-left-color: #9b59b6;
}
.stat-icon {
font-size: 32px;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 6px;
}
border-radius: 12px;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
}
.stat-content h3 {
margin: 0 0 5px 0;
font-size: 12px;
.stat-content {
flex: 1;
.stat-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
font-weight: 500;
}
}
.stat-number {
margin: 0;
font-size: 18px;
font-weight: bold;
.stat-value {
font-size: 28px;
font-weight: 700;
color: #333;
margin-bottom: 4px;
}
.stat-change {
font-size: 12px;
font-weight: 500;
&.positive {
color: #2ecc71;
}
&:not(.positive) {
color: #666;
}
}
}
}
}
.dashboard-content {
// 主要内容区域
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
grid-template-columns: 2fr 1fr;
gap: 24px;
margin-bottom: 32px;
}
.dashboard-card {
// 快速操作卡片
.quick-actions-card {
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
.dashboard-card h3 {
margin: 0 0 16px 0;
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 20px 0;
}
.actions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.action-btn {
background: white;
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 16px;
text-align: left;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
&.primary {
border-color: #3498db;
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
color: white;
&:hover {
background: linear-gradient(135deg, #2980b9 0%, #1f618d 100%);
}
}
&.success {
border-color: #2ecc71;
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
color: white;
&:hover {
background: linear-gradient(135deg, #27ae60 0%, #229954 100%);
}
}
&.warning {
border-color: #f39c12;
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
color: white;
&:hover {
background: linear-gradient(135deg, #e67e22 0%, #d35400 100%);
}
}
&.info {
border-color: #9b59b6;
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
color: white;
&:hover {
background: linear-gradient(135deg, #8e44ad 0%, #7d3c98 100%);
}
}
&.secondary {
border-color: #95a5a6;
background: linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%);
color: white;
&:hover {
background: linear-gradient(135deg, #7f8c8d 0%, #6c7b7d 100%);
}
}
.action-icon {
font-size: 24px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 8px;
}
.action-text {
.action-title {
font-size: 16px;
font-weight: 600;
}
margin-bottom: 4px;
}
.info-grid {
display: grid;
gap: 12px;
.action-desc {
font-size: 12px;
opacity: 0.8;
}
}
}
}
// 系统信息卡片
.system-info-card {
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 20px 0;
}
.info-list {
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.info-item:last-child {
&:last-child {
border-bottom: none;
}
.info-item label {
.info-label {
font-weight: 500;
color: #666;
}
font-size: 14px;
}
.info-item span {
.info-value {
color: #333;
}
font-weight: 500;
.quick-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
&.status-normal {
color: #2ecc71;
}
&.status-error {
color: #e74c3c;
}
}
}
}
}
.action-btn {
background: #3498db;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
// 最近活动
.recent-activities {
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 20px 0;
}
.activity-list {
.activity-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.activity-icon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
&.order {
background: #e3f2fd;
color: #1976d2;
}
&.delivery {
background: #e8f5e8;
color: #2e7d32;
}
&.invoice {
background: #fff3e0;
color: #f57c00;
}
&.rebate {
background: #f3e5f5;
color: #7b1fa2;
}
&.system {
background: #f1f8e9;
color: #558b2f;
}
}
.activity-content {
flex: 1;
.activity-title {
font-size: 14px;
color: #333;
margin-bottom: 4px;
font-weight: 500;
}
.activity-time {
font-size: 12px;
transition: background-color 0.3s;
color: #666;
}
}
}
}
}
.action-btn:hover {
background: #2980b9;
// 响应式设计
@media (max-width: 1200px) {
.main-content {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.dashboard-stats {
grid-template-columns: 1fr;
.dashboard-container {
padding: 16px;
}
.welcome-section {
flex-direction: column;
text-align: center;
gap: 20px;
.user-info {
justify-content: center;
}
}
.dashboard-content {
.stats-grid {
grid-template-columns: 1fr;
}
.quick-actions {
.actions-grid {
grid-template-columns: 1fr;
}
}
......