浏览代码

统计报表页面重构

skyline 1 月之前
父节点
当前提交
170b869ea3

+ 36 - 130
haha-admin-web/src/api/statistics.ts

@@ -1,28 +1,4 @@
 import { http } from "@/utils/http";
-import {
-  USE_MOCK,
-  mockStatisticsOverview,
-  mockCategoryOverview,
-  mockCategoryTrend,
-  mockProductList,
-  mockProductTop,
-  mockProductTrend,
-  mockDeviceOverview,
-  mockDeviceList,
-  mockDeviceTrend,
-  mockShopOverview,
-  mockShopList,
-  mockShopRanking,
-  mockShopTrend,
-  mockProfitOverview,
-  mockProfitList,
-  mockProfitTrend,
-  mockProfitWarning,
-  mockRepurchaseOverview,
-  mockRepurchaseDistribution,
-  mockRepurchaseTrend,
-  mockRepurchaseUsers
-} from "./statisticsMock";
 
 type Result = {
   code: number;
@@ -41,47 +17,39 @@ type ResultTable = {
   };
 };
 
-const mockDelay = (ms: number = 300) => new Promise(resolve => setTimeout(resolve, ms));
+// ==================== 统计概览 ====================
 
-export const getStatisticsOverview = async (params: {
+export const getStatisticsOverview = (params: {
   startDate: string;
   endDate: string;
   shopId?: number;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockStatisticsOverview() };
-  }
   return http.request<Result>("get", "/statistics/overview", { params });
 };
 
-export const getCategoryOverview = async (params: {
+// ==================== 品类销售统计 ====================
+
+export const getCategoryOverview = (params: {
   startDate: string;
   endDate: string;
   shopId?: number;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockCategoryOverview() };
-  }
   return http.request<Result>("get", "/statistics/category/overview", { params });
 };
 
-export const getCategoryTrend = async (params: {
+export const getCategoryTrend = (params: {
   startDate: string;
   endDate: string;
   category?: string;
   shopId?: number;
   period?: string;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockCategoryTrend() };
-  }
   return http.request<Result>("get", "/statistics/category/trend", { params });
 };
 
-export const getProductList = async (params: {
+// ==================== 商品销售统计 ====================
+
+export const getProductList = (params: {
   page: number;
   pageSize: number;
   startDate: string;
@@ -93,51 +61,37 @@ export const getProductList = async (params: {
   sortBy?: string;
   sortOrder?: string;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockProductList(params.page, params.pageSize) };
-  }
   return http.request<ResultTable>("get", "/statistics/product/list", { params });
 };
 
-export const getProductTop = async (params: {
+export const getProductTop = (params: {
   startDate: string;
   endDate: string;
   type: string;
   limit?: number;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockProductTop(params.type) };
-  }
   return http.request<Result>("get", "/statistics/product/top", { params });
 };
 
-export const getProductTrend = async (productId: number, params: {
+export const getProductTrend = (productId: number, params: {
   startDate: string;
   endDate: string;
   period?: string;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockProductTrend() };
-  }
   return http.request<Result>("get", `/statistics/product/${productId}/trend`, { params });
 };
 
-export const getDeviceOverview = async (params: {
+// ==================== 设备销售统计 ====================
+
+export const getDeviceOverview = (params: {
   startDate: string;
   endDate: string;
   shopId?: number;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockDeviceOverview() };
-  }
   return http.request<Result>("get", "/statistics/device/overview", { params });
 };
 
-export const getDeviceList = async (params: {
+export const getDeviceList = (params: {
   page: number;
   pageSize: number;
   startDate: string;
@@ -148,39 +102,29 @@ export const getDeviceList = async (params: {
   sortBy?: string;
   sortOrder?: string;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockDeviceList(params.page, params.pageSize) };
-  }
   return http.request<ResultTable>("get", "/statistics/device/list", { params });
 };
 
-export const getDeviceTrend = async (deviceId: string, params: {
+export const getDeviceTrend = (deviceId: string, params: {
   startDate: string;
   endDate: string;
   period?: string;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockDeviceTrend() };
-  }
   return http.request<Result>("get", `/statistics/device/${deviceId}/trend`, { params });
 };
 
-export const getShopOverview = async (params: {
+// ==================== 门店销售统计 ====================
+
+export const getShopOverview = (params: {
   startDate: string;
   endDate: string;
   province?: string;
   city?: string;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockShopOverview() };
-  }
   return http.request<Result>("get", "/statistics/shop/overview", { params });
 };
 
-export const getShopList = async (params: {
+export const getShopList = (params: {
   page: number;
   pageSize: number;
   startDate: string;
@@ -193,51 +137,37 @@ export const getShopList = async (params: {
   sortBy?: string;
   sortOrder?: string;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockShopList(params.page, params.pageSize) };
-  }
   return http.request<ResultTable>("get", "/statistics/shop/list", { params });
 };
 
-export const getShopRanking = async (params: {
+export const getShopRanking = (params: {
   startDate: string;
   endDate: string;
   type: string;
   limit?: number;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockShopRanking() };
-  }
   return http.request<Result>("get", "/statistics/shop/ranking", { params });
 };
 
-export const getShopTrend = async (shopId: number, params: {
+export const getShopTrend = (shopId: number, params: {
   startDate: string;
   endDate: string;
   period?: string;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockShopTrend() };
-  }
   return http.request<Result>("get", `/statistics/shop/${shopId}/trend`, { params });
 };
 
-export const getProfitOverview = async (params: {
+// ==================== 门店利润报表 ====================
+
+export const getProfitOverview = (params: {
   startDate: string;
   endDate: string;
   compareType?: string;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockProfitOverview() };
-  }
   return http.request<Result>("get", "/statistics/profit/overview", { params });
 };
 
-export const getProfitList = async (params: {
+export const getProfitList = (params: {
   page: number;
   pageSize: number;
   startDate: string;
@@ -249,78 +179,56 @@ export const getProfitList = async (params: {
   sortBy?: string;
   sortOrder?: string;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockProfitList(params.page, params.pageSize) };
-  }
   return http.request<ResultTable>("get", "/statistics/profit/list", { params });
 };
 
-export const getProfitTrend = async (params: {
+export const getProfitTrend = (params: {
   startDate: string;
   endDate: string;
   shopId?: number;
   period?: string;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockProfitTrend() };
-  }
   return http.request<Result>("get", "/statistics/profit/trend", { params });
 };
 
-export const getProfitWarning = async (params: {
+export const getProfitWarning = (params: {
   startDate: string;
   endDate: string;
   type: string;
   threshold?: number;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockProfitWarning() };
-  }
   return http.request<Result>("get", "/statistics/profit/warning", { params });
 };
 
-export const getRepurchaseOverview = async (params: {
+// ==================== 用户复购统计 ====================
+
+export const getRepurchaseOverview = (params: {
   startDate: string;
   endDate: string;
   shopId?: number;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockRepurchaseOverview() };
-  }
   return http.request<Result>("get", "/statistics/repurchase/overview", { params });
 };
 
-export const getRepurchaseDistribution = async (params: {
+export const getRepurchaseDistribution = (params: {
   startDate: string;
   endDate: string;
   shopId?: number;
   type: string;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockRepurchaseDistribution(params.type) };
-  }
   return http.request<Result>("get", "/statistics/repurchase/distribution", { params });
 };
 
-export const getRepurchaseTrend = async (params: {
+export const getRepurchaseTrend = (params: {
   startDate: string;
   endDate: string;
   shopId?: number;
   period?: string;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockRepurchaseTrend() };
-  }
   return http.request<Result>("get", "/statistics/repurchase/trend", { params });
 };
 
-export const getRepurchaseUsers = async (params: {
+export const getRepurchaseUsers = (params: {
   page: number;
   pageSize: number;
   startDate: string;
@@ -331,13 +239,11 @@ export const getRepurchaseUsers = async (params: {
   sortBy?: string;
   sortOrder?: string;
 }) => {
-  if (USE_MOCK) {
-    await mockDelay();
-    return { code: 200, message: "success", data: mockRepurchaseUsers(params.page, params.pageSize) };
-  }
   return http.request<ResultTable>("get", "/statistics/repurchase/users", { params });
 };
 
+// ==================== 导出 ====================
+
 export const exportStatistics = (params: {
   type: string;
   startDate: string;

+ 0 - 331
haha-admin-web/src/api/statisticsMock.ts

@@ -1,331 +0,0 @@
-const USE_MOCK = true;
-
-const generateDates = (days: number) => {
-  const dates: string[] = [];
-  const today = new Date();
-  for (let i = days - 1; i >= 0; i--) {
-    const date = new Date(today);
-    date.setDate(date.getDate() - i);
-    dates.push(`${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`);
-  }
-  return dates;
-};
-
-const generateRandomData = (length: number, min: number, max: number) => {
-  return Array.from({ length }, () => Math.floor(Math.random() * (max - min + 1)) + min);
-};
-
-export const mockStatisticsOverview = () => {
-  return {
-    totalSales: 1586320.50,
-    totalProfit: 425680.35,
-    avgProfitRate: 26.83,
-    totalOrders: 12580,
-    totalUsers: 8920,
-    totalDevices: 156,
-    onlineDevices: 142,
-    totalShops: 48,
-    avgOrderAmount: 126.10,
-    categoryList: [
-      { category: "饮料", salesAmount: 458620.00, profitAmount: 125680.00, profitRate: 27.40 },
-      { category: "零食", salesAmount: 386540.00, profitAmount: 98560.00, profitRate: 25.50 },
-      { category: "方便食品", salesAmount: 298760.00, profitAmount: 82340.00, profitRate: 27.56 },
-      { category: "乳制品", salesAmount: 186540.00, profitAmount: 52180.00, profitRate: 27.97 },
-      { category: "日用百货", salesAmount: 125680.00, profitAmount: 35620.00, profitRate: 28.35 },
-      { category: "水果", salesAmount: 78920.00, profitAmount: 18650.00, profitRate: 23.63 },
-      { category: "面包糕点", salesAmount: 35260.00, profitAmount: 8670.00, profitRate: 24.59 },
-      { category: "其他", salesAmount: 16000.50, profitAmount: 3980.35, profitRate: 24.88 }
-    ]
-  };
-};
-
-export const mockCategoryOverview = () => {
-  return [
-    { category: "饮料", quantity: 28560, salesAmount: 458620.00, costAmount: 332940.00, profitAmount: 125680.00, profitRate: 27.40, orderCount: 3856, percentage: 28.90 },
-    { category: "零食", quantity: 18920, salesAmount: 386540.00, costAmount: 287980.00, profitAmount: 98560.00, profitRate: 25.50, orderCount: 2980, percentage: 24.37 },
-    { category: "方便食品", quantity: 12450, salesAmount: 298760.00, costAmount: 216420.00, profitAmount: 82340.00, profitRate: 27.56, orderCount: 2156, percentage: 18.84 },
-    { category: "乳制品", quantity: 8960, salesAmount: 186540.00, costAmount: 134360.00, profitAmount: 52180.00, profitRate: 27.97, orderCount: 1680, percentage: 11.76 },
-    { category: "日用百货", quantity: 5680, salesAmount: 125680.00, costAmount: 90060.00, profitAmount: 35620.00, profitRate: 28.35, orderCount: 986, percentage: 7.92 },
-    { category: "水果", quantity: 3560, salesAmount: 78920.00, costAmount: 60270.00, profitAmount: 18650.00, profitRate: 23.63, orderCount: 568, percentage: 4.98 },
-    { category: "面包糕点", quantity: 2180, salesAmount: 35260.00, costAmount: 26590.00, profitAmount: 8670.00, profitRate: 24.59, orderCount: 320, percentage: 2.22 },
-    { category: "其他", quantity: 1250, salesAmount: 16000.50, costAmount: 12020.15, profitAmount: 3980.35, profitRate: 24.88, orderCount: 230, percentage: 1.01 }
-  ];
-};
-
-export const mockCategoryTrend = () => {
-  const dates = generateDates(30);
-  return {
-    dates,
-    series: [
-      { name: "饮料", data: generateRandomData(30, 12000, 18000) },
-      { name: "零食", data: generateRandomData(30, 10000, 15000) },
-      { name: "方便食品", data: generateRandomData(30, 8000, 12000) },
-      { name: "乳制品", data: generateRandomData(30, 5000, 8000) }
-    ]
-  };
-};
-
-export const mockProductList = (page: number, pageSize: number) => {
-  const allProducts = [
-    { productId: 1, productCode: "SP001", productName: "可口可乐500ml", category: "饮料", quantity: 3560, salesAmount: 10680.00, costAmount: 7120.00, profitAmount: 3560.00, profitRate: 33.33, orderCount: 3560, userCount: 2850 },
-    { productId: 2, productCode: "SP002", productName: "百事可乐500ml", category: "饮料", quantity: 2890, salesAmount: 8670.00, costAmount: 5780.00, profitAmount: 2890.00, profitRate: 33.33, orderCount: 2890, userCount: 2310 },
-    { productId: 3, productCode: "SP003", productName: "农夫山泉550ml", category: "饮料", quantity: 4520, salesAmount: 9040.00, costAmount: 6330.00, profitAmount: 2710.00, profitRate: 29.98, orderCount: 4520, userCount: 3620 },
-    { productId: 4, productCode: "SP004", productName: "乐事薯片原味", category: "零食", quantity: 1680, salesAmount: 11760.00, costAmount: 8400.00, profitAmount: 3360.00, profitRate: 28.57, orderCount: 1560, userCount: 1250 },
-    { productId: 5, productCode: "SP005", productName: "康师傅红烧牛肉面", category: "方便食品", quantity: 2340, salesAmount: 11700.00, costAmount: 8190.00, profitAmount: 3510.00, profitRate: 30.00, orderCount: 2100, userCount: 1680 },
-    { productId: 6, productCode: "SP006", productName: "伊利纯牛奶250ml", category: "乳制品", quantity: 3120, salesAmount: 15600.00, costAmount: 10920.00, profitAmount: 4680.00, profitRate: 30.00, orderCount: 2800, userCount: 2240 },
-    { productId: 7, productCode: "SP007", productName: "旺旺雪饼", category: "零食", quantity: 1890, salesAmount: 9450.00, costAmount: 6615.00, profitAmount: 2835.00, profitRate: 30.00, orderCount: 1750, userCount: 1400 },
-    { productId: 8, productCode: "SP008", productName: "统一冰红茶500ml", category: "饮料", quantity: 2680, salesAmount: 8040.00, costAmount: 5360.00, profitAmount: 2680.00, profitRate: 33.33, orderCount: 2680, userCount: 2140 },
-    { productId: 9, productCode: "SP009", productName: "奥利奥饼干", category: "零食", quantity: 1560, salesAmount: 10920.00, costAmount: 7800.00, profitAmount: 3120.00, profitRate: 28.57, orderCount: 1420, userCount: 1140 },
-    { productId: 10, productCode: "SP010", productName: "蒙牛酸酸乳", category: "乳制品", quantity: 2450, salesAmount: 9800.00, costAmount: 6860.00, profitAmount: 2940.00, profitRate: 30.00, orderCount: 2200, userCount: 1760 },
-    { productId: 11, productCode: "SP011", productName: "王老吉凉茶", category: "饮料", quantity: 1980, salesAmount: 9900.00, costAmount: 6930.00, profitAmount: 2970.00, profitRate: 30.00, orderCount: 1850, userCount: 1480 },
-    { productId: 12, productCode: "SP012", productName: "达利园蛋黄派", category: "面包糕点", quantity: 1350, salesAmount: 8100.00, costAmount: 5670.00, profitAmount: 2430.00, profitRate: 30.00, orderCount: 1200, userCount: 960 },
-    { productId: 13, productCode: "SP013", productName: "红牛功能饮料", category: "饮料", quantity: 1680, salesAmount: 13440.00, costAmount: 10080.00, profitAmount: 3360.00, profitRate: 25.00, orderCount: 1520, userCount: 1220 },
-    { productId: 14, productCode: "SP014", productName: "卫龙辣条", category: "零食", quantity: 2890, salesAmount: 8670.00, costAmount: 5780.00, profitAmount: 2890.00, profitRate: 33.33, orderCount: 2680, userCount: 2140 },
-    { productId: 15, productCode: "SP015", productName: "今麦郎方便面", category: "方便食品", quantity: 1860, salesAmount: 7440.00, costAmount: 5210.00, profitAmount: 2230.00, profitRate: 29.97, orderCount: 1680, userCount: 1340 }
-  ];
-  
-  const start = (page - 1) * pageSize;
-  const end = start + pageSize;
-  
-  return {
-    list: allProducts.slice(start, end),
-    total: allProducts.length,
-    pageSize,
-    currentPage: page
-  };
-};
-
-export const mockProductTop = (type: string) => {
-  const products = mockProductList(1, 15).list;
-  if (type === "quantity") {
-    return products.sort((a, b) => b.quantity - a.quantity).slice(0, 10);
-  }
-  return products.sort((a, b) => b.salesAmount - a.salesAmount).slice(0, 10);
-};
-
-export const mockProductTrend = () => {
-  const dates = generateDates(30);
-  return {
-    dates,
-    series: [
-      { name: "销售额", data: generateRandomData(30, 300, 500) }
-    ]
-  };
-};
-
-export const mockDeviceOverview = () => {
-  return {
-    totalDevices: 156,
-    onlineDevices: 142,
-    offlineDevices: 14,
-    onlineRate: "91.0%",
-    totalSales: 1586320.50,
-    totalOrders: 12580
-  };
-};
-
-export const mockDeviceList = (page: number, pageSize: number) => {
-  const allDevices = [
-    { deviceId: "D001", deviceName: "写字楼A座1楼", shopId: 1, shopName: "科技园店", status: 1, statusLabel: "在线", statusColor: "success", orderCount: 856, userCount: 425, salesAmount: 42800.00, avgOrderAmount: 50.00, dailySalesAmount: 1426.67 },
-    { deviceId: "D002", deviceName: "写字楼A座2楼", shopId: 1, shopName: "科技园店", status: 1, statusLabel: "在线", statusColor: "success", orderCount: 723, userCount: 361, salesAmount: 36150.00, avgOrderAmount: 50.00, dailySalesAmount: 1205.00 },
-    { deviceId: "D003", deviceName: "地铁站C出口", shopId: 2, shopName: "地铁商圈店", status: 1, statusLabel: "在线", statusColor: "success", orderCount: 1256, userCount: 628, salesAmount: 62800.00, avgOrderAmount: 50.00, dailySalesAmount: 2093.33 },
-    { deviceId: "D004", deviceName: "商场B区2楼", shopId: 2, shopName: "地铁商圈店", status: 1, statusLabel: "在线", statusColor: "success", orderCount: 1580, userCount: 790, salesAmount: 79000.00, avgOrderAmount: 50.00, dailySalesAmount: 2633.33 },
-    { deviceId: "D005", deviceName: "医院门诊楼", shopId: 3, shopName: "医院店", status: 1, statusLabel: "在线", statusColor: "success", orderCount: 645, userCount: 322, salesAmount: 32250.00, avgOrderAmount: 50.00, dailySalesAmount: 1075.00 },
-    { deviceId: "D006", deviceName: "体育馆入口", shopId: 4, shopName: "体育馆店", status: 0, statusLabel: "离线", statusColor: "info", orderCount: 0, userCount: 0, salesAmount: 0, avgOrderAmount: 0, dailySalesAmount: 0 },
-    { deviceId: "D007", deviceName: "公园东门", shopId: 5, shopName: "公园店", status: 1, statusLabel: "在线", statusColor: "success", orderCount: 423, userCount: 211, salesAmount: 21150.00, avgOrderAmount: 50.00, dailySalesAmount: 705.00 },
-    { deviceId: "D008", deviceName: "图书馆大厅", shopId: 6, shopName: "图书馆店", status: 1, statusLabel: "在线", statusColor: "success", orderCount: 512, userCount: 256, salesAmount: 25600.00, avgOrderAmount: 50.00, dailySalesAmount: 853.33 },
-    { deviceId: "D009", deviceName: "火车站候车室", shopId: 7, shopName: "火车站店", status: 1, statusLabel: "在线", statusColor: "success", orderCount: 1890, userCount: 945, salesAmount: 94500.00, avgOrderAmount: 50.00, dailySalesAmount: 3150.00 },
-    { deviceId: "D010", deviceName: "机场T1航站楼", shopId: 8, shopName: "机场店", status: 1, statusLabel: "在线", statusColor: "success", orderCount: 2156, userCount: 1078, salesAmount: 107800.00, avgOrderAmount: 50.00, dailySalesAmount: 3593.33 }
-  ];
-  
-  const start = (page - 1) * pageSize;
-  const end = start + pageSize;
-  
-  return {
-    list: allDevices.slice(start, end),
-    total: allDevices.length,
-    pageSize,
-    currentPage: page
-  };
-};
-
-export const mockDeviceTrend = () => {
-  const dates = generateDates(30);
-  return {
-    dates,
-    series: [
-      { name: "销售额", data: generateRandomData(30, 1000, 3000) }
-    ]
-  };
-};
-
-export const mockShopOverview = () => {
-  return {
-    totalShops: 48,
-    activeShops: 45,
-    totalSales: 1586320.50,
-    totalOrders: 12580
-  };
-};
-
-export const mockShopList = (page: number, pageSize: number) => {
-  const allShops = [
-    { shopId: 1, shopName: "科技园店", province: "广东省", city: "深圳市", district: "南山区", address: "科技园路88号", status: 1, statusLabel: "启用", deviceCount: 12, orderCount: 2856, userCount: 1428, salesAmount: 142800.00, avgOrderAmount: 50.00, dailySalesAmount: 4760.00, deviceOutput: 11900.00 },
-    { shopId: 2, shopName: "地铁商圈店", province: "广东省", city: "深圳市", district: "福田区", address: "福田地铁站B出口", status: 1, statusLabel: "启用", deviceCount: 8, orderCount: 1890, userCount: 945, salesAmount: 94500.00, avgOrderAmount: 50.00, dailySalesAmount: 3150.00, deviceOutput: 11812.50 },
-    { shopId: 3, shopName: "大学城店", province: "广东省", city: "深圳市", district: "南山区", address: "大学城学府路", status: 1, statusLabel: "启用", deviceCount: 15, orderCount: 3256, userCount: 1628, salesAmount: 162800.00, avgOrderAmount: 50.00, dailySalesAmount: 5426.67, deviceOutput: 10853.33 },
-    { shopId: 4, shopName: "医院店", province: "广东省", city: "深圳市", district: "福田区", address: "福田医院门诊楼", status: 1, statusLabel: "启用", deviceCount: 6, orderCount: 1256, userCount: 628, salesAmount: 62800.00, avgOrderAmount: 50.00, dailySalesAmount: 2093.33, deviceOutput: 10466.67 },
-    { shopId: 5, shopName: "体育馆店", province: "广东省", city: "深圳市", district: "龙岗区", address: "大运体育馆", status: 1, statusLabel: "启用", deviceCount: 10, orderCount: 1890, userCount: 945, salesAmount: 94500.00, avgOrderAmount: 50.00, dailySalesAmount: 3150.00, deviceOutput: 9450.00 },
-    { shopId: 6, shopName: "公园店", province: "广东省", city: "深圳市", district: "南山区", address: "深圳湾公园", status: 1, statusLabel: "启用", deviceCount: 5, orderCount: 856, userCount: 428, salesAmount: 42800.00, avgOrderAmount: 50.00, dailySalesAmount: 1426.67, deviceOutput: 8560.00 },
-    { shopId: 7, shopName: "图书馆店", province: "广东省", city: "深圳市", district: "福田区", address: "深圳图书馆", status: 1, statusLabel: "启用", deviceCount: 4, orderCount: 723, userCount: 361, salesAmount: 36150.00, avgOrderAmount: 50.00, dailySalesAmount: 1205.00, deviceOutput: 9037.50 },
-    { shopId: 8, shopName: "火车站店", province: "广东省", city: "深圳市", district: "罗湖区", address: "深圳火车站", status: 1, statusLabel: "启用", deviceCount: 18, orderCount: 4520, userCount: 2260, salesAmount: 226000.00, avgOrderAmount: 50.00, dailySalesAmount: 7533.33, deviceOutput: 12555.56 },
-    { shopId: 9, shopName: "机场店", province: "广东省", city: "深圳市", district: "宝安区", address: "宝安国际机场", status: 1, statusLabel: "启用", deviceCount: 20, orderCount: 5680, userCount: 2840, salesAmount: 284000.00, avgOrderAmount: 50.00, dailySalesAmount: 9466.67, deviceOutput: 14200.00 },
-    { shopId: 10, shopName: "购物中心店", province: "广东省", city: "深圳市", district: "福田区", address: "COCO Park购物中心", status: 1, statusLabel: "启用", deviceCount: 14, orderCount: 3560, userCount: 1780, salesAmount: 178000.00, avgOrderAmount: 50.00, dailySalesAmount: 5933.33, deviceOutput: 12714.29 }
-  ];
-  
-  const start = (page - 1) * pageSize;
-  const end = start + pageSize;
-  
-  return {
-    list: allShops.slice(start, end),
-    total: allShops.length,
-    pageSize,
-    currentPage: page
-  };
-};
-
-export const mockShopRanking = () => {
-  return mockShopList(1, 10).list.sort((a, b) => b.salesAmount - a.salesAmount).slice(0, 10);
-};
-
-export const mockShopTrend = () => {
-  const dates = generateDates(30);
-  return {
-    dates,
-    series: [
-      { name: "销售额", data: generateRandomData(30, 4000, 8000) }
-    ]
-  };
-};
-
-export const mockProfitOverview = () => {
-  return {
-    salesAmount: 1586320.50,
-    costAmount: 1160640.15,
-    grossProfit: 425680.35,
-    grossProfitRate: 26.83
-  };
-};
-
-export const mockProfitList = (page: number, pageSize: number) => {
-  const allProfits = [
-    { shopId: 1, shopName: "科技园店", province: "广东省", city: "深圳市", district: "南山区", deviceCount: 12, salesAmount: 142800.00, costAmount: 104256.00, grossProfit: 38544.00, grossProfitRate: 27.00, refundAmount: 0, refundRate: 0, netProfit: 38544.00, netProfitRate: 27.00, deviceProfit: 3212.00, dailyProfit: 1284.80 },
-    { shopId: 2, shopName: "地铁商圈店", province: "广东省", city: "深圳市", district: "福田区", deviceCount: 8, salesAmount: 94500.00, costAmount: 69045.00, grossProfit: 25455.00, grossProfitRate: 26.94, refundAmount: 0, refundRate: 0, netProfit: 25455.00, netProfitRate: 26.94, deviceProfit: 3181.88, dailyProfit: 848.50 },
-    { shopId: 3, shopName: "大学城店", province: "广东省", city: "深圳市", district: "南山区", deviceCount: 15, salesAmount: 162800.00, costAmount: 118844.00, grossProfit: 43956.00, grossProfitRate: 27.00, refundAmount: 0, refundRate: 0, netProfit: 43956.00, netProfitRate: 27.00, deviceProfit: 2930.40, dailyProfit: 1465.20 },
-    { shopId: 4, shopName: "医院店", province: "广东省", city: "深圳市", district: "福田区", deviceCount: 6, salesAmount: 62800.00, costAmount: 45844.00, grossProfit: 16956.00, grossProfitRate: 27.00, refundAmount: 0, refundRate: 0, netProfit: 16956.00, netProfitRate: 27.00, deviceProfit: 2826.00, dailyProfit: 565.20 },
-    { shopId: 5, shopName: "体育馆店", province: "广东省", city: "深圳市", district: "龙岗区", deviceCount: 10, salesAmount: 94500.00, costAmount: 69930.00, grossProfit: 24570.00, grossProfitRate: 26.00, refundAmount: 0, refundRate: 0, netProfit: 24570.00, netProfitRate: 26.00, deviceProfit: 2457.00, dailyProfit: 819.00 },
-    { shopId: 6, shopName: "公园店", province: "广东省", city: "深圳市", district: "南山区", deviceCount: 5, salesAmount: 42800.00, costAmount: 31458.00, grossProfit: 11342.00, grossProfitRate: 26.50, refundAmount: 0, refundRate: 0, netProfit: 11342.00, netProfitRate: 26.50, deviceProfit: 2268.40, dailyProfit: 378.07 },
-    { shopId: 7, shopName: "图书馆店", province: "广东省", city: "深圳市", district: "福田区", deviceCount: 4, salesAmount: 36150.00, costAmount: 26510.00, grossProfit: 9640.00, grossProfitRate: 26.67, refundAmount: 0, refundRate: 0, netProfit: 9640.00, netProfitRate: 26.67, deviceProfit: 2410.00, dailyProfit: 321.33 },
-    { shopId: 8, shopName: "火车站店", province: "广东省", city: "深圳市", district: "罗湖区", deviceCount: 18, salesAmount: 226000.00, costAmount: 165180.00, grossProfit: 60820.00, grossProfitRate: 26.91, refundAmount: 0, refundRate: 0, netProfit: 60820.00, netProfitRate: 26.91, deviceProfit: 3378.89, dailyProfit: 2027.33 },
-    { shopId: 9, shopName: "机场店", province: "广东省", city: "深圳市", district: "宝安区", deviceCount: 20, salesAmount: 284000.00, costAmount: 207320.00, grossProfit: 76680.00, grossProfitRate: 27.00, refundAmount: 0, refundRate: 0, netProfit: 76680.00, netProfitRate: 27.00, deviceProfit: 3834.00, dailyProfit: 2556.00 },
-    { shopId: 10, shopName: "购物中心店", province: "广东省", city: "深圳市", district: "福田区", deviceCount: 14, salesAmount: 178000.00, costAmount: 130094.00, grossProfit: 47906.00, grossProfitRate: 26.91, refundAmount: 0, refundRate: 0, netProfit: 47906.00, netProfitRate: 26.91, deviceProfit: 3421.86, dailyProfit: 1596.87 }
-  ];
-  
-  const start = (page - 1) * pageSize;
-  const end = start + pageSize;
-  
-  return {
-    list: allProfits.slice(start, end),
-    total: allProfits.length,
-    pageSize,
-    currentPage: page
-  };
-};
-
-export const mockProfitTrend = () => {
-  const dates = generateDates(30);
-  return {
-    dates,
-    series: [
-      { name: "利润", data: generateRandomData(30, 10000, 20000) }
-    ]
-  };
-};
-
-export const mockProfitWarning = () => {
-  return [
-    { shopId: 11, shopName: "测试门店A", province: "广东省", city: "深圳市", district: "南山区", deviceCount: 2, salesAmount: 8500.00, costAmount: 7650.00, grossProfit: 850.00, grossProfitRate: 10.00, netProfit: 850.00, netProfitRate: 10.00 },
-    { shopId: 12, shopName: "测试门店B", province: "广东省", city: "深圳市", district: "福田区", deviceCount: 1, salesAmount: 3200.00, costAmount: 3040.00, grossProfit: 160.00, grossProfitRate: 5.00, netProfit: 160.00, netProfitRate: 5.00 }
-  ];
-};
-
-export const mockRepurchaseOverview = () => {
-  return {
-    totalUsers: 8920,
-    newUsers: 3568,
-    repurchaseUsers: 5352,
-    repurchaseRate: 60.00,
-    avgOrderAmount: 126.10,
-    avgPurchaseCount: 1.41,
-    ltv: 177.80
-  };
-};
-
-export const mockRepurchaseDistribution = (type: string) => {
-  if (type === "interval") {
-    return {
-      distribution: {
-        "1天内": 1256,
-        "1-3天": 1890,
-        "3-7天": 2340,
-        "7-14天": 1560,
-        "14-30天": 980,
-        "30天以上": 520
-      }
-    };
-  }
-  return {
-    distribution: {
-      "新用户": 3568,
-      "活跃用户": 2856,
-      "忠诚用户": 1890,
-      "流失用户": 606
-    }
-  };
-};
-
-export const mockRepurchaseTrend = () => {
-  const dates = generateDates(30);
-  return {
-    dates,
-    series: [
-      { name: "复购率(%)", data: generateRandomData(30, 55, 75) }
-    ]
-  };
-};
-
-export const mockRepurchaseUsers = (page: number, pageSize: number) => {
-  const allUsers = [
-    { userId: 1, nickname: "张三", phone: "138****1234", orderCount: 15, totalAmount: 2250.00, avgOrderAmount: 150.00, firstOrderDate: "2025-01-15", lastOrderDate: "2025-03-24", repurchaseDays: 5, userLayer: "loyal", userLayerLabel: "忠诚用户" },
-    { userId: 2, nickname: "李四", phone: "139****5678", orderCount: 8, totalAmount: 960.00, avgOrderAmount: 120.00, firstOrderDate: "2025-02-01", lastOrderDate: "2025-03-23", repurchaseDays: 7, userLayer: "active", userLayerLabel: "活跃用户" },
-    { userId: 3, nickname: "王五", phone: "137****9012", orderCount: 12, totalAmount: 1560.00, avgOrderAmount: 130.00, firstOrderDate: "2025-01-20", lastOrderDate: "2025-03-22", repurchaseDays: 6, userLayer: "loyal", userLayerLabel: "忠诚用户" },
-    { userId: 4, nickname: "赵六", phone: "136****3456", orderCount: 3, totalAmount: 360.00, avgOrderAmount: 120.00, firstOrderDate: "2025-03-10", lastOrderDate: "2025-03-24", repurchaseDays: 7, userLayer: "active", userLayerLabel: "活跃用户" },
-    { userId: 5, nickname: "钱七", phone: "135****7890", orderCount: 1, totalAmount: 85.00, avgOrderAmount: 85.00, firstOrderDate: "2025-03-24", lastOrderDate: "2025-03-24", repurchaseDays: 0, userLayer: "new", userLayerLabel: "新用户" },
-    { userId: 6, nickname: "孙八", phone: "134****1234", orderCount: 6, totalAmount: 720.00, avgOrderAmount: 120.00, firstOrderDate: "2024-12-15", lastOrderDate: "2025-01-10", repurchaseDays: 5, userLayer: "churn", userLayerLabel: "流失用户" },
-    { userId: 7, nickname: "周九", phone: "133****5678", orderCount: 10, totalAmount: 1250.00, avgOrderAmount: 125.00, firstOrderDate: "2025-02-05", lastOrderDate: "2025-03-21", repurchaseDays: 8, userLayer: "active", userLayerLabel: "活跃用户" },
-    { userId: 8, nickname: "吴十", phone: "132****9012", orderCount: 20, totalAmount: 3200.00, avgOrderAmount: 160.00, firstOrderDate: "2025-01-01", lastOrderDate: "2025-03-24", repurchaseDays: 4, userLayer: "loyal", userLayerLabel: "忠诚用户" },
-    { userId: 9, nickname: "郑十一", phone: "131****3456", orderCount: 2, totalAmount: 240.00, avgOrderAmount: 120.00, firstOrderDate: "2025-03-20", lastOrderDate: "2025-03-24", repurchaseDays: 4, userLayer: "active", userLayerLabel: "活跃用户" },
-    { userId: 10, nickname: "王十二", phone: "130****7890", orderCount: 5, totalAmount: 650.00, avgOrderAmount: 130.00, firstOrderDate: "2025-02-28", lastOrderDate: "2025-03-23", repurchaseDays: 6, userLayer: "active", userLayerLabel: "活跃用户" }
-  ];
-  
-  const start = (page - 1) * pageSize;
-  const end = start + pageSize;
-  
-  return {
-    list: allUsers.slice(start, end),
-    total: allUsers.length,
-    pageSize,
-    currentPage: page
-  };
-};
-
-export { USE_MOCK };

+ 6 - 3
haha-admin-web/src/router/modules/statistics.ts

@@ -28,7 +28,8 @@ export default {
       name: "ProductStatistics",
       component: () => import("@/views/statistics/product/index.vue"),
       meta: {
-        title: "商品销售统计"
+        title: "商品销售统计",
+        showLink: false
       }
     },
     {
@@ -36,7 +37,8 @@ export default {
       name: "DeviceStatistics",
       component: () => import("@/views/statistics/device/index.vue"),
       meta: {
-        title: "设备销售统计"
+        title: "设备销售统计",
+        showLink: false
       }
     },
     {
@@ -44,7 +46,8 @@ export default {
       name: "ShopStatistics",
       component: () => import("@/views/statistics/shop/index.vue"),
       meta: {
-        title: "门店销售统计"
+        title: "门店销售统计",
+        showLink: false
       }
     },
     {

+ 240 - 359
haha-admin-web/src/views/statistics/category/index.vue

@@ -1,23 +1,49 @@
 <script setup lang="ts">
-import { ref, onMounted } from "vue";
+import { ref, onMounted, onUnmounted, computed } from "vue";
 import * as echarts from "echarts";
 import { getCategoryOverview, getCategoryTrend } from "@/api/statistics";
+import type { EChartsOption } from "echarts";
 
 defineOptions({
   name: "CategoryStatistics"
 });
 
+interface CategoryStat {
+  category: string;
+  quantity: number;
+  salesAmount: number;
+  costAmount: number;
+  profitAmount: number;
+  profitRate: number;
+  orderCount: number;
+  percentage: number;
+}
+
 const loading = ref(false);
 const dateRange = ref<[Date, Date]>([
   new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
   new Date()
 ]);
+const activeShortcut = ref("30d");
 
-const categoryList = ref([]);
-const totalSales = ref(0);
-const totalProfit = ref(0);
-const avgProfitRate = ref(0);
+const shortcuts = [
+  { key: "today", label: "今日" },
+  { key: "week", label: "本周" },
+  { key: "month", label: "本月" },
+  { key: "30d", label: "近30天" }
+];
 
+const categoryList = ref<CategoryStat[]>([]);
+const selectedCategory = ref("");
+
+const totalSales = computed(() => categoryList.value.reduce((s, i) => s + (i.salesAmount || 0), 0));
+const totalProfit = computed(() => categoryList.value.reduce((s, i) => s + (i.profitAmount || 0), 0));
+const avgProfitRate = computed(() => {
+  if (totalSales.value === 0) return 0;
+  return Number(((totalProfit.value / totalSales.value) * 100).toFixed(2));
+});
+
+// 图表引用
 const pieChartRef = ref<HTMLElement | null>(null);
 const barChartRef = ref<HTMLElement | null>(null);
 const trendChartRef = ref<HTMLElement | null>(null);
@@ -25,25 +51,35 @@ let pieChart: echarts.ECharts | null = null;
 let barChart: echarts.ECharts | null = null;
 let trendChart: echarts.ECharts | null = null;
 
+function formatDate(date: Date): string {
+  return date.toISOString().split("T")[0];
+}
+
+function setDateShortcut(type: string) {
+  activeShortcut.value = type;
+  const today = new Date();
+  today.setHours(23, 59, 59, 999);
+  let start: Date;
+  switch (type) {
+    case "today": start = new Date(); start.setHours(0, 0, 0, 0); break;
+    case "week": start = new Date(today); start.setDate(today.getDate() - 6); start.setHours(0, 0, 0, 0); break;
+    case "month": start = new Date(today.getFullYear(), today.getMonth(), 1); break;
+    case "30d": default: start = new Date(today); start.setDate(today.getDate() - 29); start.setHours(0, 0, 0, 0); break;
+  }
+  dateRange.value = [start, today];
+  fetchData();
+}
+
 async function fetchData() {
   loading.value = true;
   try {
-    const params = {
-      startDate: formatDate(dateRange.value[0]),
-      endDate: formatDate(dateRange.value[1])
-    };
-    
-    const { data } = await getCategoryOverview(params);
-    categoryList.value = data;
-    
-    totalSales.value = data.reduce((sum: number, item: any) => sum + (item.salesAmount || 0), 0);
-    totalProfit.value = data.reduce((sum: number, item: any) => sum + (item.profitAmount || 0), 0);
-    avgProfitRate.value = totalSales.value > 0 
-      ? Number((totalProfit.value / totalSales.value * 100).toFixed(2))
-      : 0;
-    
+    const params = { startDate: formatDate(dateRange.value[0]), endDate: formatDate(dateRange.value[1]) };
+    const res = await getCategoryOverview(params);
+    if (res.code === 200 && res.data) {
+      categoryList.value = res.data;
+    }
     initCharts();
-    await fetchTrendData();
+    fetchTrendData();
   } catch (error) {
     console.error("获取品类统计数据失败:", error);
   } finally {
@@ -53,205 +89,93 @@ async function fetchData() {
 
 async function fetchTrendData() {
   try {
-    const params = {
+    const params: any = {
       startDate: formatDate(dateRange.value[0]),
       endDate: formatDate(dateRange.value[1]),
       period: "day"
     };
-    
-    const { data } = await getCategoryTrend(params);
-    initTrendChart(data);
+    if (selectedCategory.value) params.category = selectedCategory.value;
+    const res = await getCategoryTrend(params);
+    if (res.code === 200 && res.data) {
+      initTrendChart(res.data);
+    }
   } catch (error) {
     console.error("获取品类趋势数据失败:", error);
   }
 }
 
-function formatDate(date: Date): string {
-  return date.toISOString().split("T")[0];
+function handleCategoryClick(category: string) {
+  selectedCategory.value = selectedCategory.value === category ? "" : category;
+  fetchTrendData();
 }
 
+function handleDateChange() {
+  activeShortcut.value = "";
+  fetchData();
+}
+
+// ==================== 图表 ====================
 function initCharts() {
-  setTimeout(() => {
-    initPieChart();
-    initBarChart();
-  }, 100);
+  setTimeout(() => { initPieChart(); initBarChart(); }, 100);
 }
 
+const COLORS = ["#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de", "#3ba272", "#fc8452", "#9a60b4"];
+
 function initPieChart() {
   if (!pieChartRef.value) return;
-  
-  if (pieChart) {
-    pieChart.dispose();
-  }
-  
+  pieChart?.dispose();
   pieChart = echarts.init(pieChartRef.value);
-  
-  const option: echarts.EChartsOption = {
-    title: {
-      text: "品类销售额占比",
-      left: "center",
-      textStyle: { fontSize: 14, fontWeight: "normal" }
-    },
-    tooltip: {
-      trigger: "item",
-      formatter: "{a} <br/>{b}: ¥{c} ({d}%)"
-    },
-    legend: {
-      orient: "vertical",
-      left: "left",
-      top: "middle"
-    },
-    series: [
-      {
-        name: "销售额",
-        type: "pie",
-        radius: ["40%", "70%"],
-        center: ["60%", "50%"],
-        avoidLabelOverlap: false,
-        itemStyle: {
-          borderRadius: 10,
-          borderColor: "#fff",
-          borderWidth: 2
-        },
-        label: {
-          show: false,
-          position: "center"
-        },
-        emphasis: {
-          label: {
-            show: true,
-            fontSize: 16,
-            fontWeight: "bold"
-          }
-        },
-        labelLine: {
-          show: false
-        },
-        data: categoryList.value.slice(0, 8).map((item: any) => ({
-          value: item.salesAmount,
-          name: item.category
-        }))
-      }
-    ]
+  const option: EChartsOption = {
+    title: { text: "品类销售额占比", left: "center", top: 10, textStyle: { fontSize: 14, fontWeight: "bold" } },
+    tooltip: { trigger: "item", formatter: (p: any) => `${p.name}: ¥${p.value?.toLocaleString() || 0} (${p.percent}%)` },
+    legend: { orient: "vertical", left: 10, top: "middle", itemWidth: 12, itemHeight: 12 },
+    series: [{
+      type: "pie", radius: ["45%", "72%"], center: ["58%", "55%"],
+      avoidLabelOverlap: false, itemStyle: { borderRadius: 6, borderColor: "#fff", borderWidth: 2 },
+      label: { show: false }, emphasis: { label: { show: true, fontSize: 16, fontWeight: "bold" } }, labelLine: { show: false },
+      data: categoryList.value.slice(0, 8).map((item, i) => ({
+        value: item.salesAmount, name: item.category, itemStyle: { color: COLORS[i % COLORS.length] }
+      }))
+    }]
   };
-  
   pieChart.setOption(option);
 }
 
 function initBarChart() {
   if (!barChartRef.value) return;
-  
-  if (barChart) {
-    barChart.dispose();
-  }
-  
+  barChart?.dispose();
   barChart = echarts.init(barChartRef.value);
-  
-  const categories = categoryList.value.slice(0, 10);
-  
-  const option: echarts.EChartsOption = {
-    title: {
-      text: "品类利润对比",
-      left: "center",
-      textStyle: { fontSize: 14, fontWeight: "normal" }
-    },
-    tooltip: {
-      trigger: "axis",
-      axisPointer: { type: "shadow" }
-    },
-    grid: {
-      left: "3%",
-      right: "4%",
-      bottom: "3%",
-      containLabel: true
-    },
-    xAxis: {
-      type: "category",
-      data: categories.map((item: any) => item.category),
-      axisLabel: {
-        rotate: 30
-      }
-    },
-    yAxis: {
-      type: "value",
-      name: "利润(元)"
-    },
-    series: [
-      {
-        name: "利润额",
-        type: "bar",
-        data: categories.map((item: any) => item.profitAmount),
-        itemStyle: {
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: "#83bff6" },
-            { offset: 0.5, color: "#188df0" },
-            { offset: 1, color: "#188df0" }
-          ])
-        }
-      }
-    ]
+  const list = [...categoryList.value].sort((a, b) => (b.profitAmount || 0) - (a.profitAmount || 0)).slice(0, 10);
+  const option: EChartsOption = {
+    title: { text: "品类利润对比 (TOP10)", left: "center", top: 10, textStyle: { fontSize: 14, fontWeight: "bold" } },
+    tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, formatter: (p: any) => `${p[0].name}<br/>利润: ¥${p[0].value?.toLocaleString()}` },
+    grid: { left: "3%", right: "8%", bottom: "3%", top: "15%", containLabel: true },
+    xAxis: { type: "category", data: list.map(i => i.category), axisLabel: { rotate: 30, fontSize: 11 } },
+    yAxis: { type: "value", name: "利润(元)", axisLabel: { formatter: (v: number) => v >= 10000 ? (v / 10000).toFixed(1) + "万" : String(v) } },
+    series: [{ name: "利润额", type: "bar", data: list.map(i => i.profitAmount),
+      itemStyle: { borderRadius: [4, 4, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: "#91cc75" }, { offset: 1, color: "#3ba272" }]) }
+    }]
   };
-  
   barChart.setOption(option);
 }
 
 function initTrendChart(data: any) {
   if (!trendChartRef.value) return;
-  
-  if (trendChart) {
-    trendChart.dispose();
-  }
-  
+  trendChart?.dispose();
   trendChart = echarts.init(trendChartRef.value);
-  
-  const option: echarts.EChartsOption = {
-    title: {
-      text: "品类销售趋势",
-      left: "center",
-      textStyle: { fontSize: 14, fontWeight: "normal" }
-    },
-    tooltip: {
-      trigger: "axis",
-      backgroundColor: "rgba(255, 255, 255, 0.9)",
-      borderColor: "#e5e7eb",
-      borderWidth: 1
-    },
-    legend: {
-      data: data.series?.map((s: any) => s.name) || [],
-      bottom: 0
-    },
-    grid: {
-      left: "3%",
-      right: "4%",
-      bottom: "15%",
-      containLabel: true
-    },
-    xAxis: {
-      type: "category",
-      data: data.dates || [],
-      axisLabel: {
-        rotate: 45
-      }
-    },
-    yAxis: {
-      type: "value",
-      name: "销售额(元)"
-    },
-    series: (data.series || []).map((s: any) => ({
-      name: s.name,
-      type: "line",
-      smooth: true,
-      data: s.data
-    }))
+  const series = (data.series || []).map((s: any) => ({ name: s.name, type: "line" as const, smooth: true, data: s.data, symbol: "circle", symbolSize: 4 }));
+  const option: EChartsOption = {
+    title: { text: selectedCategory.value ? `${selectedCategory.value} 销售趋势` : "品类销售趋势", left: "center", top: 10, textStyle: { fontSize: 14, fontWeight: "bold" } },
+    tooltip: { trigger: "axis", backgroundColor: "#fff", borderColor: "#e5e7eb", textStyle: { color: "#333" } },
+    legend: { data: series.map(s => s.name), bottom: 0 },
+    grid: { left: "3%", right: "4%", bottom: "12%", top: "15%", containLabel: true },
+    xAxis: { type: "category", data: data.dates || [], axisLabel: { rotate: 45, fontSize: 11 } },
+    yAxis: { type: "value", name: "销售额(元)", axisLabel: { formatter: (v: number) => v >= 10000 ? (v / 10000).toFixed(1) + "万" : String(v) } },
+    series
   };
-  
   trendChart.setOption(option);
 }
 
-function handleDateChange() {
-  fetchData();
-}
-
 function resizeCharts() {
   pieChart?.resize();
   barChart?.resize();
@@ -262,200 +186,157 @@ onMounted(() => {
   fetchData();
   window.addEventListener("resize", resizeCharts);
 });
+
+onUnmounted(() => {
+  window.removeEventListener("resize", resizeCharts);
+  pieChart?.dispose();
+  barChart?.dispose();
+  trendChart?.dispose();
+});
 </script>
 
 <template>
   <div class="category-statistics">
-    <el-card shadow="never" class="mb-4">
-      <div class="flex items-center justify-between">
-        <span class="text-lg font-medium">品类销售统计</span>
-        <el-date-picker
-          v-model="dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          format="YYYY-MM-DD"
-          value-format="YYYY-MM-DD"
-          @change="handleDateChange"
-        />
+    <!-- 工具栏 -->
+    <div class="toolbar">
+      <h2 class="page-title">品类销售统计</h2>
+      <div class="toolbar-right">
+        <div class="date-shortcuts">
+          <el-button v-for="s in shortcuts" :key="s.key" :type="activeShortcut === s.key ? 'primary' : 'default'" size="small" @click="setDateShortcut(s.key)">{{ s.label }}</el-button>
+        </div>
+        <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" size="small" @change="handleDateChange" />
       </div>
-    </el-card>
-
-    <el-row :gutter="20" class="mb-6">
-      <el-col :span="8">
-        <el-card shadow="hover" class="stat-card-wrapper">
-          <div class="stat-card">
-            <div class="stat-icon bg-blue">
-              <i class="ri:money-cny-circle-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">¥{{ totalSales.toLocaleString() }}</div>
-              <div class="stat-label">总销售额</div>
-            </div>
+    </div>
+
+    <div v-loading="loading" class="page-body">
+      <!-- KPI 卡片 -->
+      <el-row :gutter="16" class="kpi-row">
+        <el-col :span="8">
+          <div class="kpi-card kpi-blue">
+            <div class="kpi-label">总销售额</div>
+            <div class="kpi-value">¥{{ totalSales.toLocaleString() }}</div>
           </div>
-        </el-card>
-      </el-col>
-      <el-col :span="8">
-        <el-card shadow="hover" class="stat-card-wrapper">
-          <div class="stat-card">
-            <div class="stat-icon bg-green">
-              <i class="ri:profit-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">¥{{ totalProfit.toLocaleString() }}</div>
-              <div class="stat-label">总利润</div>
-            </div>
+        </el-col>
+        <el-col :span="8">
+          <div class="kpi-card kpi-green">
+            <div class="kpi-label">总利润</div>
+            <div class="kpi-value">¥{{ totalProfit.toLocaleString() }}</div>
           </div>
-        </el-card>
-      </el-col>
-      <el-col :span="8">
-        <el-card shadow="hover" class="stat-card-wrapper">
-          <div class="stat-card">
-            <div class="stat-icon bg-orange">
-              <i class="ri:percent-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">{{ avgProfitRate }}%</div>
-              <div class="stat-label">平均利润率</div>
-            </div>
+        </el-col>
+        <el-col :span="8">
+          <div class="kpi-card kpi-orange">
+            <div class="kpi-label">平均利润率</div>
+            <div class="kpi-value">{{ avgProfitRate }}%</div>
           </div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <el-row :gutter="20" class="mb-6">
-      <el-col :span="12">
-        <el-card shadow="hover" class="chart-card">
-          <div ref="pieChartRef" class="chart-container"></div>
-        </el-card>
-      </el-col>
-      <el-col :span="12">
-        <el-card shadow="hover" class="chart-card">
-          <div ref="barChartRef" class="chart-container"></div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <el-card shadow="hover" class="chart-card mb-6">
-      <div ref="trendChartRef" class="chart-container" style="height: 350px;"></div>
-    </el-card>
-
-    <el-card shadow="never">
-      <template #header>
-        <span class="font-medium">品类销售明细</span>
-      </template>
-      <el-table :data="categoryList" stripe v-loading="loading">
-        <el-table-column prop="category" label="品类名称" min-width="120" />
-        <el-table-column prop="quantity" label="销售数量" width="100" align="right">
-          <template #default="{ row }">
-            {{ row.quantity?.toLocaleString() || 0 }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="salesAmount" label="销售额(元)" width="120" align="right">
-          <template #default="{ row }">
-            ¥{{ row.salesAmount?.toLocaleString() || 0 }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="costAmount" label="成本额(元)" width="120" align="right">
-          <template #default="{ row }">
-            ¥{{ row.costAmount?.toLocaleString() || 0 }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="profitAmount" label="利润额(元)" width="120" align="right">
-          <template #default="{ row }">
-            <span :class="row.profitAmount >= 0 ? 'text-green-500' : 'text-red-500'">
-              ¥{{ row.profitAmount?.toLocaleString() || 0 }}
-            </span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="profitRate" label="利润率" width="100" align="right">
-          <template #default="{ row }">
-            <span :class="row.profitRate >= 0 ? 'text-green-500' : 'text-red-500'">
-              {{ row.profitRate }}%
-            </span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="orderCount" label="订单数" width="100" align="right">
-          <template #default="{ row }">
-            {{ row.orderCount?.toLocaleString() || 0 }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="percentage" label="销售占比" width="100" align="right">
-          <template #default="{ row }">
-            {{ row.percentage }}%
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-card>
+        </el-col>
+      </el-row>
+
+      <!-- 图表区 -->
+      <el-row :gutter="16" class="chart-row">
+        <el-col :span="12">
+          <div class="chart-box"><div ref="pieChartRef" class="chart-container"></div></div>
+        </el-col>
+        <el-col :span="12">
+          <div class="chart-box"><div ref="barChartRef" class="chart-container"></div></div>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="16" class="chart-row">
+        <el-col :span="24">
+          <div class="chart-box chart-box-tall"><div ref="trendChartRef" class="chart-container"></div></div>
+        </el-col>
+      </el-row>
+
+      <!-- 明细表格 -->
+      <div class="table-box">
+        <div class="table-header">
+          <span class="table-title">品类销售明细</span>
+          <span class="table-hint" v-if="selectedCategory">当前选中: <el-tag size="small" closable @close="handleCategoryClick(selectedCategory)">{{ selectedCategory }}</el-tag></span>
+        </div>
+        <el-table :data="categoryList" stripe v-loading="loading" @row-click="(row: any) => handleCategoryClick(row.category)" style="cursor: pointer;">
+          <el-table-column prop="category" label="品类名称" min-width="120" fixed="left">
+            <template #default="{ row }">
+              <el-button type="primary" link size="small">{{ row.category }}</el-button>
+            </template>
+          </el-table-column>
+          <el-table-column prop="quantity" label="销售数量" width="100" align="right">
+            <template #default="{ row }">{{ row.quantity?.toLocaleString() || 0 }}</template>
+          </el-table-column>
+          <el-table-column prop="salesAmount" label="销售额(元)" width="130" align="right" sortable>
+            <template #default="{ row }">¥{{ row.salesAmount?.toLocaleString() || 0 }}</template>
+          </el-table-column>
+          <el-table-column prop="costAmount" label="成本额(元)" width="130" align="right">
+            <template #default="{ row }">¥{{ row.costAmount?.toLocaleString() || 0 }}</template>
+          </el-table-column>
+          <el-table-column prop="profitAmount" label="利润额(元)" width="130" align="right" sortable>
+            <template #default="{ row }">
+              <span :style="{ color: (row.profitAmount || 0) >= 0 ? '#52c41a' : '#f5222d' }">¥{{ row.profitAmount?.toLocaleString() || 0 }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="profitRate" label="利润率" width="90" align="right" sortable>
+            <template #default="{ row }">
+              <span :style="{ color: (row.profitRate || 0) >= 0 ? '#52c41a' : '#f5222d' }">{{ row.profitRate }}%</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="orderCount" label="订单数" width="90" align="right" sortable>
+            <template #default="{ row }">{{ row.orderCount?.toLocaleString() || 0 }}</template>
+          </el-table-column>
+          <el-table-column prop="percentage" label="销售占比" width="100" align="right" sortable>
+            <template #default="{ row }">
+              <el-progress :percentage="Number(row.percentage) || 0" :stroke-width="6" :show-text="false" style="width: 60px; display: inline-block; vertical-align: middle;" />
+              <span style="margin-left: 6px;">{{ row.percentage }}%</span>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </div>
   </div>
 </template>
 
 <style lang="scss" scoped>
 .category-statistics {
-  padding: 20px;
-  background-color: var(--el-bg-color-page);
+  padding: 16px 20px;
+  background-color: #f5f7fa;
   min-height: calc(100vh - 120px);
 }
 
-.stat-card-wrapper {
-  :deep(.el-card__body) {
-    padding: 20px;
-  }
+.toolbar {
+  display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; flex-wrap: wrap; gap: 12px;
+  .page-title { margin: 0; font-size: 18px; font-weight: 600; color: #1d2129; }
+  .toolbar-right { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
+  .date-shortcuts { display: flex; gap: 4px; }
 }
 
-.stat-card {
-  display: flex;
-  align-items: center;
-  
-  .stat-icon {
-    width: 56px;
-    height: 56px;
-    border-radius: 12px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    margin-right: 16px;
-    
-    i {
-      font-size: 28px;
-      color: white;
-    }
-    
-    &.bg-blue { background: linear-gradient(135deg, #409eff, #337ecc); }
-    &.bg-green { background: linear-gradient(135deg, #67c23a, #529b2e); }
-    &.bg-orange { background: linear-gradient(135deg, #fa8c16, #d46b08); }
-  }
-  
-  .stat-info {
-    flex: 1;
-    
-    .stat-value {
-      font-size: 24px;
-      font-weight: 600;
-      color: var(--el-text-color-primary);
-      margin-bottom: 4px;
-    }
-    
-    .stat-label {
-      font-size: 14px;
-      color: var(--el-text-color-secondary);
-    }
-  }
+.page-body { min-height: 400px; }
+
+.kpi-row { margin-bottom: 12px; }
+
+.kpi-card {
+  background: #fff; border-radius: 8px; padding: 16px 20px; height: 100%; border-left: 4px solid #409eff; transition: box-shadow 0.2s;
+  &:hover { box-shadow: 0 2px 12px rgba(0,0,0,0.08); }
+  &.kpi-blue { border-left-color: #409eff; }
+  &.kpi-green { border-left-color: #67c23a; }
+  &.kpi-orange { border-left-color: #fa8c16; }
+  .kpi-label { font-size: 13px; color: #86909c; margin-bottom: 6px; }
+  .kpi-value { font-size: 22px; font-weight: 700; color: #1d2129; line-height: 1.2; }
 }
 
-.chart-card {
-  height: 350px;
-  
-  :deep(.el-card__body) {
-    padding: 20px;
-    height: 100%;
-  }
-  
-  .chart-container {
-    width: 100%;
-    height: 100%;
-    min-height: 280px;
-  }
+.chart-row { margin-bottom: 12px; }
+.chart-box {
+  background: #fff; border-radius: 8px; padding: 16px; height: 360px;
+  &.chart-box-tall { height: 380px; }
+  .chart-container { width: 100%; height: 100%; }
+}
+
+.table-box {
+  background: #fff; border-radius: 8px; padding: 16px;
+  .table-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
+  .table-title { font-size: 14px; font-weight: 600; color: #1d2129; }
+  .table-hint { font-size: 13px; color: #86909c; }
+}
+
+@media (max-width: 768px) {
+  .kpi-value { font-size: 18px !important; }
+  .chart-box, .chart-box-tall { height: 280px; }
 }
 </style>

+ 396 - 303
haha-admin-web/src/views/statistics/overview/index.vue

@@ -1,41 +1,141 @@
 <script setup lang="ts">
-import { ref, onMounted, computed } from "vue";
+import { ref, onMounted, onUnmounted, computed } from "vue";
 import * as echarts from "echarts";
 import { getStatisticsOverview } from "@/api/statistics";
+import type { EChartsOption } from "echarts";
 
 defineOptions({
   name: "StatisticsOverview"
 });
 
+// ==================== 类型定义 ====================
+interface CategoryStat {
+  category: string;
+  quantity: number;
+  salesAmount: number;
+  costAmount: number;
+  profitAmount: number;
+  profitRate: number;
+  orderCount: number;
+  percentage: number;
+}
+
+interface TrendData {
+  dates: string[];
+  label?: string;
+  value?: number;
+  series: { name: string; data: number[] }[];
+}
+
+interface OverviewData {
+  totalSales: number;
+  totalProfit: number;
+  avgProfitRate: number;
+  totalOrders: number;
+  totalUsers: number;
+  newUsers: number;
+  totalDevices: number;
+  onlineDevices: number;
+  totalShops: number;
+  repurchaseRate: number;
+  avgOrderAmount: number;
+  categoryList: CategoryStat[];
+  salesTrend: TrendData[];
+  profitTrend: TrendData[];
+}
+
+// ==================== 状态 ====================
 const loading = ref(false);
+
+// 日期范围: 默认近30天
 const dateRange = ref<[Date, Date]>([
   new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
   new Date()
 ]);
 
-const overviewData = ref({
+const overviewData = ref<OverviewData>({
   totalSales: 0,
   totalProfit: 0,
   avgProfitRate: 0,
   totalOrders: 0,
   totalUsers: 0,
+  newUsers: 0,
   totalDevices: 0,
   onlineDevices: 0,
   totalShops: 0,
+  repurchaseRate: 0,
   avgOrderAmount: 0,
-  categoryList: []
+  categoryList: [],
+  salesTrend: [{ dates: [], series: [] }],
+  profitTrend: [{ dates: [], series: [] }]
 });
 
-const salesChartRef = ref<HTMLElement | null>(null);
-const categoryChartRef = ref<HTMLElement | null>(null);
-let salesChart: echarts.ECharts | null = null;
-let categoryChart: echarts.ECharts | null = null;
+// 图表引用
+const salesTrendRef = ref<HTMLElement | null>(null);
+const categoryPieRef = ref<HTMLElement | null>(null);
+const profitTrendRef = ref<HTMLElement | null>(null);
+
+let salesTrendChart: echarts.ECharts | null = null;
+let categoryPieChart: echarts.ECharts | null = null;
+let profitTrendChart: echarts.ECharts | null = null;
 
+// 日期快捷选择
+const shortcuts = [
+  { key: "today", label: "今日" },
+  { key: "week", label: "本周" },
+  { key: "month", label: "本月" },
+  { key: "30d", label: "近30天" }
+];
+const activeShortcut = ref("30d");
+
+// ==================== 计算属性 ====================
 const onlineRate = computed(() => {
-  if (overviewData.value.totalDevices === 0) return "0%";
-  return ((overviewData.value.onlineDevices / overviewData.value.totalDevices) * 100).toFixed(1) + "%";
+  const { totalDevices, onlineDevices } = overviewData.value;
+  if (!totalDevices || totalDevices === 0) return "0%";
+  return ((onlineDevices / totalDevices) * 100).toFixed(1) + "%";
+});
+
+const newUserRate = computed(() => {
+  const { totalUsers, newUsers } = overviewData.value;
+  if (!totalUsers || totalUsers === 0) return "0%";
+  return ((newUsers / totalUsers) * 100).toFixed(1) + "%";
 });
 
+// ==================== 日期工具函数 ====================
+function formatDate(date: Date): string {
+  return date.toISOString().split("T")[0];
+}
+
+function setDateShortcut(type: string) {
+  activeShortcut.value = type;
+  const today = new Date();
+  today.setHours(23, 59, 59, 999);
+  let start: Date;
+  switch (type) {
+    case "today":
+      start = new Date();
+      start.setHours(0, 0, 0, 0);
+      break;
+    case "week":
+      start = new Date(today);
+      start.setDate(today.getDate() - 6);
+      start.setHours(0, 0, 0, 0);
+      break;
+    case "month":
+      start = new Date(today.getFullYear(), today.getMonth(), 1);
+      break;
+    case "30d":
+    default:
+      start = new Date(today);
+      start.setDate(today.getDate() - 29);
+      start.setHours(0, 0, 0, 0);
+      break;
+  }
+  dateRange.value = [start, today];
+  fetchOverviewData();
+}
+
+// ==================== 数据获取 ====================
 async function fetchOverviewData() {
   loading.value = true;
   try {
@@ -43,8 +143,10 @@ async function fetchOverviewData() {
       startDate: formatDate(dateRange.value[0]),
       endDate: formatDate(dateRange.value[1])
     };
-    const { data } = await getStatisticsOverview(params);
-    overviewData.value = data;
+    const res = await getStatisticsOverview(params);
+    if (res.code === 200 && res.data) {
+      overviewData.value = res.data;
+    }
     initCharts();
   } catch (error) {
     console.error("获取统计概览数据失败:", error);
@@ -53,155 +155,145 @@ async function fetchOverviewData() {
   }
 }
 
-function formatDate(date: Date): string {
-  return date.toISOString().split("T")[0];
+function handleDateChange() {
+  activeShortcut.value = "";
+  fetchOverviewData();
 }
 
+// ==================== 图表初始化 ====================
 function initCharts() {
   setTimeout(() => {
-    initSalesChart();
-    initCategoryChart();
+    initSalesTrendChart();
+    initCategoryPieChart();
+    initProfitTrendChart();
   }, 100);
 }
 
-function initSalesChart() {
-  if (!salesChartRef.value) return;
-  
-  if (salesChart) {
-    salesChart.dispose();
-  }
-  
-  salesChart = echarts.init(salesChartRef.value);
-  
-  const option: echarts.EChartsOption = {
-    title: {
-      text: "品类销售分布",
-      left: "center",
-      textStyle: { fontSize: 14, fontWeight: "normal" }
-    },
-    tooltip: {
-      trigger: "item",
-      formatter: "{a} <br/>{b}: ¥{c} ({d}%)"
-    },
-    legend: {
-      orient: "vertical",
-      left: "left",
-      top: "middle"
-    },
-    series: [
-      {
-        name: "销售额",
-        type: "pie",
-        radius: ["40%", "70%"],
-        center: ["60%", "50%"],
-        avoidLabelOverlap: false,
-        itemStyle: {
-          borderRadius: 10,
-          borderColor: "#fff",
-          borderWidth: 2
-        },
-        label: {
-          show: false,
-          position: "center"
-        },
-        emphasis: {
-          label: {
-            show: true,
-            fontSize: 16,
-            fontWeight: "bold"
-          }
-        },
-        labelLine: {
-          show: false
-        },
-        data: overviewData.value.categoryList.slice(0, 6).map((item: any) => ({
-          value: item.salesAmount,
-          name: item.category
-        }))
-      }
-    ]
+// 销售趋势折线图
+function initSalesTrendChart() {
+  if (!salesTrendRef.value) return;
+  salesTrendChart?.dispose();
+  salesTrendChart = echarts.init(salesTrendRef.value);
+
+  const trend = overviewData.value.salesTrend?.[0];
+  const dates = trend?.dates || [];
+  const series = (trend?.series || []).map((s: any) => ({
+    name: s.name,
+    type: "line" as const,
+    smooth: true,
+    data: s.data,
+    symbol: "circle",
+    symbolSize: 4
+  }));
+
+  const option: EChartsOption = {
+    tooltip: { trigger: "axis", backgroundColor: "#fff", borderColor: "#e5e7eb", textStyle: { color: "#333" } },
+    legend: { data: series.map(s => s.name), bottom: 0 },
+    grid: { left: "3%", right: "4%", bottom: "12%", top: "10%", containLabel: true },
+    xAxis: { type: "category", data: dates, axisLabel: { rotate: 45, fontSize: 11 } },
+    yAxis: { type: "value", name: "金额(元)", axisLabel: { formatter: (v: number) => v >= 10000 ? (v / 10000).toFixed(1) + "万" : String(v) } },
+    series
   };
-  
-  salesChart.setOption(option);
+  salesTrendChart.setOption(option);
 }
 
-function initCategoryChart() {
-  if (!categoryChartRef.value) return;
-  
-  if (categoryChart) {
-    categoryChart.dispose();
-  }
-  
-  categoryChart = echarts.init(categoryChartRef.value);
-  
-  const categories = overviewData.value.categoryList.slice(0, 8);
-  
-  const option: echarts.EChartsOption = {
-    title: {
-      text: "品类利润对比",
-      left: "center",
-      textStyle: { fontSize: 14, fontWeight: "normal" }
-    },
-    tooltip: {
-      trigger: "axis",
-      axisPointer: { type: "shadow" }
-    },
-    grid: {
-      left: "3%",
-      right: "4%",
-      bottom: "3%",
-      containLabel: true
-    },
-    xAxis: {
-      type: "category",
-      data: categories.map((item: any) => item.category),
-      axisLabel: {
-        rotate: 30
-      }
-    },
-    yAxis: {
-      type: "value",
-      name: "利润(元)"
-    },
-    series: [
-      {
-        name: "利润额",
-        type: "bar",
-        data: categories.map((item: any) => item.profitAmount),
-        itemStyle: {
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: "#83bff6" },
-            { offset: 0.5, color: "#188df0" },
-            { offset: 1, color: "#188df0" }
-          ])
-        }
-      }
-    ]
+// 品类销售分布饼图
+function initCategoryPieChart() {
+  if (!categoryPieRef.value) return;
+  categoryPieChart?.dispose();
+  categoryPieChart = echarts.init(categoryPieRef.value);
+
+  const list = overviewData.value.categoryList || [];
+  const COLORS = ["#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de", "#3ba272", "#fc8452", "#9a60b4"];
+
+  const option: EChartsOption = {
+    title: { text: "品类销售占比", left: "center", top: 10, textStyle: { fontSize: 14, fontWeight: "bold" } },
+    tooltip: { trigger: "item", formatter: (p: any) => `${p.name}: ¥${p.value?.toLocaleString() || 0} (${p.percent}%)` },
+    legend: { orient: "vertical", left: 10, top: "middle", itemWidth: 12, itemHeight: 12 },
+    series: [{
+      type: "pie",
+      radius: ["45%", "72%"],
+      center: ["58%", "55%"],
+      avoidLabelOverlap: false,
+      itemStyle: { borderRadius: 6, borderColor: "#fff", borderWidth: 2 },
+      label: { show: false },
+      emphasis: { label: { show: true, fontSize: 16, fontWeight: "bold" } },
+      labelLine: { show: false },
+      data: list.slice(0, 8).map((item, i) => ({
+        value: item.salesAmount,
+        name: item.category,
+        itemStyle: { color: COLORS[i % COLORS.length] }
+      }))
+    }]
   };
-  
-  categoryChart.setOption(option);
+  categoryPieChart.setOption(option);
 }
 
-function handleDateChange() {
-  fetchOverviewData();
+// 利润趋势面积图
+function initProfitTrendChart() {
+  if (!profitTrendRef.value) return;
+  profitTrendChart?.dispose();
+  profitTrendChart = echarts.init(profitTrendRef.value);
+
+  const trend = overviewData.value.profitTrend?.[0];
+  const dates = trend?.dates || [];
+  const data = trend?.series?.[0]?.data || [];
+
+  const option: EChartsOption = {
+    title: { text: "利润趋势", left: "center", top: 10, textStyle: { fontSize: 14, fontWeight: "bold" } },
+    tooltip: { trigger: "axis", backgroundColor: "#fff", borderColor: "#e5e7eb", textStyle: { color: "#333" } },
+    grid: { left: "3%", right: "4%", bottom: "8%", top: "15%", containLabel: true },
+    xAxis: { type: "category", data: dates, axisLabel: { rotate: 45, fontSize: 11 } },
+    yAxis: { type: "value", name: "利润(元)", axisLabel: { formatter: (v: number) => v >= 10000 ? (v / 10000).toFixed(1) + "万" : String(v) } },
+    series: [{
+      name: "利润",
+      type: "line",
+      smooth: true,
+      data,
+      symbol: "circle",
+      symbolSize: 4,
+      lineStyle: { color: "#67c23a", width: 2 },
+      areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: "rgba(103,194,58,0.25)" }, { offset: 1, color: "rgba(103,194,58,0.02)" }]) }
+    }]
+  };
+  profitTrendChart.setOption(option);
 }
 
+// ==================== 响应式 ====================
 function resizeCharts() {
-  salesChart?.resize();
-  categoryChart?.resize();
+  salesTrendChart?.resize();
+  categoryPieChart?.resize();
+  profitTrendChart?.resize();
 }
 
 onMounted(() => {
   fetchOverviewData();
   window.addEventListener("resize", resizeCharts);
 });
+
+onUnmounted(() => {
+  window.removeEventListener("resize", resizeCharts);
+  salesTrendChart?.dispose();
+  categoryPieChart?.dispose();
+  profitTrendChart?.dispose();
+});
 </script>
 
 <template>
   <div class="statistics-overview">
-    <el-card shadow="never" class="mb-4">
-      <div class="flex items-center justify-between">
-        <span class="text-lg font-medium">统计概览</span>
+    <!-- 顶部工具栏 -->
+    <div class="toolbar">
+      <h2 class="page-title">统计概览</h2>
+      <div class="toolbar-right">
+        <div class="date-shortcuts">
+          <el-button
+            v-for="s in shortcuts"
+            :key="s.key"
+            :type="activeShortcut === s.key ? 'primary' : 'default'"
+            size="small"
+            @click="setDateShortcut(s.key)"
+          >{{ s.label }}</el-button>
+        </div>
         <el-date-picker
           v-model="dateRange"
           type="daterange"
@@ -210,206 +302,207 @@ onMounted(() => {
           end-placeholder="结束日期"
           format="YYYY-MM-DD"
           value-format="YYYY-MM-DD"
+          size="small"
           @change="handleDateChange"
         />
       </div>
-    </el-card>
-
-    <el-row :gutter="20" class="mb-6">
-      <el-col :span="6">
-        <el-card shadow="hover" class="stat-card-wrapper">
-          <div class="stat-card">
-            <div class="stat-icon bg-blue">
-              <i class="ri:money-cny-circle-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">¥{{ overviewData.totalSales?.toLocaleString() || 0 }}</div>
-              <div class="stat-label">总销售额</div>
-            </div>
+    </div>
+
+    <div v-loading="loading" class="page-body">
+      <!-- 第一行: 核心经营指标 -->
+      <el-row :gutter="16" class="kpi-row">
+        <el-col :xs="12" :sm="6" :lg="3">
+          <div class="kpi-card kpi-blue">
+            <div class="kpi-label">总销售额</div>
+            <div class="kpi-value">¥{{ (overviewData.totalSales || 0).toLocaleString() }}</div>
           </div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover" class="stat-card-wrapper">
-          <div class="stat-card">
-            <div class="stat-icon bg-green">
-              <i class="ri:profit-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">¥{{ overviewData.totalProfit?.toLocaleString() || 0 }}</div>
-              <div class="stat-label">总利润</div>
-            </div>
+        </el-col>
+        <el-col :xs="12" :sm="6" :lg="3">
+          <div class="kpi-card kpi-green">
+            <div class="kpi-label">总利润</div>
+            <div class="kpi-value">¥{{ (overviewData.totalProfit || 0).toLocaleString() }}</div>
           </div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover" class="stat-card-wrapper">
-          <div class="stat-card">
-            <div class="stat-icon bg-purple">
-              <i class="ri:shopping-cart-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">{{ overviewData.totalOrders?.toLocaleString() || 0 }}</div>
-              <div class="stat-label">总订单数</div>
-            </div>
+        </el-col>
+        <el-col :xs="12" :sm="6" :lg="3">
+          <div class="kpi-card kpi-purple">
+            <div class="kpi-label">总订单数</div>
+            <div class="kpi-value">{{ (overviewData.totalOrders || 0).toLocaleString() }}</div>
           </div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover" class="stat-card-wrapper">
-          <div class="stat-card">
-            <div class="stat-icon bg-orange">
-              <i class="ri:percent-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">{{ overviewData.avgProfitRate || 0 }}%</div>
-              <div class="stat-label">平均利润率</div>
-            </div>
+        </el-col>
+        <el-col :xs="12" :sm="6" :lg="3">
+          <div class="kpi-card kpi-orange">
+            <div class="kpi-label">利润率</div>
+            <div class="kpi-value">{{ overviewData.avgProfitRate || 0 }}%</div>
           </div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <el-row :gutter="20" class="mb-6">
-      <el-col :span="6">
-        <el-card shadow="hover" class="stat-card-wrapper">
-          <div class="stat-card">
-            <div class="stat-icon bg-cyan">
-              <i class="ri:user-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">{{ overviewData.totalUsers?.toLocaleString() || 0 }}</div>
-              <div class="stat-label">购买用户数</div>
-            </div>
+        </el-col>
+      </el-row>
+
+      <!-- 第二行: 辅助指标 -->
+      <el-row :gutter="16" class="kpi-row">
+        <el-col :xs="12" :sm="6" :lg="3">
+          <div class="kpi-card kpi-cyan">
+            <div class="kpi-label">购买用户</div>
+            <div class="kpi-value">{{ (overviewData.totalUsers || 0).toLocaleString() }}</div>
+            <div class="kpi-sub">新用户 {{ (overviewData.newUsers || 0).toLocaleString() }} ({{ newUserRate }})</div>
           </div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover" class="stat-card-wrapper">
-          <div class="stat-card">
-            <div class="stat-icon bg-teal">
-              <i class="ri:device-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">{{ overviewData.totalDevices || 0 }}</div>
-              <div class="stat-label">设备总数 (在线率: {{ onlineRate }})</div>
-            </div>
+        </el-col>
+        <el-col :xs="12" :sm="6" :lg="3">
+          <div class="kpi-card kpi-teal">
+            <div class="kpi-label">设备在线率</div>
+            <div class="kpi-value">{{ onlineRate }}</div>
+            <div class="kpi-sub">{{ overviewData.onlineDevices || 0 }}/{{ overviewData.totalDevices || 0 }} 台在线</div>
           </div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover" class="stat-card-wrapper">
-          <div class="stat-card">
-            <div class="stat-icon bg-indigo">
-              <i class="ri:store-2-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">{{ overviewData.totalShops || 0 }}</div>
-              <div class="stat-label">门店总数</div>
-            </div>
+        </el-col>
+        <el-col :xs="12" :sm="6" :lg="3">
+          <div class="kpi-card kpi-indigo">
+            <div class="kpi-label">门店数</div>
+            <div class="kpi-value">{{ (overviewData.totalShops || 0).toLocaleString() }}</div>
+            <div class="kpi-sub">门店</div>
           </div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover" class="stat-card-wrapper">
-          <div class="stat-card">
-            <div class="stat-icon bg-pink">
-              <i class="ri:coins-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">¥{{ overviewData.avgOrderAmount || 0 }}</div>
-              <div class="stat-label">平均客单价</div>
-            </div>
+        </el-col>
+        <el-col :xs="12" :sm="6" :lg="3">
+          <div class="kpi-card kpi-pink">
+            <div class="kpi-label">客单价 / 复购率</div>
+            <div class="kpi-value">¥{{ (overviewData.avgOrderAmount || 0).toLocaleString() }}</div>
+            <div class="kpi-sub">复购率 {{ overviewData.repurchaseRate || 0 }}%</div>
           </div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <el-row :gutter="20">
-      <el-col :span="12">
-        <el-card shadow="hover" class="chart-card">
-          <div ref="salesChartRef" class="chart-container"></div>
-        </el-card>
-      </el-col>
-      <el-col :span="12">
-        <el-card shadow="hover" class="chart-card">
-          <div ref="categoryChartRef" class="chart-container"></div>
-        </el-card>
-      </el-col>
-    </el-row>
+        </el-col>
+      </el-row>
+
+      <!-- 第三行: 图表区 -->
+      <el-row :gutter="16" class="chart-row">
+        <el-col :span="16">
+          <div class="chart-box">
+            <div ref="salesTrendRef" class="chart-container"></div>
+          </div>
+        </el-col>
+        <el-col :span="8">
+          <div class="chart-box">
+            <div ref="categoryPieRef" class="chart-container"></div>
+          </div>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="16" class="chart-row">
+        <el-col :span="24">
+          <div class="chart-box">
+            <div ref="profitTrendRef" class="chart-container"></div>
+          </div>
+        </el-col>
+      </el-row>
+    </div>
   </div>
 </template>
 
 <style lang="scss" scoped>
 .statistics-overview {
-  padding: 20px;
-  background-color: var(--el-bg-color-page);
+  padding: 16px 20px;
+  background-color: #f5f7fa;
   min-height: calc(100vh - 120px);
 }
 
-.stat-card-wrapper {
-  :deep(.el-card__body) {
-    padding: 20px;
-  }
-}
-
-.stat-card {
+// 工具栏
+.toolbar {
   display: flex;
   align-items: center;
-  
-  .stat-icon {
-    width: 56px;
-    height: 56px;
-    border-radius: 12px;
+  justify-content: space-between;
+  margin-bottom: 16px;
+  flex-wrap: wrap;
+  gap: 12px;
+
+  .page-title {
+    margin: 0;
+    font-size: 18px;
+    font-weight: 600;
+    color: #1d2129;
+  }
+
+  .toolbar-right {
     display: flex;
     align-items: center;
-    justify-content: center;
-    margin-right: 16px;
-    
-    i {
-      font-size: 28px;
-      color: white;
-    }
-    
-    &.bg-blue { background: linear-gradient(135deg, #409eff, #337ecc); }
-    &.bg-green { background: linear-gradient(135deg, #67c23a, #529b2e); }
-    &.bg-purple { background: linear-gradient(135deg, #722ed1, #531dab); }
-    &.bg-orange { background: linear-gradient(135deg, #fa8c16, #d46b08); }
-    &.bg-cyan { background: linear-gradient(135deg, #13c2c2, #08979c); }
-    &.bg-teal { background: linear-gradient(135deg, #52c41a, #389e0d); }
-    &.bg-indigo { background: linear-gradient(135deg, #597ef7, #2f54eb); }
-    &.bg-pink { background: linear-gradient(135deg, #eb2f96, #c41d7f); }
+    gap: 12px;
+    flex-wrap: wrap;
   }
-  
-  .stat-info {
-    flex: 1;
-    
-    .stat-value {
-      font-size: 24px;
-      font-weight: 600;
-      color: var(--el-text-color-primary);
-      margin-bottom: 4px;
-    }
-    
-    .stat-label {
-      font-size: 14px;
-      color: var(--el-text-color-secondary);
-    }
+
+  .date-shortcuts {
+    display: flex;
+    gap: 4px;
   }
 }
 
-.chart-card {
-  height: 380px;
-  
-  :deep(.el-card__body) {
-    padding: 20px;
-    height: 100%;
+.page-body {
+  min-height: 400px;
+}
+
+// KPI 指标卡片
+.kpi-row {
+  margin-bottom: 12px;
+}
+
+.kpi-card {
+  background: #fff;
+  border-radius: 8px;
+  padding: 16px 20px;
+  height: 100%;
+  border-left: 4px solid #409eff;
+  transition: box-shadow 0.2s;
+
+  &:hover {
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+  }
+
+  &.kpi-blue  { border-left-color: #409eff; }
+  &.kpi-green { border-left-color: #67c23a; }
+  &.kpi-purple { border-left-color: #722ed1; }
+  &.kpi-orange { border-left-color: #fa8c16; }
+  &.kpi-cyan { border-left-color: #13c2c2; }
+  &.kpi-teal { border-left-color: #52c41a; }
+  &.kpi-indigo { border-left-color: #597ef7; }
+  &.kpi-pink { border-left-color: #eb2f96; }
+
+  .kpi-label {
+    font-size: 13px;
+    color: #86909c;
+    margin-bottom: 6px;
+  }
+
+  .kpi-value {
+    font-size: 22px;
+    font-weight: 700;
+    color: #1d2129;
+    line-height: 1.2;
+  }
+
+  .kpi-sub {
+    font-size: 12px;
+    color: #86909c;
+    margin-top: 4px;
   }
-  
+}
+
+// 图表区
+.chart-row {
+  margin-bottom: 12px;
+}
+
+.chart-box {
+  background: #fff;
+  border-radius: 8px;
+  padding: 16px;
+  height: 360px;
+
   .chart-container {
     width: 100%;
     height: 100%;
-    min-height: 320px;
+  }
+}
+
+@media (max-width: 768px) {
+  .kpi-value {
+    font-size: 18px !important;
+  }
+  .chart-box {
+    height: 280px;
   }
 }
 </style>

+ 322 - 409
haha-admin-web/src/views/statistics/profit/index.vue

@@ -1,7 +1,8 @@
 <script setup lang="ts">
-import { ref, onMounted, reactive } from "vue";
+import { ref, onMounted, onUnmounted, reactive } from "vue";
 import * as echarts from "echarts";
 import { getProfitOverview, getProfitList, getProfitTrend, getProfitWarning } from "@/api/statistics";
+import type { EChartsOption } from "echarts";
 
 defineOptions({
   name: "ProfitStatistics"
@@ -12,6 +13,20 @@ const dateRange = ref<[Date, Date]>([
   new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
   new Date()
 ]);
+const activeShortcut = ref("30d");
+
+const shortcuts = [
+  { key: "today", label: "今日" },
+  { key: "week", label: "本周" },
+  { key: "month", label: "本月" },
+  { key: "30d", label: "近30天" }
+];
+
+const compareOptions = [
+  { value: "", label: "无对比" },
+  { value: "yoy", label: "同比" },
+  { value: "mom", label: "环比" }
+];
 
 const queryParams = reactive({
   page: 1,
@@ -29,486 +44,384 @@ const overviewData = ref({
   salesAmount: 0,
   costAmount: 0,
   grossProfit: 0,
-  grossProfitRate: 0
+  grossProfitRate: 0,
+  refundAmount: 0,
+  refundRate: 0,
+  netProfit: 0,
+  netProfitRate: 0
 });
 
-const tableData = ref([]);
+const tableData = ref<any[]>([]);
 const total = ref(0);
-const warningData = ref([]);
+const warningData = ref<any[]>([]);
 
 const profitChartRef = ref<HTMLElement | null>(null);
 const warningChartRef = ref<HTMLElement | null>(null);
 let profitChart: echarts.ECharts | null = null;
 let warningChart: echarts.ECharts | null = null;
 
+function formatDate(date: Date): string { return date.toISOString().split("T")[0]; }
+
+function setDateShortcut(type: string) {
+  activeShortcut.value = type;
+  const today = new Date(); today.setHours(23, 59, 59, 999);
+  let start: Date;
+  switch (type) {
+    case "today": start = new Date(); start.setHours(0, 0, 0, 0); break;
+    case "week": start = new Date(today); start.setDate(today.getDate() - 6); start.setHours(0, 0, 0, 0); break;
+    case "month": start = new Date(today.getFullYear(), today.getMonth(), 1); break;
+    case "30d": default: start = new Date(today); start.setDate(today.getDate() - 29); start.setHours(0, 0, 0, 0); break;
+  }
+  dateRange.value = [start, today];
+  fetchAll();
+}
+
+function handleDateChange() { activeShortcut.value = ""; fetchAll(); }
+
+function fetchAll() {
+  fetchOverview();
+  fetchProfitList();
+  fetchProfitTrend();
+  fetchWarning();
+}
+
 async function fetchOverview() {
   try {
-    const params = {
-      startDate: formatDate(dateRange.value[0]),
-      endDate: formatDate(dateRange.value[1])
-    };
-    
-    const { data } = await getProfitOverview(params);
-    overviewData.value = data;
-  } catch (error) {
-    console.error("获取利润概览失败:", error);
-  }
+    const params: any = { startDate: formatDate(dateRange.value[0]), endDate: formatDate(dateRange.value[1]) };
+    if (queryParams.compareType) params.compareType = queryParams.compareType;
+    const res = await getProfitOverview(params);
+    if (res.code === 200 && res.data) overviewData.value = res.data;
+  } catch (e) { console.error(e); }
 }
 
 async function fetchProfitList() {
   loading.value = true;
   try {
-    const params = {
-      ...queryParams,
-      startDate: formatDate(dateRange.value[0]),
-      endDate: formatDate(dateRange.value[1])
-    };
-    
-    const { data } = await getProfitList(params);
-    tableData.value = data.list || [];
-    total.value = Number(data.total) || 0;
-  } catch (error) {
-    console.error("获取利润数据失败:", error);
-  } finally {
-    loading.value = false;
-  }
+    const params: any = { ...queryParams, startDate: formatDate(dateRange.value[0]), endDate: formatDate(dateRange.value[1]) };
+    const res = await getProfitList(params);
+    if (res.code === 200 && res.data) {
+      tableData.value = res.data.list || [];
+      total.value = Number(res.data.total) || 0;
+    }
+  } catch (e) { console.error(e); } finally { loading.value = false; }
 }
 
 async function fetchProfitTrend() {
   try {
-    const params = {
-      startDate: formatDate(dateRange.value[0]),
-      endDate: formatDate(dateRange.value[1])
-    };
-    
-    const { data } = await getProfitTrend(params);
-    initProfitChart(data);
-  } catch (error) {
-    console.error("获取利润趋势失败:", error);
-  }
+    const params: any = { startDate: formatDate(dateRange.value[0]), endDate: formatDate(dateRange.value[1]) };
+    if (queryParams.compareType) params.compareType = queryParams.compareType;
+    const res = await getProfitTrend(params);
+    if (res.code === 200 && res.data) initProfitChart(res.data);
+  } catch (e) { console.error(e); }
 }
 
 async function fetchWarning() {
   try {
-    const params = {
-      startDate: formatDate(dateRange.value[0]),
-      endDate: formatDate(dateRange.value[1]),
-      type: "low"
-    };
-    
-    const { data } = await getProfitWarning(params);
-    warningData.value = data || [];
-    initWarningChart();
-  } catch (error) {
-    console.error("获取利润预警失败:", error);
-  }
-}
-
-function formatDate(date: Date): string {
-  return date.toISOString().split("T")[0];
+    const params = { startDate: formatDate(dateRange.value[0]), endDate: formatDate(dateRange.value[1]), type: "low" };
+    const res = await getProfitWarning(params);
+    if (res.code === 200 && res.data) { warningData.value = res.data; initWarningChart(); }
+  } catch (e) { console.error(e); }
 }
 
 function initProfitChart(data: any) {
   if (!profitChartRef.value) return;
-  
-  if (profitChart) {
-    profitChart.dispose();
-  }
-  
+  profitChart?.dispose();
   profitChart = echarts.init(profitChartRef.value);
-  
-  const option: echarts.EChartsOption = {
-    title: {
-      text: "利润趋势",
-      left: "center",
-      textStyle: { fontSize: 14, fontWeight: "normal" }
-    },
-    tooltip: {
-      trigger: "axis",
-      backgroundColor: "rgba(255, 255, 255, 0.9)",
-      borderColor: "#e5e7eb",
-      borderWidth: 1
-    },
-    grid: {
-      left: "3%",
-      right: "4%",
-      bottom: "3%",
-      containLabel: true
-    },
-    xAxis: {
-      type: "category",
-      data: data.dates || [],
-      axisLabel: {
-        rotate: 45
-      }
-    },
-    yAxis: {
-      type: "value",
-      name: "利润(元)"
-    },
-    series: [
-      {
-        name: "利润",
-        type: "line",
-        smooth: true,
-        data: data.series?.[0]?.data || [],
-        itemStyle: { color: "#67c23a" },
-        areaStyle: {
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: "rgba(103, 194, 58, 0.3)" },
-            { offset: 1, color: "rgba(103, 194, 58, 0.05)" }
-          ])
-        }
-      }
-    ]
+  const option: EChartsOption = {
+    tooltip: { trigger: "axis", backgroundColor: "#fff", borderColor: "#e5e7eb", textStyle: { color: "#333" } },
+    grid: { left: "3%", right: "4%", bottom: "5%", top: "8%", containLabel: true },
+    xAxis: { type: "category", data: data.dates || [], axisLabel: { rotate: 45, fontSize: 11 } },
+    yAxis: { type: "value", name: "利润(元)", axisLabel: { formatter: (v: number) => v >= 10000 ? (v / 10000).toFixed(1) + "万" : String(v) } },
+    series: [{
+      name: "利润", type: "line", smooth: true, data: data.series?.[0]?.data || [], symbol: "circle", symbolSize: 4,
+      lineStyle: { color: "#67c23a", width: 2 },
+      areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: "rgba(103,194,58,0.2)" }, { offset: 1, color: "rgba(103,194,58,0.02)" }]) }
+    }]
   };
-  
   profitChart.setOption(option);
 }
 
 function initWarningChart() {
-  if (!warningChartRef.value) return;
-  
-  if (warningChart) {
-    warningChart.dispose();
-  }
-  
+  if (!warningChartRef.value || warningData.value.length === 0) return;
+  warningChart?.dispose();
   warningChart = echarts.init(warningChartRef.value);
-  
-  const option: echarts.EChartsOption = {
-    title: {
-      text: "利润预警门店",
-      left: "center",
-      textStyle: { fontSize: 14, fontWeight: "normal" }
-    },
-    tooltip: {
-      trigger: "axis",
-      axisPointer: { type: "shadow" }
-    },
-    grid: {
-      left: "3%",
-      right: "4%",
-      bottom: "3%",
-      containLabel: true
-    },
-    xAxis: {
-      type: "value",
-      name: "净利润(元)"
-    },
-    yAxis: {
-      type: "category",
-      data: warningData.value.slice(0, 10).map((item: any) => item.shopName).reverse(),
-      axisLabel: {
-        width: 80,
-        overflow: "truncate"
-      }
-    },
-    series: [
-      {
-        name: "净利润",
-        type: "bar",
-        data: warningData.value.slice(0, 10).map((item: any) => item.netProfit).reverse(),
-        itemStyle: {
-          color: (params: any) => params.value >= 0 ? "#67c23a" : "#f56c6c"
-        }
-      }
-    ]
+  const list = [...warningData.value].slice(0, 10).reverse();
+  const option: EChartsOption = {
+    tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, formatter: (p: any) => `${p[0].name}<br/>净利润: ¥${(p[0].value ?? 0).toLocaleString()}` },
+    grid: { left: "3%", right: "8%", bottom: "3%", top: "8%", containLabel: true },
+    xAxis: { type: "value", name: "净利润(元)" },
+    yAxis: { type: "category", data: list.map((i: any) => i.shopName), axisLabel: { width: 100, overflow: "truncate" } },
+    series: [{
+      name: "净利润", type: "bar", data: list.map((i: any) => i.netProfit),
+      itemStyle: { borderRadius: [0, 4, 4, 0], color: (p: any) => p.value >= 0 ? "#67c23a" : "#f56c6c" }
+    }]
   };
-  
   warningChart.setOption(option);
 }
 
-function handleSearch() {
-  queryParams.page = 1;
-  fetchProfitList();
-}
+function handleSearch() { queryParams.page = 1; fetchProfitList(); }
+function handleReset() { queryParams.keyword = ""; queryParams.province = ""; queryParams.city = ""; queryParams.shopId = null; queryParams.compareType = ""; queryParams.page = 1; fetchAll(); }
+function handleSortChange({ prop, order }: any) { queryParams.sortBy = prop || "netProfit"; queryParams.sortOrder = order === "ascending" ? "asc" : "desc"; fetchProfitList(); }
+function handlePageChange(page: number) { queryParams.page = page; fetchProfitList(); }
+function handleSizeChange(size: number) { queryParams.pageSize = size; queryParams.page = 1; fetchProfitList(); }
+function handleCompareChange() { fetchOverview(); fetchProfitList(); fetchProfitTrend(); }
 
-function handleReset() {
-  queryParams.keyword = "";
-  queryParams.province = "";
-  queryParams.city = "";
-  queryParams.shopId = null;
-  queryParams.compareType = "";
-  queryParams.page = 1;
-  fetchProfitList();
-}
-
-function handleSortChange({ prop, order }: any) {
-  queryParams.sortBy = prop || "netProfit";
-  queryParams.sortOrder = order === "ascending" ? "asc" : "desc";
-  fetchProfitList();
-}
-
-function handlePageChange(page: number) {
-  queryParams.page = page;
-  fetchProfitList();
-}
-
-function handleSizeChange(size: number) {
-  queryParams.pageSize = size;
-  queryParams.page = 1;
-  fetchProfitList();
-}
-
-function handleDateChange() {
-  fetchOverview();
-  fetchProfitList();
-  fetchProfitTrend();
-  fetchWarning();
-}
-
-function resizeCharts() {
-  profitChart?.resize();
-  warningChart?.resize();
-}
+function resizeCharts() { profitChart?.resize(); warningChart?.resize(); }
 
 onMounted(() => {
-  fetchOverview();
-  fetchProfitList();
-  fetchProfitTrend();
-  fetchWarning();
+  fetchAll();
   window.addEventListener("resize", resizeCharts);
 });
+onUnmounted(() => {
+  window.removeEventListener("resize", resizeCharts);
+  profitChart?.dispose();
+  warningChart?.dispose();
+});
 </script>
 
 <template>
   <div class="profit-statistics">
-    <el-card shadow="never" class="mb-4">
-      <div class="flex items-center justify-between">
-        <span class="text-lg font-medium">门店利润报表</span>
-        <el-date-picker
-          v-model="dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          format="YYYY-MM-DD"
-          value-format="YYYY-MM-DD"
-          @change="handleDateChange"
-        />
+    <div class="toolbar">
+      <h2 class="page-title">门店利润报表</h2>
+      <div class="toolbar-right">
+        <div class="date-shortcuts">
+          <el-button v-for="s in shortcuts" :key="s.key" :type="activeShortcut === s.key ? 'primary' : 'default'" size="small" @click="setDateShortcut(s.key)">{{ s.label }}</el-button>
+        </div>
+        <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" size="small" @change="handleDateChange" />
       </div>
-    </el-card>
-
-    <el-row :gutter="20" class="mb-6">
-      <el-col :span="6">
-        <el-card shadow="hover">
-          <div class="stat-card">
-            <div class="stat-icon bg-blue">
-              <i class="ri:money-cny-circle-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">¥{{ overviewData.salesAmount?.toLocaleString() || 0 }}</div>
-              <div class="stat-label">销售收入</div>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover">
-          <div class="stat-card">
-            <div class="stat-icon bg-red">
-              <i class="ri:wallet-3-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">¥{{ overviewData.costAmount?.toLocaleString() || 0 }}</div>
-              <div class="stat-label">销售成本</div>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover">
-          <div class="stat-card">
-            <div class="stat-icon bg-green">
-              <i class="ri:profit-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">¥{{ overviewData.grossProfit?.toLocaleString() || 0 }}</div>
-              <div class="stat-label">毛利润</div>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover">
-          <div class="stat-card">
-            <div class="stat-icon bg-orange">
-              <i class="ri:percent-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">{{ overviewData.grossProfitRate }}%</div>
-              <div class="stat-label">毛利率</div>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <el-row :gutter="20" class="mb-6">
-      <el-col :span="12">
-        <el-card shadow="hover" class="chart-card">
-          <div ref="profitChartRef" class="chart-container"></div>
-        </el-card>
-      </el-col>
-      <el-col :span="12">
-        <el-card shadow="hover" class="chart-card">
-          <div ref="warningChartRef" class="chart-container"></div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <el-card shadow="never">
-      <template #header>
-        <div class="flex items-center justify-between">
-          <span class="font-medium">门店利润明细</span>
-          <div class="flex items-center gap-2">
-            <el-input
-              v-model="queryParams.keyword"
-              placeholder="门店名称"
-              clearable
-              style="width: 200px"
-              @keyup.enter="handleSearch"
-            />
-            <el-select v-model="queryParams.compareType" placeholder="对比周期" clearable style="width: 120px">
-              <el-option label="同比" value="yoy" />
-              <el-option label="环比" value="mom" />
+    </div>
+
+    <div v-loading="loading" class="page-body">
+      <el-row :gutter="16" class="kpi-row">
+        <el-col :xs="12" :sm="6" :lg="4">
+          <div class="kpi-card kpi-blue"><div class="kpi-label">销售收入</div><div class="kpi-value">¥{{ (overviewData.salesAmount || 0).toLocaleString() }}</div></div>
+        </el-col>
+        <el-col :xs="12" :sm="6" :lg="4">
+          <div class="kpi-card kpi-red"><div class="kpi-label">销售成本</div><div class="kpi-value">¥{{ (overviewData.costAmount || 0).toLocaleString() }}</div></div>
+        </el-col>
+        <el-col :xs="12" :sm="6" :lg="4">
+          <div class="kpi-card kpi-green"><div class="kpi-label">毛利润</div><div class="kpi-value">¥{{ (overviewData.grossProfit || 0).toLocaleString() }}</div><div class="kpi-sub">毛利率 {{ overviewData.grossProfitRate || 0 }}%</div></div>
+        </el-col>
+        <el-col :xs="12" :sm="6" :lg="4">
+          <div class="kpi-card kpi-purple"><div class="kpi-label">净利润</div><div class="kpi-value">¥{{ (overviewData.netProfit || 0).toLocaleString() }}</div><div class="kpi-sub">净利率 {{ overviewData.netProfitRate || 0 }}%</div></div>
+        </el-col>
+        <el-col :xs="12" :sm="6" :lg="4">
+          <div class="kpi-card kpi-orange"><div class="kpi-label">退款金额</div><div class="kpi-value">¥{{ (overviewData.refundAmount || 0).toLocaleString() }}</div><div class="kpi-sub">退款率 {{ overviewData.refundRate || 0 }}%</div></div>
+        </el-col>
+        <el-col :xs="12" :sm="6" :lg="4">
+          <div class="kpi-card kpi-cyan">
+            <div class="kpi-label">对比周期</div>
+            <el-select v-model="queryParams.compareType" size="small" style="width: 100%;" @change="handleCompareChange">
+              <el-option v-for="o in compareOptions" :key="o.value" :label="o.label" :value="o.value" />
             </el-select>
-            <el-button type="primary" @click="handleSearch">搜索</el-button>
-            <el-button @click="handleReset">重置</el-button>
           </div>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="16" class="chart-row">
+        <el-col :span="14">
+          <div class="chart-box"><div class="chart-title">利润趋势</div><div ref="profitChartRef" class="chart-container"></div></div>
+        </el-col>
+        <el-col :span="10">
+          <div class="chart-box"><div class="chart-title">低利润预警门店</div><div ref="warningChartRef" class="chart-container"></div></div>
+        </el-col>
+      </el-row>
+
+      <div class="table-box">
+        <div class="table-header">
+          <span class="table-title">门店利润明细</span>
+          <div class="table-actions">
+            <el-input v-model="queryParams.keyword" placeholder="门店名称" clearable size="small" style="width: 160px;" @keyup.enter="handleSearch" @clear="handleSearch" />
+            <el-button type="primary" size="small" @click="handleSearch">搜索</el-button>
+            <el-button size="small" @click="handleReset">重置</el-button>
+          </div>
+        </div>
+        <el-table :data="tableData" stripe v-loading="loading" @sort-change="handleSortChange">
+          <el-table-column prop="shopName" label="门店" min-width="130" fixed="left" show-overflow-tooltip />
+          <el-table-column prop="province" label="省份" width="80" />
+          <el-table-column prop="city" label="城市" width="80" />
+          <el-table-column prop="deviceCount" label="设备数" width="75" align="right" />
+          <el-table-column prop="salesAmount" label="销售收入" width="120" align="right" sortable="custom">
+            <template #default="{ row }">¥{{ (row.salesAmount || 0).toLocaleString() }}</template>
+          </el-table-column>
+          <el-table-column prop="costAmount" label="销售成本" width="120" align="right">
+            <template #default="{ row }">¥{{ (row.costAmount || 0).toLocaleString() }}</template>
+          </el-table-column>
+          <el-table-column prop="grossProfit" label="毛利润" width="110" align="right" sortable="custom">
+            <template #default="{ row }"><span :style="{ color: (row.grossProfit || 0) >= 0 ? '#52c41a' : '#f5222d' }">¥{{ (row.grossProfit || 0).toLocaleString() }}</span></template>
+          </el-table-column>
+          <el-table-column prop="grossProfitRate" label="毛利率" width="80" align="right">
+            <template #default="{ row }"><span :style="{ color: (row.grossProfitRate || 0) >= 0 ? '#52c41a' : '#f5222d' }">{{ row.grossProfitRate || 0 }}%</span></template>
+          </el-table-column>
+          <el-table-column prop="netProfit" label="净利润" width="110" align="right" sortable="custom">
+            <template #default="{ row }"><span :style="{ color: (row.netProfit || 0) >= 0 ? '#52c41a' : '#f5222d' }">¥{{ (row.netProfit || 0).toLocaleString() }}</span></template>
+          </el-table-column>
+          <el-table-column prop="netProfitRate" label="净利率" width="80" align="right">
+            <template #default="{ row }"><span :style="{ color: (row.netProfitRate || 0) >= 0 ? '#52c41a' : '#f5222d' }">{{ row.netProfitRate || 0 }}%</span></template>
+          </el-table-column>
+          <el-table-column prop="deviceProfit" label="单设备利润" width="110" align="right">
+            <template #default="{ row }">¥{{ (row.deviceProfit || 0).toLocaleString() }}</template>
+          </el-table-column>
+          <el-table-column prop="yoyGrowth" label="同比增长" width="90" align="right">
+            <template #default="{ row }">
+              <span v-if="row.yoyGrowth != null" :style="{ color: (row.yoyGrowth || 0) >= 0 ? '#52c41a' : '#f5222d' }">{{ row.yoyGrowth }}%</span>
+              <span v-else style="color: #c0c4cc;">--</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="momGrowth" label="环比增长" width="90" align="right">
+            <template #default="{ row }">
+              <span v-if="row.momGrowth != null" :style="{ color: (row.momGrowth || 0) >= 0 ? '#52c41a' : '#f5222d' }">{{ row.momGrowth }}%</span>
+              <span v-else style="color: #c0c4cc;">--</span>
+            </template>
+          </el-table-column>
+        </el-table>
+        <div class="table-footer">
+          <el-pagination
+            v-model:current-page="queryParams.page"
+            v-model:page-size="queryParams.pageSize"
+            :page-sizes="[10, 20, 50, 100]"
+            :total="total"
+            layout="total, sizes, prev, pager, next, jumper"
+            background small
+            @size-change="handleSizeChange"
+            @current-change="handlePageChange"
+          />
         </div>
-      </template>
-      
-      <el-table :data="tableData" stripe v-loading="loading" @sort-change="handleSortChange">
-        <el-table-column prop="shopName" label="门店名称" min-width="120" show-overflow-tooltip />
-        <el-table-column prop="province" label="省份" width="80" />
-        <el-table-column prop="city" label="城市" width="80" />
-        <el-table-column prop="salesAmount" label="销售收入(元)" width="120" align="right" sortable="custom">
-          <template #default="{ row }">
-            ¥{{ row.salesAmount?.toLocaleString() || 0 }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="costAmount" label="销售成本(元)" width="120" align="right">
-          <template #default="{ row }">
-            ¥{{ row.costAmount?.toLocaleString() || 0 }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="grossProfit" label="毛利润(元)" width="110" align="right" sortable="custom">
-          <template #default="{ row }">
-            <span :class="row.grossProfit >= 0 ? 'text-green-500' : 'text-red-500'">
-              ¥{{ row.grossProfit?.toLocaleString() || 0 }}
-            </span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="grossProfitRate" label="毛利率" width="90" align="right">
-          <template #default="{ row }">
-            <span :class="row.grossProfitRate >= 0 ? 'text-green-500' : 'text-red-500'">
-              {{ row.grossProfitRate }}%
-            </span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="netProfit" label="净利润(元)" width="110" align="right" sortable="custom">
-          <template #default="{ row }">
-            <span :class="row.netProfit >= 0 ? 'text-green-500' : 'text-red-500'">
-              ¥{{ row.netProfit?.toLocaleString() || 0 }}
-            </span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="netProfitRate" label="净利率" width="90" align="right">
-          <template #default="{ row }">
-            <span :class="row.netProfitRate >= 0 ? 'text-green-500' : 'text-red-500'">
-              {{ row.netProfitRate }}%
-            </span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="deviceCount" label="设备数" width="80" align="right" />
-        <el-table-column prop="deviceProfit" label="单设备利润" width="100" align="right">
-          <template #default="{ row }">
-            ¥{{ row.deviceProfit || 0 }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="dailyProfit" label="日均利润" width="100" align="right">
-          <template #default="{ row }">
-            ¥{{ row.dailyProfit || 0 }}
-          </template>
-        </el-table-column>
-      </el-table>
-      
-      <div class="flex justify-end mt-4">
-        <el-pagination
-          v-model:current-page="queryParams.page"
-          v-model:page-size="queryParams.pageSize"
-          :page-sizes="[10, 20, 50, 100]"
-          :total="total"
-          layout="total, sizes, prev, pager, next, jumper"
-          @size-change="handleSizeChange"
-          @current-change="handlePageChange"
-        />
       </div>
-    </el-card>
+    </div>
   </div>
 </template>
 
 <style lang="scss" scoped>
 .profit-statistics {
-  padding: 20px;
-  background-color: var(--el-bg-color-page);
+  padding: 16px 20px;
+  background-color: #f5f7fa;
   min-height: calc(100vh - 120px);
 }
 
-.stat-card {
+.toolbar {
   display: flex;
   align-items: center;
-  
-  .stat-icon {
-    width: 50px;
-    height: 50px;
-    border-radius: 12px;
+  justify-content: space-between;
+  margin-bottom: 16px;
+  flex-wrap: wrap;
+  gap: 12px;
+
+  .page-title {
+    margin: 0;
+    font-size: 18px;
+    font-weight: 600;
+    color: #1d2129;
+  }
+
+  .toolbar-right {
     display: flex;
     align-items: center;
-    justify-content: center;
-    margin-right: 16px;
-    
-    i {
-      font-size: 24px;
-      color: white;
-    }
-    
-    &.bg-blue { background: linear-gradient(135deg, #409eff, #337ecc); }
-    &.bg-green { background: linear-gradient(135deg, #67c23a, #529b2e); }
-    &.bg-red { background: linear-gradient(135deg, #f56c6c, #c45656); }
-    &.bg-orange { background: linear-gradient(135deg, #fa8c16, #d46b08); }
+    gap: 12px;
+    flex-wrap: wrap;
   }
-  
-  .stat-info {
-    flex: 1;
-    
-    .stat-value {
-      font-size: 24px;
-      font-weight: 600;
-      color: var(--el-text-color-primary);
-      margin-bottom: 4px;
-    }
-    
-    .stat-label {
-      font-size: 14px;
-      color: var(--el-text-color-secondary);
-    }
+
+  .date-shortcuts {
+    display: flex;
+    gap: 4px;
   }
 }
 
-.chart-card {
-  height: 350px;
-  
-  :deep(.el-card__body) {
-    padding: 20px;
-    height: 100%;
+.page-body { min-height: 400px; }
+
+.kpi-row { margin-bottom: 12px; }
+
+.kpi-card {
+  background: #fff;
+  border-radius: 8px;
+  padding: 16px 20px;
+  height: 100%;
+  border-left: 4px solid #409eff;
+  transition: box-shadow 0.2s;
+
+  &:hover { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); }
+
+  &.kpi-blue { border-left-color: #409eff; }
+  &.kpi-red { border-left-color: #f56c6c; }
+  &.kpi-green { border-left-color: #67c23a; }
+  &.kpi-purple { border-left-color: #722ed1; }
+  &.kpi-orange { border-left-color: #fa8c16; }
+  &.kpi-cyan { border-left-color: #13c2c2; }
+
+  .kpi-label {
+    font-size: 13px;
+    color: #86909c;
+    margin-bottom: 6px;
   }
-  
+
+  .kpi-value {
+    font-size: 22px;
+    font-weight: 700;
+    color: #1d2129;
+    line-height: 1.2;
+  }
+
+  .kpi-sub {
+    font-size: 12px;
+    color: #86909c;
+    margin-top: 4px;
+  }
+}
+
+.chart-row { margin-bottom: 12px; }
+
+.chart-box {
+  background: #fff;
+  border-radius: 8px;
+  padding: 16px;
+  height: 380px;
+
+  .chart-title {
+    font-size: 14px;
+    font-weight: 600;
+    color: #1d2129;
+    margin-bottom: 12px;
+  }
+
   .chart-container {
     width: 100%;
-    height: 100%;
-    min-height: 280px;
+    height: calc(100% - 30px);
+  }
+}
+
+.table-box {
+  background: #fff;
+  border-radius: 8px;
+  padding: 16px;
+
+  .table-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 12px;
+  }
+
+  .table-title {
+    font-size: 14px;
+    font-weight: 600;
+    color: #1d2129;
+  }
+
+  .table-actions {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+
+  .table-footer {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 16px;
   }
 }
+
+@media (max-width: 768px) {
+  .kpi-value { font-size: 18px !important; }
+  .chart-box { height: 280px; }
+}
 </style>

+ 297 - 455
haha-admin-web/src/views/statistics/repurchase/index.vue

@@ -1,12 +1,8 @@
 <script setup lang="ts">
-import { ref, onMounted, reactive } from "vue";
+import { ref, onMounted, onUnmounted, reactive } from "vue";
 import * as echarts from "echarts";
-import { 
-  getRepurchaseOverview, 
-  getRepurchaseDistribution, 
-  getRepurchaseTrend, 
-  getRepurchaseUsers 
-} from "@/api/statistics";
+import { getRepurchaseOverview, getRepurchaseDistribution, getRepurchaseTrend, getRepurchaseUsers } from "@/api/statistics";
+import type { EChartsOption } from "echarts";
 
 defineOptions({
   name: "RepurchaseStatistics"
@@ -17,6 +13,14 @@ const dateRange = ref<[Date, Date]>([
   new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
   new Date()
 ]);
+const activeShortcut = ref("30d");
+
+const shortcuts = [
+  { key: "today", label: "今日" },
+  { key: "week", label: "本周" },
+  { key: "month", label: "本月" },
+  { key: "30d", label: "近30天" }
+];
 
 const queryParams = reactive({
   page: 1,
@@ -38,7 +42,7 @@ const overviewData = ref({
   ltv: 0
 });
 
-const tableData = ref([]);
+const tableData = ref<any[]>([]);
 const total = ref(0);
 
 const layerChartRef = ref<HTMLElement | null>(null);
@@ -48,537 +52,375 @@ let layerChart: echarts.ECharts | null = null;
 let intervalChart: echarts.ECharts | null = null;
 let trendChart: echarts.ECharts | null = null;
 
+function formatDate(date: Date): string { return date.toISOString().split("T")[0]; }
+
+function setDateShortcut(type: string) {
+  activeShortcut.value = type;
+  const today = new Date(); today.setHours(23, 59, 59, 999);
+  let start: Date;
+  switch (type) {
+    case "today": start = new Date(); start.setHours(0, 0, 0, 0); break;
+    case "week": start = new Date(today); start.setDate(today.getDate() - 6); start.setHours(0, 0, 0, 0); break;
+    case "month": start = new Date(today.getFullYear(), today.getMonth(), 1); break;
+    case "30d": default: start = new Date(today); start.setDate(today.getDate() - 29); start.setHours(0, 0, 0, 0); break;
+  }
+  dateRange.value = [start, today];
+  fetchAll();
+}
+
+function handleDateChange() { activeShortcut.value = ""; fetchAll(); }
+
+function fetchAll() {
+  fetchOverview();
+  fetchDistribution();
+  fetchTrend();
+  fetchUserList();
+}
+
 async function fetchOverview() {
   try {
-    const params = {
-      startDate: formatDate(dateRange.value[0]),
-      endDate: formatDate(dateRange.value[1])
-    };
-    
-    const { data } = await getRepurchaseOverview(params);
-    overviewData.value = data;
-  } catch (error) {
-    console.error("获取复购概览失败:", error);
-  }
+    const params = { startDate: formatDate(dateRange.value[0]), endDate: formatDate(dateRange.value[1]) };
+    const res = await getRepurchaseOverview(params);
+    if (res.code === 200 && res.data) overviewData.value = res.data;
+  } catch (e) { console.error(e); }
 }
 
 async function fetchDistribution() {
   try {
-    const params = {
-      startDate: formatDate(dateRange.value[0]),
-      endDate: formatDate(dateRange.value[1]),
-      type: "layer"
-    };
-    
-    const { data } = await getRepurchaseDistribution(params);
-    initLayerChart(data.distribution);
-    
-    const intervalParams = {
-      startDate: formatDate(dateRange.value[0]),
-      endDate: formatDate(dateRange.value[1]),
-      type: "interval"
-    };
-    
-    const intervalRes = await getRepurchaseDistribution(intervalParams);
-    initIntervalChart(intervalRes.data.distribution);
-  } catch (error) {
-    console.error("获取复购分布失败:", error);
-  }
+    const baseParams = { startDate: formatDate(dateRange.value[0]), endDate: formatDate(dateRange.value[1]) };
+    const layerRes = await getRepurchaseDistribution({ ...baseParams, type: "layer" });
+    if (layerRes.code === 200 && layerRes.data) initLayerChart(layerRes.data.distribution);
+    const intervalRes = await getRepurchaseDistribution({ ...baseParams, type: "interval" });
+    if (intervalRes.code === 200 && intervalRes.data) initIntervalChart(intervalRes.data.distribution);
+  } catch (e) { console.error(e); }
 }
 
 async function fetchTrend() {
   try {
-    const params = {
-      startDate: formatDate(dateRange.value[0]),
-      endDate: formatDate(dateRange.value[1])
-    };
-    
-    const { data } = await getRepurchaseTrend(params);
-    initTrendChart(data);
-  } catch (error) {
-    console.error("获取复购趋势失败:", error);
-  }
+    const params = { startDate: formatDate(dateRange.value[0]), endDate: formatDate(dateRange.value[1]) };
+    const res = await getRepurchaseTrend(params);
+    if (res.code === 200 && res.data) initTrendChart(res.data);
+  } catch (e) { console.error(e); }
 }
 
 async function fetchUserList() {
   loading.value = true;
   try {
-    const params = {
-      ...queryParams,
-      startDate: formatDate(dateRange.value[0]),
-      endDate: formatDate(dateRange.value[1])
-    };
-    
-    const { data } = await getRepurchaseUsers(params);
-    tableData.value = data.list || [];
-    total.value = Number(data.total) || 0;
-  } catch (error) {
-    console.error("获取用户复购数据失败:", error);
-  } finally {
-    loading.value = false;
-  }
-}
-
-function formatDate(date: Date): string {
-  return date.toISOString().split("T")[0];
+    const params: any = { ...queryParams, startDate: formatDate(dateRange.value[0]), endDate: formatDate(dateRange.value[1]) };
+    const res = await getRepurchaseUsers(params);
+    if (res.code === 200 && res.data) {
+      tableData.value = res.data.list || [];
+      total.value = Number(res.data.total) || 0;
+    }
+  } catch (e) { console.error(e); } finally { loading.value = false; }
 }
 
 function initLayerChart(distribution: any) {
   if (!layerChartRef.value) return;
-  
-  if (layerChart) {
-    layerChart.dispose();
-  }
-  
+  layerChart?.dispose();
   layerChart = echarts.init(layerChartRef.value);
-  
-  const data = Object.entries(distribution || {}).map(([name, value]) => ({
-    name,
-    value
-  }));
-  
-  const option: echarts.EChartsOption = {
-    title: {
-      text: "用户分层分布",
-      left: "center",
-      textStyle: { fontSize: 14, fontWeight: "normal" }
-    },
-    tooltip: {
-      trigger: "item",
-      formatter: "{a} <br/>{b}: {c} ({d}%)"
-    },
-    legend: {
-      bottom: "5%",
-      left: "center"
-    },
-    series: [
-      {
-        name: "用户层级",
-        type: "pie",
-        radius: ["40%", "70%"],
-        avoidLabelOverlap: false,
-        itemStyle: {
-          borderRadius: 10,
-          borderColor: "#fff",
-          borderWidth: 2
-        },
-        label: {
-          show: false,
-          position: "center"
-        },
-        emphasis: {
-          label: {
-            show: true,
-            fontSize: 16,
-            fontWeight: "bold"
-          }
-        },
-        labelLine: {
-          show: false
-        },
-        data: data.map((item: any, index: number) => ({
-          ...item,
-          itemStyle: {
-            color: ["#409eff", "#67c23a", "#e6a23c", "#f56c6c"][index % 4]
-          }
-        }))
-      }
-    ]
+  const chartData = Object.entries(distribution || {}).map(([name, value]) => ({ name, value }));
+  const option: EChartsOption = {
+    tooltip: { trigger: "item", formatter: "{b}: {c} ({d}%)" },
+    legend: { bottom: "0%", left: "center", textStyle: { fontSize: 11 } },
+    series: [{
+      name: "用户层级", type: "pie", radius: ["45%", "72%"], avoidLabelOverlap: false,
+      itemStyle: { borderRadius: 6, borderColor: "#fff", borderWidth: 2 },
+      label: { show: false }, emphasis: { label: { show: true, fontSize: 14, fontWeight: "bold" } }, labelLine: { show: false },
+      data: chartData.map((item: any, i: number) => ({ ...item, itemStyle: { color: ["#409eff", "#67c23a", "#e6a23c", "#f56c6c"][i % 4] } }))
+    }]
   };
-  
   layerChart.setOption(option);
 }
 
 function initIntervalChart(distribution: any) {
   if (!intervalChartRef.value) return;
-  
-  if (intervalChart) {
-    intervalChart.dispose();
-  }
-  
+  intervalChart?.dispose();
   intervalChart = echarts.init(intervalChartRef.value);
-  
   const categories = Object.keys(distribution || {});
   const values = Object.values(distribution || {});
-  
-  const option: echarts.EChartsOption = {
-    title: {
-      text: "复购间隔分布",
-      left: "center",
-      textStyle: { fontSize: 14, fontWeight: "normal" }
-    },
-    tooltip: {
-      trigger: "axis",
-      axisPointer: { type: "shadow" }
-    },
-    grid: {
-      left: "3%",
-      right: "4%",
-      bottom: "3%",
-      containLabel: true
-    },
-    xAxis: {
-      type: "category",
-      data: categories
-    },
-    yAxis: {
-      type: "value",
-      name: "用户数"
-    },
-    series: [
-      {
-        name: "用户数",
-        type: "bar",
-        data: values,
-        itemStyle: {
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: "#83bff6" },
-            { offset: 1, color: "#188df0" }
-          ])
-        }
-      }
-    ]
+  const option: EChartsOption = {
+    tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
+    grid: { left: "3%", right: "4%", bottom: "3%", top: "8%", containLabel: true },
+    xAxis: { type: "category", data: categories, axisLabel: { rotate: 30, fontSize: 10 } },
+    yAxis: { type: "value", name: "用户数" },
+    series: [{ name: "用户数", type: "bar", data: values, barWidth: "50%", itemStyle: { borderRadius: [4, 4, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: "#83bff6" }, { offset: 1, color: "#188df0" }]) } }]
   };
-  
   intervalChart.setOption(option);
 }
 
 function initTrendChart(data: any) {
   if (!trendChartRef.value) return;
-  
-  if (trendChart) {
-    trendChart.dispose();
-  }
-  
+  trendChart?.dispose();
   trendChart = echarts.init(trendChartRef.value);
-  
-  const option: echarts.EChartsOption = {
-    title: {
-      text: "复购率趋势",
-      left: "center",
-      textStyle: { fontSize: 14, fontWeight: "normal" }
-    },
-    tooltip: {
-      trigger: "axis",
-      backgroundColor: "rgba(255, 255, 255, 0.9)",
-      borderColor: "#e5e7eb",
-      borderWidth: 1
-    },
-    grid: {
-      left: "3%",
-      right: "4%",
-      bottom: "3%",
-      containLabel: true
-    },
-    xAxis: {
-      type: "category",
-      data: data.dates || [],
-      axisLabel: {
-        rotate: 45
-      }
-    },
-    yAxis: {
-      type: "value",
-      name: "复购率(%)"
-    },
-    series: [
-      {
-        name: "复购率",
-        type: "line",
-        smooth: true,
-        data: data.series?.[0]?.data || [],
-        itemStyle: { color: "#722ed1" },
-        areaStyle: {
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: "rgba(114, 46, 209, 0.3)" },
-            { offset: 1, color: "rgba(114, 46, 209, 0.05)" }
-          ])
-        }
-      }
-    ]
+  const option: EChartsOption = {
+    tooltip: { trigger: "axis", backgroundColor: "#fff", borderColor: "#e5e7eb", textStyle: { color: "#333" } },
+    grid: { left: "3%", right: "4%", bottom: "5%", top: "8%", containLabel: true },
+    xAxis: { type: "category", data: data.dates || [], axisLabel: { rotate: 45, fontSize: 10 } },
+    yAxis: { type: "value", name: "复购率(%)", max: 100 },
+    series: [{ name: "复购率", type: "line", smooth: true, data: data.series?.[0]?.data || [], symbol: "circle", symbolSize: 4, lineStyle: { color: "#722ed1", width: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: "rgba(114,46,209,0.25)" }, { offset: 1, color: "rgba(114,46,209,0.02)" }]) } }]
   };
-  
   trendChart.setOption(option);
 }
 
-function handleSearch() {
-  queryParams.page = 1;
-  fetchUserList();
-}
+function handleSearch() { queryParams.page = 1; fetchUserList(); }
+function handleReset() { queryParams.shopId = null; queryParams.userLayer = ""; queryParams.minOrderCount = null; queryParams.page = 1; fetchUserList(); }
+function handleSortChange({ prop, order }: any) { queryParams.sortBy = prop || "totalAmount"; queryParams.sortOrder = order === "ascending" ? "asc" : "desc"; fetchUserList(); }
+function handlePageChange(page: number) { queryParams.page = page; fetchUserList(); }
+function handleSizeChange(size: number) { queryParams.pageSize = size; queryParams.page = 1; fetchUserList(); }
 
-function handleReset() {
-  queryParams.shopId = null;
-  queryParams.userLayer = "";
-  queryParams.minOrderCount = null;
-  queryParams.page = 1;
-  fetchUserList();
-}
-
-function handleSortChange({ prop, order }: any) {
-  queryParams.sortBy = prop || "totalAmount";
-  queryParams.sortOrder = order === "ascending" ? "asc" : "desc";
-  fetchUserList();
-}
-
-function handlePageChange(page: number) {
-  queryParams.page = page;
-  fetchUserList();
-}
-
-function handleSizeChange(size: number) {
-  queryParams.pageSize = size;
-  queryParams.page = 1;
-  fetchUserList();
-}
-
-function handleDateChange() {
-  fetchOverview();
-  fetchDistribution();
-  fetchTrend();
-  fetchUserList();
-}
-
-function resizeCharts() {
-  layerChart?.resize();
-  intervalChart?.resize();
-  trendChart?.resize();
-}
-
-function getUserLayerTagType(layer: string): string {
+function getUserLayerTagType(layer: string): "info" | "success" | "warning" | "danger" {
   switch (layer) {
     case "new": return "info";
     case "active": return "success";
     case "loyal": return "warning";
     case "churn": return "danger";
-    default: return "";
+    default: return "info";
   }
 }
 
+function resizeCharts() { layerChart?.resize(); intervalChart?.resize(); trendChart?.resize(); }
+
 onMounted(() => {
-  fetchOverview();
-  fetchDistribution();
-  fetchTrend();
-  fetchUserList();
+  fetchAll();
   window.addEventListener("resize", resizeCharts);
 });
+onUnmounted(() => {
+  window.removeEventListener("resize", resizeCharts);
+  layerChart?.dispose();
+  intervalChart?.dispose();
+  trendChart?.dispose();
+});
 </script>
 
 <template>
   <div class="repurchase-statistics">
-    <el-card shadow="never" class="mb-4">
-      <div class="flex items-center justify-between">
-        <span class="text-lg font-medium">用户复购统计</span>
-        <el-date-picker
-          v-model="dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          format="YYYY-MM-DD"
-          value-format="YYYY-MM-DD"
-          @change="handleDateChange"
-        />
+    <div class="toolbar">
+      <h2 class="page-title">用户复购统计</h2>
+      <div class="toolbar-right">
+        <div class="date-shortcuts">
+          <el-button v-for="s in shortcuts" :key="s.key" :type="activeShortcut === s.key ? 'primary' : 'default'" size="small" @click="setDateShortcut(s.key)">{{ s.label }}</el-button>
+        </div>
+        <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" size="small" @change="handleDateChange" />
       </div>
-    </el-card>
-
-    <el-row :gutter="20" class="mb-6">
-      <el-col :span="6">
-        <el-card shadow="hover">
-          <div class="stat-card">
-            <div class="stat-icon bg-blue">
-              <i class="ri:user-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">{{ overviewData.totalUsers?.toLocaleString() || 0 }}</div>
-              <div class="stat-label">总用户数</div>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover">
-          <div class="stat-card">
-            <div class="stat-icon bg-green">
-              <i class="ri:user-follow-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">{{ overviewData.repurchaseUsers?.toLocaleString() || 0 }}</div>
-              <div class="stat-label">复购用户数</div>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover">
-          <div class="stat-card">
-            <div class="stat-icon bg-purple">
-              <i class="ri:percent-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">{{ overviewData.repurchaseRate }}%</div>
-              <div class="stat-label">复购率</div>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover">
-          <div class="stat-card">
-            <div class="stat-icon bg-orange">
-              <i class="ri:coins-line"></i>
-            </div>
-            <div class="stat-info">
-              <div class="stat-value">¥{{ overviewData.ltv }}</div>
-              <div class="stat-label">用户LTV</div>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <el-row :gutter="20" class="mb-6">
-      <el-col :span="8">
-        <el-card shadow="hover" class="chart-card">
-          <div ref="layerChartRef" class="chart-container"></div>
-        </el-card>
-      </el-col>
-      <el-col :span="8">
-        <el-card shadow="hover" class="chart-card">
-          <div ref="intervalChartRef" class="chart-container"></div>
-        </el-card>
-      </el-col>
-      <el-col :span="8">
-        <el-card shadow="hover" class="chart-card">
-          <div ref="trendChartRef" class="chart-container"></div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <el-card shadow="never">
-      <template #header>
-        <div class="flex items-center justify-between">
-          <span class="font-medium">用户复购明细</span>
-          <div class="flex items-center gap-2">
-            <el-select v-model="queryParams.userLayer" placeholder="用户层级" clearable style="width: 120px">
+    </div>
+
+    <div v-loading="loading" class="page-body">
+      <el-row :gutter="16" class="kpi-row">
+        <el-col :xs="12" :sm="6" :lg="6">
+          <div class="kpi-card kpi-blue"><div class="kpi-label">总用户数</div><div class="kpi-value">{{ (overviewData.totalUsers || 0).toLocaleString() }}</div></div>
+        </el-col>
+        <el-col :xs="12" :sm="6" :lg="6">
+          <div class="kpi-card kpi-green"><div class="kpi-label">复购用户数</div><div class="kpi-value">{{ (overviewData.repurchaseUsers || 0).toLocaleString() }}</div></div>
+        </el-col>
+        <el-col :xs="12" :sm="6" :lg="6">
+          <div class="kpi-card kpi-purple"><div class="kpi-label">复购率</div><div class="kpi-value">{{ overviewData.repurchaseRate || 0 }}%</div></div>
+        </el-col>
+        <el-col :xs="12" :sm="6" :lg="6">
+          <div class="kpi-card kpi-orange"><div class="kpi-label">用户LTV</div><div class="kpi-value">¥{{ (overviewData.ltv || 0).toLocaleString() }}</div></div>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="16" class="kpi-row">
+        <el-col :xs="12" :sm="8" :lg="8">
+          <div class="kpi-card kpi-cyan"><div class="kpi-label">新用户数</div><div class="kpi-value">{{ (overviewData.newUsers || 0).toLocaleString() }}</div></div>
+        </el-col>
+        <el-col :xs="12" :sm="8" :lg="8">
+          <div class="kpi-card kpi-teal"><div class="kpi-label">平均客单价</div><div class="kpi-value">¥{{ (overviewData.avgOrderAmount || 0).toLocaleString() }}</div></div>
+        </el-col>
+        <el-col :xs="12" :sm="8" :lg="8">
+          <div class="kpi-card kpi-indigo"><div class="kpi-label">平均购买次数</div><div class="kpi-value">{{ overviewData.avgPurchaseCount || 0 }}</div></div>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="16" class="chart-row">
+        <el-col :span="8">
+          <div class="chart-box"><div class="chart-title">用户分层分布</div><div ref="layerChartRef" class="chart-container"></div></div>
+        </el-col>
+        <el-col :span="8">
+          <div class="chart-box"><div class="chart-title">复购间隔分布</div><div ref="intervalChartRef" class="chart-container"></div></div>
+        </el-col>
+        <el-col :span="8">
+          <div class="chart-box"><div class="chart-title">复购率趋势</div><div ref="trendChartRef" class="chart-container"></div></div>
+        </el-col>
+      </el-row>
+
+      <div class="table-box">
+        <div class="table-header">
+          <span class="table-title">用户复购明细</span>
+          <div class="table-actions">
+            <el-select v-model="queryParams.userLayer" placeholder="用户层级" clearable size="small" style="width: 120px">
               <el-option label="新用户" value="new" />
               <el-option label="活跃用户" value="active" />
               <el-option label="忠诚用户" value="loyal" />
               <el-option label="流失用户" value="churn" />
             </el-select>
-            <el-input-number
-              v-model="queryParams.minOrderCount"
-              placeholder="最小购买次数"
-              :min="1"
-              controls-position="right"
-              style="width: 140px"
-            />
-            <el-button type="primary" @click="handleSearch">搜索</el-button>
-            <el-button @click="handleReset">重置</el-button>
+            <el-input-number v-model="queryParams.minOrderCount" placeholder="最小购买次数" :min="1" controls-position="right" size="small" style="width: 140px" />
+            <el-button type="primary" size="small" @click="handleSearch">搜索</el-button>
+            <el-button size="small" @click="handleReset">重置</el-button>
           </div>
         </div>
-      </template>
-      
-      <el-table :data="tableData" stripe v-loading="loading" @sort-change="handleSortChange">
-        <el-table-column prop="nickname" label="用户昵称" width="120" show-overflow-tooltip />
-        <el-table-column prop="phone" label="手机号" width="120" />
-        <el-table-column prop="orderCount" label="购买次数" width="100" align="right" sortable="custom" />
-        <el-table-column prop="totalAmount" label="累计消费(元)" width="120" align="right" sortable="custom">
-          <template #default="{ row }">
-            ¥{{ row.totalAmount?.toLocaleString() || 0 }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="avgOrderAmount" label="平均客单价" width="100" align="right">
-          <template #default="{ row }">
-            ¥{{ row.avgOrderAmount || 0 }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="firstOrderDate" label="首次下单" width="110" />
-        <el-table-column prop="lastOrderDate" label="最近下单" width="110" />
-        <el-table-column prop="repurchaseDays" label="复购间隔(天)" width="110" align="right" />
-        <el-table-column prop="userLayerLabel" label="用户层级" width="100" align="center">
-          <template #default="{ row }">
-            <el-tag :type="getUserLayerTagType(row.userLayer)" size="small">
-              {{ row.userLayerLabel }}
-            </el-tag>
-          </template>
-        </el-table-column>
-      </el-table>
-      
-      <div class="flex justify-end mt-4">
-        <el-pagination
-          v-model:current-page="queryParams.page"
-          v-model:page-size="queryParams.pageSize"
-          :page-sizes="[10, 20, 50, 100]"
-          :total="total"
-          layout="total, sizes, prev, pager, next, jumper"
-          @size-change="handleSizeChange"
-          @current-change="handlePageChange"
-        />
+        <el-table :data="tableData" stripe v-loading="loading" @sort-change="handleSortChange">
+          <el-table-column prop="nickname" label="用户昵称" min-width="120" fixed="left" show-overflow-tooltip />
+          <el-table-column prop="phone" label="手机号" width="120" />
+          <el-table-column prop="userLayerLabel" label="用户层级" width="100" align="center">
+            <template #default="{ row }">
+              <el-tag :type="getUserLayerTagType(row.userLayer)" size="small">{{ row.userLayerLabel }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="orderCount" label="购买次数" width="90" align="right" sortable="custom" />
+          <el-table-column prop="totalAmount" label="累计消费" width="120" align="right" sortable="custom">
+            <template #default="{ row }">¥{{ (row.totalAmount || 0).toLocaleString() }}</template>
+          </el-table-column>
+          <el-table-column prop="avgOrderAmount" label="平均客单价" width="110" align="right">
+            <template #default="{ row }">¥{{ (row.avgOrderAmount || 0).toLocaleString() }}</template>
+          </el-table-column>
+          <el-table-column prop="firstOrderDate" label="首次下单" width="110" />
+          <el-table-column prop="lastOrderDate" label="最近下单" width="110" />
+          <el-table-column prop="repurchaseDays" label="复购间隔(天)" width="110" align="right" />
+        </el-table>
+        <div class="table-footer">
+          <el-pagination
+            v-model:current-page="queryParams.page"
+            v-model:page-size="queryParams.pageSize"
+            :page-sizes="[10, 20, 50, 100]"
+            :total="total"
+            layout="total, sizes, prev, pager, next, jumper"
+            background small
+            @size-change="handleSizeChange"
+            @current-change="handlePageChange"
+          />
+        </div>
       </div>
-    </el-card>
+    </div>
   </div>
 </template>
 
 <style lang="scss" scoped>
 .repurchase-statistics {
-  padding: 20px;
-  background-color: var(--el-bg-color-page);
+  padding: 16px 20px;
+  background-color: #f5f7fa;
   min-height: calc(100vh - 120px);
 }
 
-.stat-card {
+.toolbar {
   display: flex;
   align-items: center;
-  
-  .stat-icon {
-    width: 50px;
-    height: 50px;
-    border-radius: 12px;
+  justify-content: space-between;
+  margin-bottom: 16px;
+  flex-wrap: wrap;
+  gap: 12px;
+
+  .page-title {
+    margin: 0;
+    font-size: 18px;
+    font-weight: 600;
+    color: #1d2129;
+  }
+
+  .toolbar-right {
     display: flex;
     align-items: center;
-    justify-content: center;
-    margin-right: 16px;
-    
-    i {
-      font-size: 24px;
-      color: white;
-    }
-    
-    &.bg-blue { background: linear-gradient(135deg, #409eff, #337ecc); }
-    &.bg-green { background: linear-gradient(135deg, #67c23a, #529b2e); }
-    &.bg-purple { background: linear-gradient(135deg, #722ed1, #531dab); }
-    &.bg-orange { background: linear-gradient(135deg, #fa8c16, #d46b08); }
+    gap: 12px;
+    flex-wrap: wrap;
   }
-  
-  .stat-info {
-    flex: 1;
-    
-    .stat-value {
-      font-size: 24px;
-      font-weight: 600;
-      color: var(--el-text-color-primary);
-      margin-bottom: 4px;
-    }
-    
-    .stat-label {
-      font-size: 14px;
-      color: var(--el-text-color-secondary);
-    }
+
+  .date-shortcuts {
+    display: flex;
+    gap: 4px;
   }
 }
 
-.chart-card {
-  height: 320px;
-  
-  :deep(.el-card__body) {
-    padding: 20px;
-    height: 100%;
+.page-body { min-height: 400px; }
+
+.kpi-row { margin-bottom: 12px; }
+
+.kpi-card {
+  background: #fff;
+  border-radius: 8px;
+  padding: 16px 20px;
+  height: 100%;
+  border-left: 4px solid #409eff;
+  transition: box-shadow 0.2s;
+
+  &:hover { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); }
+
+  &.kpi-blue { border-left-color: #409eff; }
+  &.kpi-green { border-left-color: #67c23a; }
+  &.kpi-purple { border-left-color: #722ed1; }
+  &.kpi-orange { border-left-color: #fa8c16; }
+  &.kpi-cyan { border-left-color: #13c2c2; }
+  &.kpi-teal { border-left-color: #52c41a; }
+  &.kpi-indigo { border-left-color: #597ef7; }
+
+  .kpi-label {
+    font-size: 13px;
+    color: #86909c;
+    margin-bottom: 6px;
   }
-  
+
+  .kpi-value {
+    font-size: 22px;
+    font-weight: 700;
+    color: #1d2129;
+    line-height: 1.2;
+  }
+}
+
+.chart-row { margin-bottom: 12px; }
+
+.chart-box {
+  background: #fff;
+  border-radius: 8px;
+  padding: 16px;
+  height: 340px;
+
+  .chart-title {
+    font-size: 14px;
+    font-weight: 600;
+    color: #1d2129;
+    margin-bottom: 8px;
+  }
+
   .chart-container {
     width: 100%;
-    height: 100%;
-    min-height: 260px;
+    height: calc(100% - 26px);
+  }
+}
+
+.table-box {
+  background: #fff;
+  border-radius: 8px;
+  padding: 16px;
+
+  .table-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 12px;
+  }
+
+  .table-title {
+    font-size: 14px;
+    font-weight: 600;
+    color: #1d2129;
+  }
+
+  .table-actions {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+
+  .table-footer {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 16px;
   }
 }
+
+@media (max-width: 768px) {
+  .kpi-value { font-size: 18px !important; }
+  .chart-box { height: 280px; }
+}
 </style>