Merge branch 'master' of http://gitlab.erry.com/zhouhui.jiang/test_cursor
Showing
30 changed files
with
2763 additions
and
386 deletions
| ... | @@ -27,7 +27,55 @@ | ... | @@ -27,7 +27,55 @@ |
| 27 | - `message`: 响应消息 | 27 | - `message`: 响应消息 |
| 28 | - `data`: 响应数据,具体内容根据接口而定 | 28 | - `data`: 响应数据,具体内容根据接口而定 |
| 29 | 29 | ||
| 30 | -## 1. 认证管理 (AuthController) | 30 | +## 1. 首页仪表板 (DashboardController) |
| 31 | + | ||
| 32 | +### 1.1 获取首页统计数据 | ||
| 33 | + | ||
| 34 | +**接口路径:** `GET /api/dashboard/stats` | ||
| 35 | + | ||
| 36 | +**功能描述:** 获取首页仪表板的统计数据,包括今日订单、待出库、待开票、销售额等信息 | ||
| 37 | + | ||
| 38 | +**请求头:** | ||
| 39 | +``` | ||
| 40 | +Authorization: Bearer <JWT_TOKEN> | ||
| 41 | +``` | ||
| 42 | + | ||
| 43 | +**响应示例:** | ||
| 44 | +```json | ||
| 45 | +{ | ||
| 46 | + "code": 200, | ||
| 47 | + "message": "获取统计数据成功", | ||
| 48 | + "data": { | ||
| 49 | + "todayOrderCount": 23, | ||
| 50 | + "pendingDeliveryCount": 8, | ||
| 51 | + "pendingInvoiceCount": 5, | ||
| 52 | + "todaySalesAmount": 125680.50, | ||
| 53 | + "onlineUserCount": 156, | ||
| 54 | + "systemStatus": "正常运行", | ||
| 55 | + "systemVersion": "v1.0.0" | ||
| 56 | + } | ||
| 57 | +} | ||
| 58 | +``` | ||
| 59 | + | ||
| 60 | +**错误响应示例:** | ||
| 61 | +```json | ||
| 62 | +{ | ||
| 63 | + "code": 500, | ||
| 64 | + "message": "获取统计数据失败: 数据库连接异常", | ||
| 65 | + "data": null | ||
| 66 | +} | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +**响应字段说明:** | ||
| 70 | +- `todayOrderCount`: 今日订单数量 | ||
| 71 | +- `pendingDeliveryCount`: 待出库数量 | ||
| 72 | +- `pendingInvoiceCount`: 待开票数量 | ||
| 73 | +- `todaySalesAmount`: 今日销售额 | ||
| 74 | +- `onlineUserCount`: 在线用户数 | ||
| 75 | +- `systemStatus`: 系统状态 | ||
| 76 | +- `systemVersion`: 系统版本 | ||
| 77 | + | ||
| 78 | +## 2. 认证管理 (AuthController) | ||
| 31 | 79 | ||
| 32 | ### 1.1 用户登录 | 80 | ### 1.1 用户登录 |
| 33 | 81 | ||
| ... | @@ -2491,8 +2539,418 @@ | ... | @@ -2491,8 +2539,418 @@ |
| 2491 | 2539 | ||
| 2492 | **响应:** 返回Excel文件流,文件名格式:`发票数据_yyyyMMdd_HHmmss.xlsx` | 2540 | **响应:** 返回Excel文件流,文件名格式:`发票数据_yyyyMMdd_HHmmss.xlsx` |
| 2493 | 2541 | ||
| 2542 | +### 4. 异常工单数据导出 | ||
| 2543 | + | ||
| 2544 | +**接口路径:** `POST /api/exception-workorder/export` | ||
| 2545 | +**请求方法:** POST | ||
| 2546 | +**权限要求:** `exception:workorder:export` | ||
| 2547 | + | ||
| 2548 | +**请求参数:** 同异常工单查询接口参数 | ||
| 2549 | + | ||
| 2550 | +**响应:** 返回Excel文件流,文件名格式:`异常工单数据_yyyyMMdd_HHmmss.xlsx` | ||
| 2551 | + | ||
| 2552 | +## 12. 异常工单管理 (ExceptionWorkorderController) | ||
| 2553 | + | ||
| 2554 | +### 12.1 分页查询异常工单列表 | ||
| 2555 | + | ||
| 2556 | +**接口路径:** `GET /api/exception-workorder/list` | ||
| 2557 | + | ||
| 2558 | +**功能描述:** 根据条件分页查询异常工单列表 | ||
| 2559 | + | ||
| 2560 | +**请求头:** | ||
| 2561 | +``` | ||
| 2562 | +Authorization: Bearer <JWT_TOKEN> | ||
| 2563 | +``` | ||
| 2564 | + | ||
| 2565 | +**请求参数:** | ||
| 2566 | +| 参数名 | 类型 | 必填 | 说明 | | ||
| 2567 | +|--------|------|------|------| | ||
| 2568 | +| workorderNo | String | 否 | 工单编号(模糊查询) | | ||
| 2569 | +| orderNo | String | 否 | 关联订单号(模糊查询) | | ||
| 2570 | +| dealerCode | String | 否 | 经销商编码 | | ||
| 2571 | +| dealerName | String | 否 | 经销商名称(模糊查询) | | ||
| 2572 | +| workorderStatus | Integer | 否 | 工单状态(1-待处理/2-处理中/3-已解决/4-已关闭) | | ||
| 2573 | +| exceptionType | Integer | 否 | 异常类型(1-逻辑验证异常/2-源头验证异常/3-交叉验证异常) | | ||
| 2574 | +| severityLevel | Integer | 否 | 严重程度(1-高/2-中/3-低) | | ||
| 2575 | +| handlerUser | String | 否 | 处理人(模糊查询) | | ||
| 2576 | +| startTime | String | 否 | 创建开始时间(yyyy-MM-dd HH:mm:ss) | | ||
| 2577 | +| endTime | String | 否 | 创建结束时间(yyyy-MM-dd HH:mm:ss) | | ||
| 2578 | +| pageNum | Integer | 是 | 页码,默认1 | | ||
| 2579 | +| pageSize | Integer | 是 | 每页大小,默认10 | | ||
| 2580 | + | ||
| 2581 | +**权限要求:** `exception:workorder:list` | ||
| 2582 | + | ||
| 2583 | +**响应示例:** | ||
| 2584 | +```json | ||
| 2585 | +{ | ||
| 2586 | + "code": 200, | ||
| 2587 | + "message": "操作成功", | ||
| 2588 | + "data": { | ||
| 2589 | + "records": [ | ||
| 2590 | + { | ||
| 2591 | + "workorderId": 1, | ||
| 2592 | + "workorderNo": "EW202501290001", | ||
| 2593 | + "orderNo": "ORD202501290001", | ||
| 2594 | + "dealerCode": "D001", | ||
| 2595 | + "dealerName": "北京经销商", | ||
| 2596 | + "exceptionType": 1, | ||
| 2597 | + "exceptionTypeName": "逻辑验证异常", | ||
| 2598 | + "severityLevel": 1, | ||
| 2599 | + "severityLevelName": "高", | ||
| 2600 | + "workorderStatus": 1, | ||
| 2601 | + "workorderStatusName": "待处理", | ||
| 2602 | + "createTime": "2025-01-29 10:00:00", | ||
| 2603 | + "expectCompleteTime": "2025-01-30 18:00:00", | ||
| 2604 | + "handlerUser": "张三", | ||
| 2605 | + "exceptionDesc": "订单金额与产品单价不匹配", | ||
| 2606 | + "handleSuggest": "请核实订单明细", | ||
| 2607 | + "dataSource": "系统自动生成", | ||
| 2608 | + "createBy": "system", | ||
| 2609 | + "updateBy": null, | ||
| 2610 | + "updateTime": null, | ||
| 2611 | + "workorderLogs": [] | ||
| 2612 | + } | ||
| 2613 | + ], | ||
| 2614 | + "total": 1, | ||
| 2615 | + "size": 10, | ||
| 2616 | + "current": 1, | ||
| 2617 | + "pages": 1 | ||
| 2618 | + } | ||
| 2619 | +} | ||
| 2620 | +``` | ||
| 2621 | + | ||
| 2622 | +### 12.2 获取异常工单详情 | ||
| 2623 | + | ||
| 2624 | +**接口路径:** `GET /api/exception-workorder/{workorderId}` | ||
| 2625 | + | ||
| 2626 | +**功能描述:** 根据工单ID获取异常工单详细信息,包含处理日志 | ||
| 2627 | + | ||
| 2628 | +**请求头:** | ||
| 2629 | +``` | ||
| 2630 | +Authorization: Bearer <JWT_TOKEN> | ||
| 2631 | +``` | ||
| 2632 | + | ||
| 2633 | +**路径参数:** | ||
| 2634 | +| 参数名 | 类型 | 必填 | 说明 | | ||
| 2635 | +|--------|------|------|------| | ||
| 2636 | +| workorderId | Long | 是 | 工单ID | | ||
| 2637 | + | ||
| 2638 | +**权限要求:** `exception:workorder:detail` | ||
| 2639 | + | ||
| 2640 | +**响应示例:** | ||
| 2641 | +```json | ||
| 2642 | +{ | ||
| 2643 | + "code": 200, | ||
| 2644 | + "message": "操作成功", | ||
| 2645 | + "data": { | ||
| 2646 | + "workorderId": 1, | ||
| 2647 | + "workorderNo": "EW202501290001", | ||
| 2648 | + "orderNo": "ORD202501290001", | ||
| 2649 | + "dealerCode": "D001", | ||
| 2650 | + "dealerName": "北京经销商", | ||
| 2651 | + "exceptionType": 1, | ||
| 2652 | + "exceptionTypeName": "逻辑验证异常", | ||
| 2653 | + "severityLevel": 1, | ||
| 2654 | + "severityLevelName": "高", | ||
| 2655 | + "workorderStatus": 2, | ||
| 2656 | + "workorderStatusName": "处理中", | ||
| 2657 | + "createTime": "2025-01-29 10:00:00", | ||
| 2658 | + "expectCompleteTime": "2025-01-30 18:00:00", | ||
| 2659 | + "handlerUser": "张三", | ||
| 2660 | + "exceptionDesc": "订单金额与产品单价不匹配", | ||
| 2661 | + "handleSuggest": "请核实订单明细", | ||
| 2662 | + "dataSource": "系统自动生成", | ||
| 2663 | + "createBy": "system", | ||
| 2664 | + "updateBy": "张三", | ||
| 2665 | + "updateTime": "2025-01-29 11:00:00", | ||
| 2666 | + "workorderLogs": [ | ||
| 2667 | + { | ||
| 2668 | + "logId": 1, | ||
| 2669 | + "workorderId": 1, | ||
| 2670 | + "workorderNo": "EW202501290001", | ||
| 2671 | + "handleUser": "张三", | ||
| 2672 | + "handleTime": "2025-01-29 11:00:00", | ||
| 2673 | + "beforeStatus": 1, | ||
| 2674 | + "beforeStatusName": "待处理", | ||
| 2675 | + "afterStatus": 2, | ||
| 2676 | + "afterStatusName": "处理中", | ||
| 2677 | + "handleOpinion": "开始处理此工单", | ||
| 2678 | + "attachUrl": null, | ||
| 2679 | + "createBy": "张三", | ||
| 2680 | + "createTime": "2025-01-29 11:00:00" | ||
| 2681 | + } | ||
| 2682 | + ] | ||
| 2683 | + } | ||
| 2684 | +} | ||
| 2685 | +``` | ||
| 2686 | + | ||
| 2687 | +### 12.3 新增异常工单 | ||
| 2688 | + | ||
| 2689 | +**接口路径:** `POST /api/exception-workorder` | ||
| 2690 | + | ||
| 2691 | +**功能描述:** 创建新的异常工单 | ||
| 2692 | + | ||
| 2693 | +**请求头:** | ||
| 2694 | +``` | ||
| 2695 | +Authorization: Bearer <JWT_TOKEN> | ||
| 2696 | +Content-Type: application/json | ||
| 2697 | +``` | ||
| 2698 | + | ||
| 2699 | +**请求体:** | ||
| 2700 | +```json | ||
| 2701 | +{ | ||
| 2702 | + "workorderNo": "EW202501290002", | ||
| 2703 | + "orderNo": "ORD202501290002", | ||
| 2704 | + "dealerCode": "D002", | ||
| 2705 | + "dealerName": "上海经销商", | ||
| 2706 | + "exceptionType": 2, | ||
| 2707 | + "severityLevel": 2, | ||
| 2708 | + "workorderStatus": 1, | ||
| 2709 | + "expectCompleteTime": "2025-01-30 18:00:00", | ||
| 2710 | + "handlerUser": "李四", | ||
| 2711 | + "exceptionDesc": "经销商信息不完整", | ||
| 2712 | + "handleSuggest": "请补充经销商详细信息", | ||
| 2713 | + "dataSource": "手动创建" | ||
| 2714 | +} | ||
| 2715 | +``` | ||
| 2716 | + | ||
| 2717 | +**权限要求:** `exception:workorder:add` | ||
| 2718 | + | ||
| 2719 | +**响应示例:** | ||
| 2720 | +```json | ||
| 2721 | +{ | ||
| 2722 | + "code": 200, | ||
| 2723 | + "message": "新增异常工单成功", | ||
| 2724 | + "data": null | ||
| 2725 | +} | ||
| 2726 | +``` | ||
| 2727 | + | ||
| 2728 | +### 12.4 更新异常工单 | ||
| 2729 | + | ||
| 2730 | +**接口路径:** `PUT /api/exception-workorder` | ||
| 2731 | + | ||
| 2732 | +**功能描述:** 更新异常工单信息 | ||
| 2733 | + | ||
| 2734 | +**请求头:** | ||
| 2735 | +``` | ||
| 2736 | +Authorization: Bearer <JWT_TOKEN> | ||
| 2737 | +Content-Type: application/json | ||
| 2738 | +``` | ||
| 2739 | + | ||
| 2740 | +**请求体:** | ||
| 2741 | +```json | ||
| 2742 | +{ | ||
| 2743 | + "workorderId": 1, | ||
| 2744 | + "workorderNo": "EW202501290001", | ||
| 2745 | + "orderNo": "ORD202501290001", | ||
| 2746 | + "dealerCode": "D001", | ||
| 2747 | + "dealerName": "北京经销商", | ||
| 2748 | + "exceptionType": 1, | ||
| 2749 | + "severityLevel": 1, | ||
| 2750 | + "workorderStatus": 2, | ||
| 2751 | + "expectCompleteTime": "2025-01-30 18:00:00", | ||
| 2752 | + "handlerUser": "张三", | ||
| 2753 | + "exceptionDesc": "订单金额与产品单价不匹配", | ||
| 2754 | + "handleSuggest": "请核实订单明细并联系客户确认", | ||
| 2755 | + "dataSource": "系统自动生成" | ||
| 2756 | +} | ||
| 2757 | +``` | ||
| 2758 | + | ||
| 2759 | +**权限要求:** `exception:workorder:edit` | ||
| 2760 | + | ||
| 2761 | +**响应示例:** | ||
| 2762 | +```json | ||
| 2763 | +{ | ||
| 2764 | + "code": 200, | ||
| 2765 | + "message": "更新异常工单成功", | ||
| 2766 | + "data": null | ||
| 2767 | +} | ||
| 2768 | +``` | ||
| 2769 | + | ||
| 2770 | +### 12.5 更新工单状态 | ||
| 2771 | + | ||
| 2772 | +**接口路径:** `POST /api/exception-workorder/status` | ||
| 2773 | + | ||
| 2774 | +**功能描述:** 更新异常工单状态并记录处理日志 | ||
| 2775 | + | ||
| 2776 | +**请求头:** | ||
| 2777 | +``` | ||
| 2778 | +Authorization: Bearer <JWT_TOKEN> | ||
| 2779 | +Content-Type: application/json | ||
| 2780 | +``` | ||
| 2781 | + | ||
| 2782 | +**请求体:** | ||
| 2783 | +```json | ||
| 2784 | +{ | ||
| 2785 | + "workorderId": 1, | ||
| 2786 | + "workorderStatus": 3, | ||
| 2787 | + "handlerUser": "张三", | ||
| 2788 | + "handleOpinion": "问题已解决,客户确认无误", | ||
| 2789 | + "attachUrl": "http://example.com/attachment.pdf" | ||
| 2790 | +} | ||
| 2791 | +``` | ||
| 2792 | + | ||
| 2793 | +**权限要求:** `exception:workorder:edit` | ||
| 2794 | + | ||
| 2795 | +**响应示例:** | ||
| 2796 | +```json | ||
| 2797 | +{ | ||
| 2798 | + "code": 200, | ||
| 2799 | + "message": "更新工单状态成功", | ||
| 2800 | + "data": null | ||
| 2801 | +} | ||
| 2802 | +``` | ||
| 2803 | + | ||
| 2804 | +### 12.6 删除异常工单 | ||
| 2805 | + | ||
| 2806 | +**接口路径:** `DELETE /api/exception-workorder/{workorderId}` | ||
| 2807 | + | ||
| 2808 | +**功能描述:** 根据工单ID逻辑删除异常工单 | ||
| 2809 | + | ||
| 2810 | +**请求头:** | ||
| 2811 | +``` | ||
| 2812 | +Authorization: Bearer <JWT_TOKEN> | ||
| 2813 | +``` | ||
| 2814 | + | ||
| 2815 | +**路径参数:** | ||
| 2816 | +| 参数名 | 类型 | 必填 | 说明 | | ||
| 2817 | +|--------|------|------|------| | ||
| 2818 | +| workorderId | Long | 是 | 工单ID | | ||
| 2819 | + | ||
| 2820 | +**权限要求:** `exception:workorder:delete` | ||
| 2821 | + | ||
| 2822 | +**响应示例:** | ||
| 2823 | +```json | ||
| 2824 | +{ | ||
| 2825 | + "code": 200, | ||
| 2826 | + "message": "删除异常工单成功", | ||
| 2827 | + "data": null | ||
| 2828 | +} | ||
| 2829 | +``` | ||
| 2830 | + | ||
| 2831 | +### 12.7 批量删除异常工单 | ||
| 2832 | + | ||
| 2833 | +**接口路径:** `DELETE /api/exception-workorder/batch` | ||
| 2834 | + | ||
| 2835 | +**功能描述:** 根据工单ID列表批量逻辑删除异常工单 | ||
| 2836 | + | ||
| 2837 | +**请求头:** | ||
| 2838 | +``` | ||
| 2839 | +Authorization: Bearer <JWT_TOKEN> | ||
| 2840 | +Content-Type: application/json | ||
| 2841 | +``` | ||
| 2842 | + | ||
| 2843 | +**请求体:** | ||
| 2844 | +```json | ||
| 2845 | +[1, 2, 3] | ||
| 2846 | +``` | ||
| 2847 | + | ||
| 2848 | +**权限要求:** `exception:workorder:delete` | ||
| 2849 | + | ||
| 2850 | +**响应示例:** | ||
| 2851 | +```json | ||
| 2852 | +{ | ||
| 2853 | + "code": 200, | ||
| 2854 | + "message": "批量删除异常工单成功", | ||
| 2855 | + "data": null | ||
| 2856 | +} | ||
| 2857 | +``` | ||
| 2858 | + | ||
| 2859 | +### 12.8 批量更新工单状态 | ||
| 2860 | + | ||
| 2861 | +**接口路径:** `POST /api/exception-workorder/batch-status` | ||
| 2862 | + | ||
| 2863 | +**功能描述:** 批量更新异常工单状态 | ||
| 2864 | + | ||
| 2865 | +**请求头:** | ||
| 2866 | +``` | ||
| 2867 | +Authorization: Bearer <JWT_TOKEN> | ||
| 2868 | +``` | ||
| 2869 | + | ||
| 2870 | +**请求参数:** | ||
| 2871 | +| 参数名 | 类型 | 必填 | 说明 | | ||
| 2872 | +|--------|------|------|------| | ||
| 2873 | +| workorderIds | String | 是 | 工单ID列表,逗号分隔 | | ||
| 2874 | +| workorderStatus | Integer | 是 | 工单状态 | | ||
| 2875 | +| handlerUser | String | 是 | 处理人 | | ||
| 2876 | + | ||
| 2877 | +**权限要求:** `exception:workorder:edit` | ||
| 2878 | + | ||
| 2879 | +**响应示例:** | ||
| 2880 | +```json | ||
| 2881 | +{ | ||
| 2882 | + "code": 200, | ||
| 2883 | + "message": "批量更新工单状态成功", | ||
| 2884 | + "data": null | ||
| 2885 | +} | ||
| 2886 | +``` | ||
| 2887 | + | ||
| 2888 | +### 12.9 获取异常工单统计信息 | ||
| 2889 | + | ||
| 2890 | +**接口路径:** `GET /api/exception-workorder/stats` | ||
| 2891 | + | ||
| 2892 | +**功能描述:** 获取异常工单的统计信息 | ||
| 2893 | + | ||
| 2894 | +**请求头:** | ||
| 2895 | +``` | ||
| 2896 | +Authorization: Bearer <JWT_TOKEN> | ||
| 2897 | +``` | ||
| 2898 | + | ||
| 2899 | +**权限要求:** `exception:workorder:list` | ||
| 2900 | + | ||
| 2901 | +**响应示例:** | ||
| 2902 | +```json | ||
| 2903 | +{ | ||
| 2904 | + "code": 200, | ||
| 2905 | + "message": "操作成功", | ||
| 2906 | + "data": [ | ||
| 2907 | + { | ||
| 2908 | + "workorderNo": "total", | ||
| 2909 | + "workorderId": 10, | ||
| 2910 | + "exceptionType": 3, | ||
| 2911 | + "severityLevel": 2, | ||
| 2912 | + "workorderStatus": 4, | ||
| 2913 | + "handlerUser": 1, | ||
| 2914 | + "exceptionDesc": 5, | ||
| 2915 | + "handleSuggest": 3, | ||
| 2916 | + "dataSource": 2 | ||
| 2917 | + } | ||
| 2918 | + ] | ||
| 2919 | +} | ||
| 2920 | +``` | ||
| 2921 | + | ||
| 2922 | +### 12.10 导出异常工单数据 | ||
| 2923 | + | ||
| 2924 | +**接口路径:** `POST /api/exception-workorder/export` | ||
| 2925 | + | ||
| 2926 | +**功能描述:** 根据查询条件导出异常工单数据到Excel | ||
| 2927 | + | ||
| 2928 | +**请求头:** | ||
| 2929 | +``` | ||
| 2930 | +Authorization: Bearer <JWT_TOKEN> | ||
| 2931 | +Content-Type: application/json | ||
| 2932 | +``` | ||
| 2933 | + | ||
| 2934 | +**请求体:** 同异常工单查询接口参数(可选,为空时导出所有数据) | ||
| 2935 | + | ||
| 2936 | +**权限要求:** `exception:workorder:export` | ||
| 2937 | + | ||
| 2938 | +**响应:** 返回Excel文件流,文件名格式:`异常工单数据_yyyyMMdd_HHmmss.xlsx` | ||
| 2939 | + | ||
| 2940 | +**导出字段:** | ||
| 2941 | +- 工单ID | ||
| 2942 | +- 工单编号 | ||
| 2943 | +- 工单类型(异常类型名称) | ||
| 2944 | +- 严重程度(严重程度名称) | ||
| 2945 | +- 工单状态(工单状态名称) | ||
| 2946 | +- 问题描述 | ||
| 2947 | +- 处理人 | ||
| 2948 | +- 创建人 | ||
| 2949 | +- 创建时间 | ||
| 2950 | +- 更新时间 | ||
| 2951 | + | ||
| 2494 | --- | 2952 | --- |
| 2495 | 2953 | ||
| 2496 | **文档版本:** 1.0.0 | 2954 | **文档版本:** 1.0.0 |
| 2497 | -**最后更新:** 2025-01-27 | 2955 | +**最后更新:** 2025-01-29 |
| 2498 | **维护人员:** Apple ERP Team | 2956 | **维护人员:** Apple ERP Team | ... | ... |
| 1 | package com.apple.erp.config; | 1 | package com.apple.erp.config; |
| 2 | - | ||
| 3 | -import com.apple.erp.utils.JwtUtils; | ||
| 4 | import org.springframework.beans.factory.annotation.Autowired; | 2 | import org.springframework.beans.factory.annotation.Autowired; |
| 5 | import org.springframework.context.annotation.Bean; | 3 | import org.springframework.context.annotation.Bean; |
| 6 | import org.springframework.context.annotation.Configuration; | 4 | import org.springframework.context.annotation.Configuration; |
| ... | @@ -34,9 +32,6 @@ import java.util.Arrays; | ... | @@ -34,9 +32,6 @@ import java.util.Arrays; |
| 34 | public class SecurityConfig { | 32 | public class SecurityConfig { |
| 35 | 33 | ||
| 36 | @Autowired | 34 | @Autowired |
| 37 | - private JwtUtils jwtUtils; | ||
| 38 | - | ||
| 39 | - @Autowired | ||
| 40 | private JwtAuthenticationFilter jwtAuthenticationFilter; | 35 | private JwtAuthenticationFilter jwtAuthenticationFilter; |
| 41 | 36 | ||
| 42 | @Autowired | 37 | @Autowired |
| ... | @@ -72,6 +67,7 @@ public class SecurityConfig { | ... | @@ -72,6 +67,7 @@ public class SecurityConfig { |
| 72 | .antMatchers("/api/auth/**").permitAll() | 67 | .antMatchers("/api/auth/**").permitAll() |
| 73 | .antMatchers("/api/public/**").permitAll() | 68 | .antMatchers("/api/public/**").permitAll() |
| 74 | .antMatchers("/api/test/public").permitAll() // 允许测试接口 | 69 | .antMatchers("/api/test/public").permitAll() // 允许测试接口 |
| 70 | + .antMatchers("/api/dashboard/**").authenticated() // 首页统计需要认证 | ||
| 75 | .antMatchers("/actuator/**").permitAll() | 71 | .antMatchers("/actuator/**").permitAll() |
| 76 | .antMatchers("/druid/**").permitAll() | 72 | .antMatchers("/druid/**").permitAll() |
| 77 | .antMatchers("/swagger-ui/**").permitAll() | 73 | .antMatchers("/swagger-ui/**").permitAll() | ... | ... |
| ... | @@ -9,6 +9,7 @@ import com.apple.erp.dto.response.UserInfoRes; | ... | @@ -9,6 +9,7 @@ import com.apple.erp.dto.response.UserInfoRes; |
| 9 | import com.apple.erp.entity.SysUser; | 9 | import com.apple.erp.entity.SysUser; |
| 10 | import com.apple.erp.service.SysUserService; | 10 | import com.apple.erp.service.SysUserService; |
| 11 | import com.apple.erp.service.SysLogService; | 11 | import com.apple.erp.service.SysLogService; |
| 12 | +import com.apple.erp.service.SessionService; | ||
| 12 | import com.apple.erp.utils.JwtUtils; | 13 | import com.apple.erp.utils.JwtUtils; |
| 13 | import io.swagger.v3.oas.annotations.Operation; | 14 | import io.swagger.v3.oas.annotations.Operation; |
| 14 | import io.swagger.v3.oas.annotations.Parameter; | 15 | import io.swagger.v3.oas.annotations.Parameter; |
| ... | @@ -24,7 +25,9 @@ import org.springframework.web.bind.annotation.*; | ... | @@ -24,7 +25,9 @@ import org.springframework.web.bind.annotation.*; |
| 24 | 25 | ||
| 25 | import javax.servlet.http.HttpServletRequest; | 26 | import javax.servlet.http.HttpServletRequest; |
| 26 | import javax.validation.Valid; | 27 | import javax.validation.Valid; |
| 28 | +import java.util.HashMap; | ||
| 27 | import java.util.List; | 29 | import java.util.List; |
| 30 | +import java.util.Map; | ||
| 28 | 31 | ||
| 29 | /** | 32 | /** |
| 30 | * 认证控制器 | 33 | * 认证控制器 |
| ... | @@ -51,6 +54,9 @@ public class AuthController { | ... | @@ -51,6 +54,9 @@ public class AuthController { |
| 51 | 54 | ||
| 52 | @Autowired | 55 | @Autowired |
| 53 | private SysLogService sysLogService; | 56 | private SysLogService sysLogService; |
| 57 | + | ||
| 58 | + @Autowired | ||
| 59 | + private SessionService sessionService; | ||
| 54 | 60 | ||
| 55 | /** | 61 | /** |
| 56 | * 用户登录 | 62 | * 用户登录 |
| ... | @@ -95,6 +101,21 @@ public class AuthController { | ... | @@ -95,6 +101,21 @@ public class AuthController { |
| 95 | // 生成JWT令牌 | 101 | // 生成JWT令牌 |
| 96 | String token = jwtUtils.generateToken(username); | 102 | String token = jwtUtils.generateToken(username); |
| 97 | String refreshToken = jwtUtils.generateRefreshToken(username); | 103 | String refreshToken = jwtUtils.generateRefreshToken(username); |
| 104 | + | ||
| 105 | + // 将用户会话信息存储到Redis | ||
| 106 | + try { | ||
| 107 | + Map<String, Object> sessionData = new HashMap<>(); | ||
| 108 | + sessionData.put("username", username); | ||
| 109 | + sessionData.put("loginTime", System.currentTimeMillis()); | ||
| 110 | + sessionData.put("clientIp", getClientIp(request)); | ||
| 111 | + sessionData.put("token", token); | ||
| 112 | + | ||
| 113 | + sessionService.storeUserSession(username, sessionData); | ||
| 114 | + log.info("用户会话已存储: {}", username); | ||
| 115 | + } catch (Exception e) { | ||
| 116 | + log.error("存储用户会话失败", e); | ||
| 117 | + // Redis失败不影响登录流程 | ||
| 118 | + } | ||
| 98 | 119 | ||
| 99 | LoginRes loginResponse = new LoginRes(token, refreshToken, username); | 120 | LoginRes loginResponse = new LoginRes(token, refreshToken, username); |
| 100 | ApiRes<LoginRes> response = ApiRes.success("登录成功", loginResponse); | 121 | ApiRes<LoginRes> response = ApiRes.success("登录成功", loginResponse); |
| ... | @@ -192,6 +213,16 @@ public class AuthController { | ... | @@ -192,6 +213,16 @@ public class AuthController { |
| 192 | } | 213 | } |
| 193 | } | 214 | } |
| 194 | 215 | ||
| 216 | + // 清除Redis中的用户会话 | ||
| 217 | + if (username != null) { | ||
| 218 | + try { | ||
| 219 | + sessionService.removeUserSession(username); | ||
| 220 | + log.info("用户会话已清除: {}", username); | ||
| 221 | + } catch (Exception e) { | ||
| 222 | + log.error("清除用户会话失败", e); | ||
| 223 | + } | ||
| 224 | + } | ||
| 225 | + | ||
| 195 | // 清除Spring Security上下文 | 226 | // 清除Spring Security上下文 |
| 196 | SecurityContextHolder.clearContext(); | 227 | SecurityContextHolder.clearContext(); |
| 197 | 228 | ... | ... |
| 1 | +package com.apple.erp.controller; | ||
| 2 | + | ||
| 3 | +import com.apple.erp.dto.response.ApiRes; | ||
| 4 | +import com.apple.erp.dto.response.DashboardStatsRes; | ||
| 5 | +import com.apple.erp.service.DashboardService; | ||
| 6 | +import lombok.extern.slf4j.Slf4j; | ||
| 7 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 8 | +import org.springframework.web.bind.annotation.GetMapping; | ||
| 9 | +import org.springframework.web.bind.annotation.RequestMapping; | ||
| 10 | +import org.springframework.web.bind.annotation.RestController; | ||
| 11 | + | ||
| 12 | +import java.util.List; | ||
| 13 | +import java.util.Map; | ||
| 14 | + | ||
| 15 | +/** | ||
| 16 | + * 首页仪表板控制器 | ||
| 17 | + * 提供首页统计数据接口 | ||
| 18 | + */ | ||
| 19 | +@Slf4j | ||
| 20 | +@RestController | ||
| 21 | +@RequestMapping("/api/dashboard") | ||
| 22 | +public class DashboardController { | ||
| 23 | + | ||
| 24 | + @Autowired | ||
| 25 | + private DashboardService dashboardService; | ||
| 26 | + | ||
| 27 | + /** | ||
| 28 | + * 获取首页统计数据 | ||
| 29 | + * @return 统计数据 | ||
| 30 | + */ | ||
| 31 | + @GetMapping("/stats") | ||
| 32 | + public ApiRes<DashboardStatsRes> getDashboardStats() { | ||
| 33 | + log.info("获取首页统计数据请求"); | ||
| 34 | + try { | ||
| 35 | + DashboardStatsRes result = dashboardService.getDashboardStats(); | ||
| 36 | + log.info("首页统计数据获取成功: {}", result); | ||
| 37 | + return ApiRes.success("获取统计数据成功", result); | ||
| 38 | + } catch (Exception e) { | ||
| 39 | + log.error("获取首页统计数据失败", e); | ||
| 40 | + return ApiRes.error(500, "获取统计数据失败: " + e.getMessage()); | ||
| 41 | + } | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + /** | ||
| 45 | + * 测试接口 | ||
| 46 | + * @return 测试数据 | ||
| 47 | + */ | ||
| 48 | + @GetMapping("/test") | ||
| 49 | + public ApiRes<DashboardStatsRes> getTestStats() { | ||
| 50 | + log.info("测试接口调用"); | ||
| 51 | + DashboardStatsRes stats = new DashboardStatsRes(); | ||
| 52 | + stats.setTodayOrderCount(10); | ||
| 53 | + stats.setPendingDeliveryCount(5); | ||
| 54 | + stats.setPendingInvoiceCount(3); | ||
| 55 | + stats.setTodaySalesAmount(10000.0); | ||
| 56 | + stats.setOnlineUserCount(100); | ||
| 57 | + stats.setSystemStatus("测试正常"); | ||
| 58 | + stats.setSystemVersion("v1.0.0"); | ||
| 59 | + | ||
| 60 | + return ApiRes.success("测试成功", stats); | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + /** | ||
| 64 | + * 获取最近活动 | ||
| 65 | + * @return 最近活动列表 | ||
| 66 | + */ | ||
| 67 | + @GetMapping("/activities") | ||
| 68 | + public ApiRes<List<Map<String, Object>>> getRecentActivities() { | ||
| 69 | + log.info("获取最近活动请求"); | ||
| 70 | + try { | ||
| 71 | + List<Map<String, Object>> activities = dashboardService.getRecentActivities(); | ||
| 72 | + return ApiRes.success("获取最近活动成功", activities); | ||
| 73 | + } catch (Exception e) { | ||
| 74 | + log.error("获取最近活动失败", e); | ||
| 75 | + return ApiRes.error(500, "获取最近活动失败: " + e.getMessage()); | ||
| 76 | + } | ||
| 77 | + } | ||
| 78 | +} |
| ... | @@ -6,12 +6,14 @@ import com.apple.erp.dto.ExceptionWorkorderRes; | ... | @@ -6,12 +6,14 @@ import com.apple.erp.dto.ExceptionWorkorderRes; |
| 6 | import com.apple.erp.dto.ExceptionWorkorderStatusUpdateReq; | 6 | import com.apple.erp.dto.ExceptionWorkorderStatusUpdateReq; |
| 7 | import com.apple.erp.dto.ExceptionWorkorderUpdateReq; | 7 | import com.apple.erp.dto.ExceptionWorkorderUpdateReq; |
| 8 | import com.apple.erp.service.ExceptionWorkorderService; | 8 | import com.apple.erp.service.ExceptionWorkorderService; |
| 9 | +import com.apple.erp.service.ExcelExportService; | ||
| 9 | import com.apple.erp.dto.response.ApiRes; | 10 | import com.apple.erp.dto.response.ApiRes; |
| 10 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | 11 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| 11 | import io.swagger.v3.oas.annotations.Operation; | 12 | import io.swagger.v3.oas.annotations.Operation; |
| 12 | import io.swagger.v3.oas.annotations.Parameter; | 13 | import io.swagger.v3.oas.annotations.Parameter; |
| 13 | import io.swagger.v3.oas.annotations.tags.Tag; | 14 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 14 | import org.springframework.beans.factory.annotation.Autowired; | 15 | import org.springframework.beans.factory.annotation.Autowired; |
| 16 | +import org.springframework.http.ResponseEntity; | ||
| 15 | import org.springframework.security.access.prepost.PreAuthorize; | 17 | import org.springframework.security.access.prepost.PreAuthorize; |
| 16 | import org.springframework.validation.annotation.Validated; | 18 | import org.springframework.validation.annotation.Validated; |
| 17 | import org.springframework.web.bind.annotation.*; | 19 | import org.springframework.web.bind.annotation.*; |
| ... | @@ -33,6 +35,9 @@ public class ExceptionWorkorderController { | ... | @@ -33,6 +35,9 @@ public class ExceptionWorkorderController { |
| 33 | 35 | ||
| 34 | @Autowired | 36 | @Autowired |
| 35 | private ExceptionWorkorderService exceptionWorkorderService; | 37 | private ExceptionWorkorderService exceptionWorkorderService; |
| 38 | + | ||
| 39 | + @Autowired | ||
| 40 | + private ExcelExportService excelExportService; | ||
| 36 | 41 | ||
| 37 | @Operation(summary = "分页查询异常工单列表", description = "根据条件分页查询异常工单列表") | 42 | @Operation(summary = "分页查询异常工单列表", description = "根据条件分页查询异常工单列表") |
| 38 | @GetMapping("/list") | 43 | @GetMapping("/list") |
| ... | @@ -168,4 +173,22 @@ public class ExceptionWorkorderController { | ... | @@ -168,4 +173,22 @@ public class ExceptionWorkorderController { |
| 168 | List<ExceptionWorkorderRes> stats = exceptionWorkorderService.getExceptionWorkorderStats(); | 173 | List<ExceptionWorkorderRes> stats = exceptionWorkorderService.getExceptionWorkorderStats(); |
| 169 | return ApiRes.success(stats); | 174 | return ApiRes.success(stats); |
| 170 | } | 175 | } |
| 176 | + | ||
| 177 | + @Operation(summary = "导出异常工单数据", description = "根据查询条件导出异常工单数据到Excel") | ||
| 178 | + @PostMapping("/export") | ||
| 179 | + @PreAuthorize("hasAuthority('exception:workorder:export')") | ||
| 180 | + public ResponseEntity<byte[]> exportExceptionWorkorders(@RequestBody(required = false) ExceptionWorkorderQueryReq queryReq) { | ||
| 181 | + try { | ||
| 182 | + // 如果请求体为空,创建默认查询条件 | ||
| 183 | + if (queryReq == null) { | ||
| 184 | + queryReq = new ExceptionWorkorderQueryReq(); | ||
| 185 | + } | ||
| 186 | + // 获取异常工单数据 | ||
| 187 | + List<ExceptionWorkorderRes> workorders = exceptionWorkorderService.getExceptionWorkorderListForExport(queryReq); | ||
| 188 | + // 导出Excel | ||
| 189 | + return excelExportService.exportExceptionWorkorders(workorders); | ||
| 190 | + } catch (Exception e) { | ||
| 191 | + throw new RuntimeException("导出异常工单数据失败: " + e.getMessage()); | ||
| 192 | + } | ||
| 193 | + } | ||
| 171 | } | 194 | } | ... | ... |
| 1 | +package com.apple.erp.dto.response; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * 首页统计数据响应DTO | ||
| 7 | + */ | ||
| 8 | +@Data | ||
| 9 | +public class DashboardStatsRes { | ||
| 10 | + | ||
| 11 | + /** | ||
| 12 | + * 今日订单数量 | ||
| 13 | + */ | ||
| 14 | + private Integer todayOrderCount; | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 待出库数量 | ||
| 18 | + */ | ||
| 19 | + private Integer pendingDeliveryCount; | ||
| 20 | + | ||
| 21 | + /** | ||
| 22 | + * 待开票数量 | ||
| 23 | + */ | ||
| 24 | + private Integer pendingInvoiceCount; | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 今日销售额 | ||
| 28 | + */ | ||
| 29 | + private Double todaySalesAmount; | ||
| 30 | + | ||
| 31 | + /** | ||
| 32 | + * 在线用户数 | ||
| 33 | + */ | ||
| 34 | + private Integer onlineUserCount; | ||
| 35 | + | ||
| 36 | + /** | ||
| 37 | + * 系统状态 | ||
| 38 | + */ | ||
| 39 | + private String systemStatus; | ||
| 40 | + | ||
| 41 | + /** | ||
| 42 | + * 系统版本 | ||
| 43 | + */ | ||
| 44 | + private String systemVersion; | ||
| 45 | + | ||
| 46 | + /** | ||
| 47 | + * 今日订单增长率 | ||
| 48 | + */ | ||
| 49 | + private String orderGrowthRate; | ||
| 50 | + | ||
| 51 | + /** | ||
| 52 | + * 今日销售额增长率 | ||
| 53 | + */ | ||
| 54 | + private String salesGrowthRate; | ||
| 55 | +} |
| ... | @@ -54,4 +54,12 @@ public interface ExceptionWorkorderMapper extends BaseMapper<ExceptionWorkorder> | ... | @@ -54,4 +54,12 @@ public interface ExceptionWorkorderMapper extends BaseMapper<ExceptionWorkorder> |
| 54 | int batchUpdateWorkorderStatus(@Param("workorderIds") List<Long> workorderIds, | 54 | int batchUpdateWorkorderStatus(@Param("workorderIds") List<Long> workorderIds, |
| 55 | @Param("workorderStatus") Integer workorderStatus, | 55 | @Param("workorderStatus") Integer workorderStatus, |
| 56 | @Param("handlerUser") String handlerUser); | 56 | @Param("handlerUser") String handlerUser); |
| 57 | + | ||
| 58 | + /** | ||
| 59 | + * 获取异常工单列表用于导出 | ||
| 60 | + * | ||
| 61 | + * @param queryReq 查询条件 | ||
| 62 | + * @return 异常工单列表(不分页) | ||
| 63 | + */ | ||
| 64 | + List<ExceptionWorkorderRes> selectExceptionWorkorderListForExport(@Param("query") ExceptionWorkorderQueryReq queryReq); | ||
| 57 | } | 65 | } | ... | ... |
| 1 | +package com.apple.erp.service; | ||
| 2 | + | ||
| 3 | +import com.apple.erp.dto.response.DashboardStatsRes; | ||
| 4 | + | ||
| 5 | +import java.util.List; | ||
| 6 | +import java.util.Map; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 首页仪表板服务接口 | ||
| 10 | + */ | ||
| 11 | +public interface DashboardService { | ||
| 12 | + | ||
| 13 | + /** | ||
| 14 | + * 获取首页统计数据 | ||
| 15 | + * @return 统计数据 | ||
| 16 | + */ | ||
| 17 | + DashboardStatsRes getDashboardStats(); | ||
| 18 | + | ||
| 19 | + /** | ||
| 20 | + * 获取最近活动 | ||
| 21 | + * @return 最近活动列表 | ||
| 22 | + */ | ||
| 23 | + List<Map<String, Object>> getRecentActivities(); | ||
| 24 | +} |
| ... | @@ -67,6 +67,20 @@ public interface DeliveryMainService extends IService<DeliveryMain> { | ... | @@ -67,6 +67,20 @@ public interface DeliveryMainService extends IService<DeliveryMain> { |
| 67 | * @return 是否成功 | 67 | * @return 是否成功 |
| 68 | */ | 68 | */ |
| 69 | boolean updateDeliveryStatus(Long deliveryId, Integer deliveryStatus); | 69 | boolean updateDeliveryStatus(Long deliveryId, Integer deliveryStatus); |
| 70 | + | ||
| 71 | + /** | ||
| 72 | + * 获取待出库数量 | ||
| 73 | + * @return 待出库数量 | ||
| 74 | + */ | ||
| 75 | + Integer getPendingDeliveryCount(); | ||
| 76 | + | ||
| 77 | + /** | ||
| 78 | + * 获取最近的出库记录 | ||
| 79 | + * | ||
| 80 | + * @param limit 限制数量 | ||
| 81 | + * @return 最近出库列表 | ||
| 82 | + */ | ||
| 83 | + List<DeliveryRes> getRecentDeliveries(Integer limit); | ||
| 70 | } | 84 | } |
| 71 | 85 | ||
| 72 | 86 | ... | ... |
| ... | @@ -91,4 +91,12 @@ public interface ExceptionWorkorderService extends IService<ExceptionWorkorder> | ... | @@ -91,4 +91,12 @@ public interface ExceptionWorkorderService extends IService<ExceptionWorkorder> |
| 91 | * @return 统计信息 | 91 | * @return 统计信息 |
| 92 | */ | 92 | */ |
| 93 | List<ExceptionWorkorderRes> getExceptionWorkorderStats(); | 93 | List<ExceptionWorkorderRes> getExceptionWorkorderStats(); |
| 94 | + | ||
| 95 | + /** | ||
| 96 | + * 获取异常工单列表用于导出 | ||
| 97 | + * | ||
| 98 | + * @param queryReq 查询条件 | ||
| 99 | + * @return 异常工单列表(不分页) | ||
| 100 | + */ | ||
| 101 | + List<ExceptionWorkorderRes> getExceptionWorkorderListForExport(ExceptionWorkorderQueryReq queryReq); | ||
| 94 | } | 102 | } | ... | ... |
| ... | @@ -67,6 +67,20 @@ public interface InvoiceMainService extends IService<InvoiceMain> { | ... | @@ -67,6 +67,20 @@ public interface InvoiceMainService extends IService<InvoiceMain> { |
| 67 | * @return 是否成功 | 67 | * @return 是否成功 |
| 68 | */ | 68 | */ |
| 69 | boolean updateInvoiceStatus(Long invoiceId, Integer invoiceStatus); | 69 | boolean updateInvoiceStatus(Long invoiceId, Integer invoiceStatus); |
| 70 | + | ||
| 71 | + /** | ||
| 72 | + * 获取待开票数量 | ||
| 73 | + * @return 待开票数量 | ||
| 74 | + */ | ||
| 75 | + Integer getPendingInvoiceCount(); | ||
| 76 | + | ||
| 77 | + /** | ||
| 78 | + * 获取最近的发票记录 | ||
| 79 | + * | ||
| 80 | + * @param limit 限制数量 | ||
| 81 | + * @return 最近发票列表 | ||
| 82 | + */ | ||
| 83 | + List<InvoiceRes> getRecentInvoices(Integer limit); | ||
| 70 | } | 84 | } |
| 71 | 85 | ||
| 72 | 86 | ... | ... |
| ... | @@ -92,5 +92,29 @@ public interface OrderMainService extends IService<OrderMain> { | ... | @@ -92,5 +92,29 @@ public interface OrderMainService extends IService<OrderMain> { |
| 92 | * @return 是否成功 | 92 | * @return 是否成功 |
| 93 | */ | 93 | */ |
| 94 | boolean updateRebateCalcFlag(Long orderId, Integer rebateCalcFlag); | 94 | boolean updateRebateCalcFlag(Long orderId, Integer rebateCalcFlag); |
| 95 | + | ||
| 96 | + /** | ||
| 97 | + * 获取今日订单数量 | ||
| 98 | + * | ||
| 99 | + * @param date 日期 | ||
| 100 | + * @return 订单数量 | ||
| 101 | + */ | ||
| 102 | + Integer getTodayOrderCount(String date); | ||
| 103 | + | ||
| 104 | + /** | ||
| 105 | + * 获取今日销售额 | ||
| 106 | + * | ||
| 107 | + * @param date 日期 | ||
| 108 | + * @return 销售额 | ||
| 109 | + */ | ||
| 110 | + Double getTodaySalesAmount(String date); | ||
| 111 | + | ||
| 112 | + /** | ||
| 113 | + * 获取最近的订单记录 | ||
| 114 | + * | ||
| 115 | + * @param limit 限制数量 | ||
| 116 | + * @return 最近订单列表 | ||
| 117 | + */ | ||
| 118 | + List<OrderRes> getRecentOrders(Integer limit); | ||
| 95 | } | 119 | } |
| 96 | 120 | ... | ... |
| ... | @@ -139,4 +139,12 @@ public interface RebateService extends IService<Rebate> { | ... | @@ -139,4 +139,12 @@ public interface RebateService extends IService<Rebate> { |
| 139 | * @return 趋势统计数据 | 139 | * @return 趋势统计数据 |
| 140 | */ | 140 | */ |
| 141 | List<Map<String, Object>> getRebateTrendStats(String startDate, String endDate); | 141 | List<Map<String, Object>> getRebateTrendStats(String startDate, String endDate); |
| 142 | + | ||
| 143 | + /** | ||
| 144 | + * 获取最近的返利记录 | ||
| 145 | + * | ||
| 146 | + * @param limit 限制数量 | ||
| 147 | + * @return 最近返利列表 | ||
| 148 | + */ | ||
| 149 | + List<RebateRes> getRecentRebates(Integer limit); | ||
| 142 | } | 150 | } | ... | ... |
| 1 | +package com.apple.erp.service; | ||
| 2 | + | ||
| 3 | +import java.util.Map; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * 会话管理服务接口 | ||
| 7 | + * | ||
| 8 | + * @author Apple ERP Team | ||
| 9 | + * @version 1.0.0 | ||
| 10 | + * @since 2024-01-01 | ||
| 11 | + */ | ||
| 12 | +public interface SessionService { | ||
| 13 | + | ||
| 14 | + /** | ||
| 15 | + * 存储用户会话信息 | ||
| 16 | + * @param username 用户名 | ||
| 17 | + * @param sessionData 会话数据 | ||
| 18 | + */ | ||
| 19 | + void storeUserSession(String username, Map<String, Object> sessionData); | ||
| 20 | + | ||
| 21 | + /** | ||
| 22 | + * 获取用户会话信息 | ||
| 23 | + * @param username 用户名 | ||
| 24 | + * @return 会话数据 | ||
| 25 | + */ | ||
| 26 | + Map<String, Object> getUserSession(String username); | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 删除用户会话 | ||
| 30 | + * @param username 用户名 | ||
| 31 | + */ | ||
| 32 | + void removeUserSession(String username); | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * 获取在线用户数 | ||
| 36 | + * @return 在线用户数 | ||
| 37 | + */ | ||
| 38 | + Integer getOnlineUserCount(); | ||
| 39 | + | ||
| 40 | + /** | ||
| 41 | + * 清理过期会话 | ||
| 42 | + */ | ||
| 43 | + void cleanupExpiredSessions(); | ||
| 44 | +} |
| 1 | +package com.apple.erp.service.impl; | ||
| 2 | + | ||
| 3 | +import com.apple.erp.dto.response.DashboardStatsRes; | ||
| 4 | +import com.apple.erp.dto.OrderRes; | ||
| 5 | +import com.apple.erp.dto.DeliveryRes; | ||
| 6 | +import com.apple.erp.dto.InvoiceRes; | ||
| 7 | +import com.apple.erp.dto.response.RebateRes; | ||
| 8 | +import com.apple.erp.service.DashboardService; | ||
| 9 | +import com.apple.erp.service.OrderMainService; | ||
| 10 | +import com.apple.erp.service.DeliveryMainService; | ||
| 11 | +import com.apple.erp.service.InvoiceMainService; | ||
| 12 | +import com.apple.erp.service.RebateService; | ||
| 13 | +import com.apple.erp.service.SessionService; | ||
| 14 | +import lombok.extern.slf4j.Slf4j; | ||
| 15 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 16 | +import org.springframework.stereotype.Service; | ||
| 17 | + | ||
| 18 | +import java.time.LocalDate; | ||
| 19 | +import java.time.format.DateTimeFormatter; | ||
| 20 | +import java.util.ArrayList; | ||
| 21 | +import java.util.HashMap; | ||
| 22 | +import java.util.List; | ||
| 23 | +import java.util.Map; | ||
| 24 | + | ||
| 25 | +/** | ||
| 26 | + * 首页仪表板服务实现 | ||
| 27 | + */ | ||
| 28 | +@Slf4j | ||
| 29 | +@Service | ||
| 30 | +public class DashboardServiceImpl implements DashboardService { | ||
| 31 | + | ||
| 32 | + @Autowired | ||
| 33 | + private OrderMainService orderMainService; | ||
| 34 | + | ||
| 35 | + @Autowired | ||
| 36 | + private DeliveryMainService deliveryMainService; | ||
| 37 | + | ||
| 38 | + @Autowired | ||
| 39 | + private InvoiceMainService invoiceMainService; | ||
| 40 | + | ||
| 41 | + @Autowired | ||
| 42 | + private RebateService rebateService; | ||
| 43 | + | ||
| 44 | + @Autowired | ||
| 45 | + private SessionService sessionService; | ||
| 46 | + | ||
| 47 | + @Override | ||
| 48 | + public DashboardStatsRes getDashboardStats() { | ||
| 49 | + log.info("开始获取首页统计数据"); | ||
| 50 | + DashboardStatsRes stats = new DashboardStatsRes(); | ||
| 51 | + | ||
| 52 | + try { | ||
| 53 | + // 获取今日订单数量 | ||
| 54 | + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); | ||
| 55 | + log.info("查询今日订单数量,日期: {}", today); | ||
| 56 | + stats.setTodayOrderCount(orderMainService.getTodayOrderCount(today)); | ||
| 57 | + | ||
| 58 | + // 获取待出库数量(状态为已确认但未出库的订单) | ||
| 59 | + log.info("查询待出库数量"); | ||
| 60 | + stats.setPendingDeliveryCount(deliveryMainService.getPendingDeliveryCount()); | ||
| 61 | + | ||
| 62 | + // 获取待开票数量(已出库但未开票的订单) | ||
| 63 | + log.info("查询待开票数量"); | ||
| 64 | + stats.setPendingInvoiceCount(invoiceMainService.getPendingInvoiceCount()); | ||
| 65 | + | ||
| 66 | + // 获取今日销售额 | ||
| 67 | + log.info("查询今日销售额,日期: {}", today); | ||
| 68 | + stats.setTodaySalesAmount(orderMainService.getTodaySalesAmount(today)); | ||
| 69 | + | ||
| 70 | + // 获取在线用户数(从Redis或Session中获取) | ||
| 71 | + stats.setOnlineUserCount(getOnlineUserCount()); | ||
| 72 | + | ||
| 73 | + // 系统状态 | ||
| 74 | + stats.setSystemStatus(getSystemStatus()); | ||
| 75 | + | ||
| 76 | + // 系统版本 | ||
| 77 | + stats.setSystemVersion("v1.0.0"); | ||
| 78 | + | ||
| 79 | + // 计算增长率数据 | ||
| 80 | + stats.setOrderGrowthRate(calculateOrderGrowthRate(today)); | ||
| 81 | + stats.setSalesGrowthRate(calculateSalesGrowthRate(today)); | ||
| 82 | + | ||
| 83 | + log.info("首页统计数据获取完成: {}", stats); | ||
| 84 | + | ||
| 85 | + } catch (Exception e) { | ||
| 86 | + log.error("获取首页统计数据失败", e); | ||
| 87 | + // 如果获取数据失败,返回默认值 | ||
| 88 | + stats.setTodayOrderCount(0); | ||
| 89 | + stats.setPendingDeliveryCount(0); | ||
| 90 | + stats.setPendingInvoiceCount(0); | ||
| 91 | + stats.setTodaySalesAmount(0.0); | ||
| 92 | + stats.setOnlineUserCount(0); | ||
| 93 | + stats.setSystemStatus("系统异常"); | ||
| 94 | + stats.setSystemVersion("v1.0.0"); | ||
| 95 | + stats.setOrderGrowthRate("0%"); | ||
| 96 | + stats.setSalesGrowthRate("0%"); | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + return stats; | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + @Override | ||
| 103 | + public List<Map<String, Object>> getRecentActivities() { | ||
| 104 | + log.info("开始获取最近活动数据"); | ||
| 105 | + List<Map<String, Object>> activities = new ArrayList<>(); | ||
| 106 | + | ||
| 107 | + try { | ||
| 108 | + // 从数据库获取最近的活动数据 | ||
| 109 | + activities.addAll(getRecentOrders()); | ||
| 110 | + activities.addAll(getRecentDeliveries()); | ||
| 111 | + activities.addAll(getRecentInvoices()); | ||
| 112 | + activities.addAll(getRecentRebates()); | ||
| 113 | + | ||
| 114 | + // 按时间排序,取最新的5条 | ||
| 115 | + activities.sort((a, b) -> { | ||
| 116 | + String timeA = (String) a.get("time"); | ||
| 117 | + String timeB = (String) b.get("time"); | ||
| 118 | + return timeB.compareTo(timeA); | ||
| 119 | + }); | ||
| 120 | + | ||
| 121 | + if (activities.size() > 5) { | ||
| 122 | + activities = activities.subList(0, 5); | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + log.info("最近活动数据获取完成,共{}条记录", activities.size()); | ||
| 126 | + | ||
| 127 | + } catch (Exception e) { | ||
| 128 | + log.error("获取最近活动数据失败", e); | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + return activities; | ||
| 132 | + } | ||
| 133 | + | ||
| 134 | + /** | ||
| 135 | + * 获取最近的订单活动 | ||
| 136 | + */ | ||
| 137 | + private List<Map<String, Object>> getRecentOrders() { | ||
| 138 | + List<Map<String, Object>> activities = new ArrayList<>(); | ||
| 139 | + try { | ||
| 140 | + List<OrderRes> recentOrders = orderMainService.getRecentOrders(2); | ||
| 141 | + for (OrderRes order : recentOrders) { | ||
| 142 | + Map<String, Object> activity = new HashMap<>(); | ||
| 143 | + activity.put("id", order.getOrderId()); | ||
| 144 | + activity.put("type", "order"); | ||
| 145 | + activity.put("title", "新订单 " + order.getOrderNo() + " 已创建"); | ||
| 146 | + activity.put("time", formatTimeAgo(order.getCreateTime())); | ||
| 147 | + activities.add(activity); | ||
| 148 | + } | ||
| 149 | + } catch (Exception e) { | ||
| 150 | + log.error("获取最近订单活动失败", e); | ||
| 151 | + } | ||
| 152 | + return activities; | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + /** | ||
| 156 | + * 获取最近的出库活动 | ||
| 157 | + */ | ||
| 158 | + private List<Map<String, Object>> getRecentDeliveries() { | ||
| 159 | + List<Map<String, Object>> activities = new ArrayList<>(); | ||
| 160 | + try { | ||
| 161 | + List<DeliveryRes> recentDeliveries = deliveryMainService.getRecentDeliveries(2); | ||
| 162 | + for (DeliveryRes delivery : recentDeliveries) { | ||
| 163 | + Map<String, Object> activity = new HashMap<>(); | ||
| 164 | + activity.put("id", delivery.getDeliveryId()); | ||
| 165 | + activity.put("type", "delivery"); | ||
| 166 | + activity.put("title", "出库单 " + delivery.getDeliveryNo() + " 已创建"); | ||
| 167 | + activity.put("time", formatTimeAgo(delivery.getCreateTime())); | ||
| 168 | + activities.add(activity); | ||
| 169 | + } | ||
| 170 | + } catch (Exception e) { | ||
| 171 | + log.error("获取最近出库活动失败", e); | ||
| 172 | + } | ||
| 173 | + return activities; | ||
| 174 | + } | ||
| 175 | + | ||
| 176 | + /** | ||
| 177 | + * 获取最近的发票活动 | ||
| 178 | + */ | ||
| 179 | + private List<Map<String, Object>> getRecentInvoices() { | ||
| 180 | + List<Map<String, Object>> activities = new ArrayList<>(); | ||
| 181 | + try { | ||
| 182 | + List<InvoiceRes> recentInvoices = invoiceMainService.getRecentInvoices(2); | ||
| 183 | + for (InvoiceRes invoice : recentInvoices) { | ||
| 184 | + Map<String, Object> activity = new HashMap<>(); | ||
| 185 | + activity.put("id", invoice.getInvoiceId()); | ||
| 186 | + activity.put("type", "invoice"); | ||
| 187 | + activity.put("title", "发票 " + invoice.getInvoiceNo() + " 已开具"); | ||
| 188 | + activity.put("time", formatTimeAgo(invoice.getCreateTime())); | ||
| 189 | + activities.add(activity); | ||
| 190 | + } | ||
| 191 | + } catch (Exception e) { | ||
| 192 | + log.error("获取最近发票活动失败", e); | ||
| 193 | + } | ||
| 194 | + return activities; | ||
| 195 | + } | ||
| 196 | + | ||
| 197 | + /** | ||
| 198 | + * 获取最近的返利活动 | ||
| 199 | + */ | ||
| 200 | + private List<Map<String, Object>> getRecentRebates() { | ||
| 201 | + List<Map<String, Object>> activities = new ArrayList<>(); | ||
| 202 | + try { | ||
| 203 | + List<RebateRes> recentRebates = rebateService.getRecentRebates(2); | ||
| 204 | + for (RebateRes rebate : recentRebates) { | ||
| 205 | + Map<String, Object> activity = new HashMap<>(); | ||
| 206 | + activity.put("id", rebate.getRebateId()); | ||
| 207 | + activity.put("type", "rebate"); | ||
| 208 | + activity.put("title", "返利 " + rebate.getRebateNo() + " 已计算"); | ||
| 209 | + activity.put("time", formatTimeAgo(rebate.getCreateTime())); | ||
| 210 | + activities.add(activity); | ||
| 211 | + } | ||
| 212 | + } catch (Exception e) { | ||
| 213 | + log.error("获取最近返利活动失败", e); | ||
| 214 | + } | ||
| 215 | + return activities; | ||
| 216 | + } | ||
| 217 | + | ||
| 218 | + /** | ||
| 219 | + * 获取在线用户数 | ||
| 220 | + */ | ||
| 221 | + private Integer getOnlineUserCount() { | ||
| 222 | + try { | ||
| 223 | + // 使用SessionService获取在线用户数 | ||
| 224 | + Integer onlineCount = sessionService.getOnlineUserCount(); | ||
| 225 | + log.info("获取在线用户数: {}", onlineCount); | ||
| 226 | + return onlineCount; | ||
| 227 | + } catch (Exception e) { | ||
| 228 | + log.error("获取在线用户数失败", e); | ||
| 229 | + // 如果Redis不可用,尝试从数据库获取活跃用户数 | ||
| 230 | + return getActiveUserCountFromDatabase(); | ||
| 231 | + } | ||
| 232 | + } | ||
| 233 | + | ||
| 234 | + /** | ||
| 235 | + * 从数据库获取活跃用户数(Redis不可用时的备用方案) | ||
| 236 | + */ | ||
| 237 | + private Integer getActiveUserCountFromDatabase() { | ||
| 238 | + try { | ||
| 239 | + // 查询最近30分钟内有登录记录的用户数 | ||
| 240 | + // 这里需要根据实际的用户表结构调整 | ||
| 241 | + log.info("从数据库获取活跃用户数"); | ||
| 242 | + // TODO: 实现数据库查询逻辑 | ||
| 243 | + return 0; | ||
| 244 | + } catch (Exception e) { | ||
| 245 | + log.error("从数据库获取活跃用户数失败", e); | ||
| 246 | + return 0; | ||
| 247 | + } | ||
| 248 | + } | ||
| 249 | + | ||
| 250 | + /** | ||
| 251 | + * 获取系统状态 | ||
| 252 | + */ | ||
| 253 | + private String getSystemStatus() { | ||
| 254 | + try { | ||
| 255 | + // 检查数据库连接状态 | ||
| 256 | + // 检查关键服务状态 | ||
| 257 | + // 这里暂时返回正常运行,后续可以添加健康检查 | ||
| 258 | + return "正常运行"; | ||
| 259 | + } catch (Exception e) { | ||
| 260 | + log.error("获取系统状态失败", e); | ||
| 261 | + return "系统异常"; | ||
| 262 | + } | ||
| 263 | + } | ||
| 264 | + | ||
| 265 | + /** | ||
| 266 | + * 计算订单增长率 | ||
| 267 | + */ | ||
| 268 | + private String calculateOrderGrowthRate(String today) { | ||
| 269 | + try { | ||
| 270 | + // 获取昨日订单数量 | ||
| 271 | + String yesterday = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); | ||
| 272 | + Integer yesterdayCount = orderMainService.getTodayOrderCount(yesterday); | ||
| 273 | + Integer todayCount = orderMainService.getTodayOrderCount(today); | ||
| 274 | + | ||
| 275 | + if (yesterdayCount == null || yesterdayCount == 0) { | ||
| 276 | + return todayCount > 0 ? "+100%" : "0%"; | ||
| 277 | + } | ||
| 278 | + | ||
| 279 | + double growthRate = ((double) (todayCount - yesterdayCount) / yesterdayCount) * 100; | ||
| 280 | + return String.format("%+.1f%%", growthRate); | ||
| 281 | + } catch (Exception e) { | ||
| 282 | + log.error("计算订单增长率失败", e); | ||
| 283 | + return "0%"; | ||
| 284 | + } | ||
| 285 | + } | ||
| 286 | + | ||
| 287 | + /** | ||
| 288 | + * 计算销售额增长率 | ||
| 289 | + */ | ||
| 290 | + private String calculateSalesGrowthRate(String today) { | ||
| 291 | + try { | ||
| 292 | + // 获取昨日销售额 | ||
| 293 | + String yesterday = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); | ||
| 294 | + Double yesterdayAmount = orderMainService.getTodaySalesAmount(yesterday); | ||
| 295 | + Double todayAmount = orderMainService.getTodaySalesAmount(today); | ||
| 296 | + | ||
| 297 | + if (yesterdayAmount == null || yesterdayAmount == 0) { | ||
| 298 | + return todayAmount > 0 ? "+100%" : "0%"; | ||
| 299 | + } | ||
| 300 | + | ||
| 301 | + double growthRate = ((todayAmount - yesterdayAmount) / yesterdayAmount) * 100; | ||
| 302 | + return String.format("%+.1f%%", growthRate); | ||
| 303 | + } catch (Exception e) { | ||
| 304 | + log.error("计算销售额增长率失败", e); | ||
| 305 | + return "0%"; | ||
| 306 | + } | ||
| 307 | + } | ||
| 308 | + | ||
| 309 | + /** | ||
| 310 | + * 格式化时间为相对时间 | ||
| 311 | + */ | ||
| 312 | + private String formatTimeAgo(java.time.LocalDateTime dateTime) { | ||
| 313 | + if (dateTime == null) { | ||
| 314 | + return "未知时间"; | ||
| 315 | + } | ||
| 316 | + | ||
| 317 | + java.time.LocalDateTime now = java.time.LocalDateTime.now(); | ||
| 318 | + long minutes = java.time.Duration.between(dateTime, now).toMinutes(); | ||
| 319 | + | ||
| 320 | + if (minutes < 1) { | ||
| 321 | + return "刚刚"; | ||
| 322 | + } else if (minutes < 60) { | ||
| 323 | + return minutes + "分钟前"; | ||
| 324 | + } else if (minutes < 1440) { // 24小时 | ||
| 325 | + long hours = minutes / 60; | ||
| 326 | + return hours + "小时前"; | ||
| 327 | + } else { | ||
| 328 | + long days = minutes / 1440; | ||
| 329 | + return days + "天前"; | ||
| 330 | + } | ||
| 331 | + } | ||
| 332 | +} |
| ... | @@ -22,6 +22,7 @@ import org.springframework.util.StringUtils; | ... | @@ -22,6 +22,7 @@ import org.springframework.util.StringUtils; |
| 22 | 22 | ||
| 23 | import java.time.LocalDateTime; | 23 | import java.time.LocalDateTime; |
| 24 | import java.time.format.DateTimeFormatter; | 24 | import java.time.format.DateTimeFormatter; |
| 25 | +import java.util.ArrayList; | ||
| 25 | import java.util.List; | 26 | import java.util.List; |
| 26 | import java.util.stream.Collectors; | 27 | import java.util.stream.Collectors; |
| 27 | 28 | ||
| ... | @@ -254,6 +255,34 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del | ... | @@ -254,6 +255,34 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del |
| 254 | return null; | 255 | return null; |
| 255 | } | 256 | } |
| 256 | } | 257 | } |
| 258 | + | ||
| 259 | + @Override | ||
| 260 | + public Integer getPendingDeliveryCount() { | ||
| 261 | + LambdaQueryWrapper<DeliveryMain> wrapper = new LambdaQueryWrapper<>(); | ||
| 262 | + wrapper.eq(DeliveryMain::getDelFlag, "0") | ||
| 263 | + .eq(DeliveryMain::getDeliveryStatus, 0); // 0表示待出库 | ||
| 264 | + return Math.toIntExact(count(wrapper)); | ||
| 265 | + } | ||
| 266 | + | ||
| 267 | + @Override | ||
| 268 | + public List<DeliveryRes> getRecentDeliveries(Integer limit) { | ||
| 269 | + try { | ||
| 270 | + LambdaQueryWrapper<DeliveryMain> wrapper = new LambdaQueryWrapper<>(); | ||
| 271 | + wrapper.eq(DeliveryMain::getDelFlag, "0") | ||
| 272 | + .orderByDesc(DeliveryMain::getCreateTime) | ||
| 273 | + .last("LIMIT " + (limit != null ? limit : 5)); | ||
| 274 | + | ||
| 275 | + List<DeliveryMain> deliveries = list(wrapper); | ||
| 276 | + return deliveries.stream().map(delivery -> { | ||
| 277 | + DeliveryRes deliveryRes = new DeliveryRes(); | ||
| 278 | + BeanUtils.copyProperties(delivery, deliveryRes); | ||
| 279 | + return deliveryRes; | ||
| 280 | + }).collect(Collectors.toList()); | ||
| 281 | + } catch (Exception e) { | ||
| 282 | + log.error("获取最近出库记录失败", e); | ||
| 283 | + return new ArrayList<>(); | ||
| 284 | + } | ||
| 285 | + } | ||
| 257 | } | 286 | } |
| 258 | 287 | ||
| 259 | 288 | ... | ... |
| ... | @@ -18,11 +18,9 @@ import org.springframework.beans.BeanUtils; | ... | @@ -18,11 +18,9 @@ import org.springframework.beans.BeanUtils; |
| 18 | import org.springframework.beans.factory.annotation.Autowired; | 18 | import org.springframework.beans.factory.annotation.Autowired; |
| 19 | import org.springframework.stereotype.Service; | 19 | import org.springframework.stereotype.Service; |
| 20 | import org.springframework.transaction.annotation.Transactional; | 20 | import org.springframework.transaction.annotation.Transactional; |
| 21 | -import org.springframework.util.StringUtils; | ||
| 22 | 21 | ||
| 23 | import java.time.LocalDateTime; | 22 | import java.time.LocalDateTime; |
| 24 | import java.util.List; | 23 | import java.util.List; |
| 25 | -import java.util.stream.Collectors; | ||
| 26 | 24 | ||
| 27 | /** | 25 | /** |
| 28 | * 异常工单Service实现类 | 26 | * 异常工单Service实现类 |
| ... | @@ -200,4 +198,10 @@ public class ExceptionWorkorderServiceImpl extends ServiceImpl<ExceptionWorkorde | ... | @@ -200,4 +198,10 @@ public class ExceptionWorkorderServiceImpl extends ServiceImpl<ExceptionWorkorde |
| 200 | public List<ExceptionWorkorderRes> getExceptionWorkorderStats() { | 198 | public List<ExceptionWorkorderRes> getExceptionWorkorderStats() { |
| 201 | return exceptionWorkorderMapper.selectExceptionWorkorderStats(); | 199 | return exceptionWorkorderMapper.selectExceptionWorkorderStats(); |
| 202 | } | 200 | } |
| 201 | + | ||
| 202 | + @Override | ||
| 203 | + public List<ExceptionWorkorderRes> getExceptionWorkorderListForExport(ExceptionWorkorderQueryReq queryReq) { | ||
| 204 | + // 不分页查询所有数据用于导出 | ||
| 205 | + return exceptionWorkorderMapper.selectExceptionWorkorderListForExport(queryReq); | ||
| 206 | + } | ||
| 203 | } | 207 | } | ... | ... |
| ... | @@ -21,6 +21,7 @@ import org.springframework.util.StringUtils; | ... | @@ -21,6 +21,7 @@ import org.springframework.util.StringUtils; |
| 21 | import java.time.LocalDate; | 21 | import java.time.LocalDate; |
| 22 | import java.time.LocalDateTime; | 22 | import java.time.LocalDateTime; |
| 23 | import java.time.format.DateTimeFormatter; | 23 | import java.time.format.DateTimeFormatter; |
| 24 | +import java.util.ArrayList; | ||
| 24 | import java.util.List; | 25 | import java.util.List; |
| 25 | import java.util.stream.Collectors; | 26 | import java.util.stream.Collectors; |
| 26 | 27 | ||
| ... | @@ -229,4 +230,31 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi | ... | @@ -229,4 +230,31 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi |
| 229 | } | 230 | } |
| 230 | } | 231 | } |
| 231 | 232 | ||
| 233 | + @Override | ||
| 234 | + public Integer getPendingInvoiceCount() { | ||
| 235 | + LambdaQueryWrapper<InvoiceMain> wrapper = new LambdaQueryWrapper<>(); | ||
| 236 | + wrapper.eq(InvoiceMain::getDelFlag, "0") | ||
| 237 | + .eq(InvoiceMain::getInvoiceStatus, 0); // 0表示待开票 | ||
| 238 | + return Math.toIntExact(count(wrapper)); | ||
| 239 | + } | ||
| 240 | + | ||
| 241 | + @Override | ||
| 242 | + public List<InvoiceRes> getRecentInvoices(Integer limit) { | ||
| 243 | + try { | ||
| 244 | + LambdaQueryWrapper<InvoiceMain> wrapper = new LambdaQueryWrapper<>(); | ||
| 245 | + wrapper.eq(InvoiceMain::getDelFlag, "0") | ||
| 246 | + .orderByDesc(InvoiceMain::getCreateTime) | ||
| 247 | + .last("LIMIT " + (limit != null ? limit : 5)); | ||
| 248 | + | ||
| 249 | + List<InvoiceMain> invoices = list(wrapper); | ||
| 250 | + return invoices.stream().map(invoice -> { | ||
| 251 | + InvoiceRes invoiceRes = new InvoiceRes(); | ||
| 252 | + BeanUtils.copyProperties(invoice, invoiceRes); | ||
| 253 | + return invoiceRes; | ||
| 254 | + }).collect(Collectors.toList()); | ||
| 255 | + } catch (Exception e) { | ||
| 256 | + log.error("获取最近发票记录失败", e); | ||
| 257 | + return new ArrayList<>(); | ||
| 258 | + } | ||
| 259 | + } | ||
| 232 | } | 260 | } | ... | ... |
| ... | @@ -16,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional; | ... | @@ -16,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional; |
| 16 | import org.springframework.util.StringUtils; | 16 | import org.springframework.util.StringUtils; |
| 17 | 17 | ||
| 18 | import java.time.LocalDateTime; | 18 | import java.time.LocalDateTime; |
| 19 | +import java.util.ArrayList; | ||
| 19 | import java.util.List; | 20 | import java.util.List; |
| 20 | import java.util.stream.Collectors; | 21 | import java.util.stream.Collectors; |
| 21 | 22 | ||
| ... | @@ -288,5 +289,59 @@ public class OrderMainServiceImpl extends ServiceImpl<OrderMainMapper, OrderMain | ... | @@ -288,5 +289,59 @@ public class OrderMainServiceImpl extends ServiceImpl<OrderMainMapper, OrderMain |
| 288 | order.setUpdateTime(LocalDateTime.now()); | 289 | order.setUpdateTime(LocalDateTime.now()); |
| 289 | return orderMainMapper.updateById(order) > 0; | 290 | return orderMainMapper.updateById(order) > 0; |
| 290 | } | 291 | } |
| 292 | + | ||
| 293 | + @Override | ||
| 294 | + public Integer getTodayOrderCount(String date) { | ||
| 295 | + try { | ||
| 296 | + LambdaQueryWrapper<OrderMain> wrapper = new LambdaQueryWrapper<>(); | ||
| 297 | + wrapper.eq(OrderMain::getDelFlag, "0") | ||
| 298 | + .like(OrderMain::getCreateTime, date); | ||
| 299 | + long count = count(wrapper); | ||
| 300 | + System.out.println("今日订单数量查询结果: " + count + ", 日期: " + date); | ||
| 301 | + return Math.toIntExact(count); | ||
| 302 | + } catch (Exception e) { | ||
| 303 | + System.err.println("查询今日订单数量失败: " + e.getMessage()); | ||
| 304 | + return 0; | ||
| 305 | + } | ||
| 306 | + } | ||
| 307 | + | ||
| 308 | + @Override | ||
| 309 | + public Double getTodaySalesAmount(String date) { | ||
| 310 | + try { | ||
| 311 | + LambdaQueryWrapper<OrderMain> wrapper = new LambdaQueryWrapper<>(); | ||
| 312 | + wrapper.eq(OrderMain::getDelFlag, "0") | ||
| 313 | + .like(OrderMain::getCreateTime, date); | ||
| 314 | + | ||
| 315 | + List<OrderMain> orders = list(wrapper); | ||
| 316 | + double totalAmount = orders.stream() | ||
| 317 | + .mapToDouble(order -> order.getTotalAmount() != null ? order.getTotalAmount().doubleValue() : 0.0) | ||
| 318 | + .sum(); | ||
| 319 | + System.out.println("今日销售额查询结果: " + totalAmount + ", 日期: " + date); | ||
| 320 | + return totalAmount; | ||
| 321 | + } catch (Exception e) { | ||
| 322 | + System.err.println("查询今日销售额失败: " + e.getMessage()); | ||
| 323 | + return 0.0; | ||
| 324 | + } | ||
| 325 | + } | ||
| 326 | + | ||
| 327 | + @Override | ||
| 328 | + public List<OrderRes> getRecentOrders(Integer limit) { | ||
| 329 | + try { | ||
| 330 | + LambdaQueryWrapper<OrderMain> wrapper = new LambdaQueryWrapper<>(); | ||
| 331 | + wrapper.eq(OrderMain::getDelFlag, "0") | ||
| 332 | + .orderByDesc(OrderMain::getCreateTime) | ||
| 333 | + .last("LIMIT " + (limit != null ? limit : 5)); | ||
| 334 | + | ||
| 335 | + List<OrderMain> orders = list(wrapper); | ||
| 336 | + return orders.stream().map(order -> { | ||
| 337 | + OrderRes orderRes = new OrderRes(); | ||
| 338 | + BeanUtils.copyProperties(order, orderRes); | ||
| 339 | + return orderRes; | ||
| 340 | + }).collect(Collectors.toList()); | ||
| 341 | + } catch (Exception e) { | ||
| 342 | + log.error("获取最近订单失败", e); | ||
| 343 | + return new ArrayList<>(); | ||
| 344 | + } | ||
| 345 | + } | ||
| 291 | } | 346 | } |
| 292 | 347 | ... | ... |
| ... | @@ -275,4 +275,24 @@ public class RebateServiceImpl extends ServiceImpl<RebateMapper, Rebate> impleme | ... | @@ -275,4 +275,24 @@ public class RebateServiceImpl extends ServiceImpl<RebateMapper, Rebate> impleme |
| 275 | log.info("获取返利趋势统计数据,开始日期:{},结束日期:{}", startDate, endDate); | 275 | log.info("获取返利趋势统计数据,开始日期:{},结束日期:{}", startDate, endDate); |
| 276 | return rebateMapper.getRebateTrendStats(startDate, endDate); | 276 | return rebateMapper.getRebateTrendStats(startDate, endDate); |
| 277 | } | 277 | } |
| 278 | + | ||
| 279 | + @Override | ||
| 280 | + public List<RebateRes> getRecentRebates(Integer limit) { | ||
| 281 | + try { | ||
| 282 | + LambdaQueryWrapper<Rebate> wrapper = new LambdaQueryWrapper<>(); | ||
| 283 | + wrapper.eq(Rebate::getDelFlag, "0") | ||
| 284 | + .orderByDesc(Rebate::getCreateTime) | ||
| 285 | + .last("LIMIT " + (limit != null ? limit : 5)); | ||
| 286 | + | ||
| 287 | + List<Rebate> rebates = list(wrapper); | ||
| 288 | + return rebates.stream().map(rebate -> { | ||
| 289 | + RebateRes rebateRes = new RebateRes(); | ||
| 290 | + BeanUtils.copyProperties(rebate, rebateRes); | ||
| 291 | + return rebateRes; | ||
| 292 | + }).collect(Collectors.toList()); | ||
| 293 | + } catch (Exception e) { | ||
| 294 | + log.error("获取最近返利记录失败", e); | ||
| 295 | + return new ArrayList<>(); | ||
| 296 | + } | ||
| 297 | + } | ||
| 278 | } | 298 | } | ... | ... |
| 1 | +package com.apple.erp.service.impl; | ||
| 2 | + | ||
| 3 | +import com.apple.erp.service.SessionService; | ||
| 4 | +import lombok.extern.slf4j.Slf4j; | ||
| 5 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 6 | +import org.springframework.data.redis.core.RedisTemplate; | ||
| 7 | +import org.springframework.scheduling.annotation.Scheduled; | ||
| 8 | +import org.springframework.stereotype.Service; | ||
| 9 | + | ||
| 10 | +import java.util.HashMap; | ||
| 11 | +import java.util.Map; | ||
| 12 | +import java.util.Set; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * 会话管理服务实现 | ||
| 16 | + * | ||
| 17 | + * @author Apple ERP Team | ||
| 18 | + * @version 1.0.0 | ||
| 19 | + * @since 2024-01-01 | ||
| 20 | + */ | ||
| 21 | +@Slf4j | ||
| 22 | +@Service | ||
| 23 | +public class SessionServiceImpl implements SessionService { | ||
| 24 | + | ||
| 25 | + private static final String SESSION_KEY_PREFIX = "user:session:"; | ||
| 26 | + private static final long SESSION_TIMEOUT = 1800; // 30分钟 | ||
| 27 | + | ||
| 28 | + @Autowired | ||
| 29 | + private RedisTemplate<String, Object> redisTemplate; | ||
| 30 | + | ||
| 31 | + @Override | ||
| 32 | + public void storeUserSession(String username, Map<String, Object> sessionData) { | ||
| 33 | + try { | ||
| 34 | + String sessionKey = SESSION_KEY_PREFIX + username; | ||
| 35 | + redisTemplate.opsForValue().set(sessionKey, sessionData, SESSION_TIMEOUT); | ||
| 36 | + log.debug("用户会话已存储: {}", username); | ||
| 37 | + } catch (Exception e) { | ||
| 38 | + log.error("存储用户会话失败: {}", username, e); | ||
| 39 | + } | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + @Override | ||
| 43 | + public Map<String, Object> getUserSession(String username) { | ||
| 44 | + try { | ||
| 45 | + String sessionKey = SESSION_KEY_PREFIX + username; | ||
| 46 | + Object sessionData = redisTemplate.opsForValue().get(sessionKey); | ||
| 47 | + if (sessionData instanceof Map) { | ||
| 48 | + return (Map<String, Object>) sessionData; | ||
| 49 | + } | ||
| 50 | + } catch (Exception e) { | ||
| 51 | + log.error("获取用户会话失败: {}", username, e); | ||
| 52 | + } | ||
| 53 | + return new HashMap<>(); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + public void removeUserSession(String username) { | ||
| 58 | + try { | ||
| 59 | + String sessionKey = SESSION_KEY_PREFIX + username; | ||
| 60 | + redisTemplate.delete(sessionKey); | ||
| 61 | + log.debug("用户会话已删除: {}", username); | ||
| 62 | + } catch (Exception e) { | ||
| 63 | + log.error("删除用户会话失败: {}", username, e); | ||
| 64 | + } | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + @Override | ||
| 68 | + public Integer getOnlineUserCount() { | ||
| 69 | + try { | ||
| 70 | + String pattern = SESSION_KEY_PREFIX + "*"; | ||
| 71 | + Set<String> sessionKeys = redisTemplate.keys(pattern); | ||
| 72 | + | ||
| 73 | + if (sessionKeys != null) { | ||
| 74 | + int onlineCount = sessionKeys.size(); | ||
| 75 | + log.debug("当前在线用户数: {}", onlineCount); | ||
| 76 | + return onlineCount; | ||
| 77 | + } else { | ||
| 78 | + log.warn("Redis中未找到用户会话数据"); | ||
| 79 | + return 0; | ||
| 80 | + } | ||
| 81 | + } catch (Exception e) { | ||
| 82 | + log.error("获取在线用户数失败", e); | ||
| 83 | + return 0; | ||
| 84 | + } | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + @Override | ||
| 88 | + @Scheduled(fixedRate = 300000) // 每5分钟执行一次 | ||
| 89 | + public void cleanupExpiredSessions() { | ||
| 90 | + try { | ||
| 91 | + String pattern = SESSION_KEY_PREFIX + "*"; | ||
| 92 | + Set<String> sessionKeys = redisTemplate.keys(pattern); | ||
| 93 | + | ||
| 94 | + if (sessionKeys != null) { | ||
| 95 | + int cleanedCount = 0; | ||
| 96 | + for (String sessionKey : sessionKeys) { | ||
| 97 | + // Redis会自动清理过期的key,这里可以添加额外的清理逻辑 | ||
| 98 | + // 比如检查会话的最后活动时间等 | ||
| 99 | + cleanedCount++; | ||
| 100 | + } | ||
| 101 | + log.debug("会话清理完成,检查了 {} 个会话", cleanedCount); | ||
| 102 | + } | ||
| 103 | + } catch (Exception e) { | ||
| 104 | + log.error("清理过期会话失败", e); | ||
| 105 | + } | ||
| 106 | + } | ||
| 107 | +} |
| ... | @@ -11,7 +11,6 @@ import org.springframework.stereotype.Service; | ... | @@ -11,7 +11,6 @@ import org.springframework.stereotype.Service; |
| 11 | 11 | ||
| 12 | import java.util.ArrayList; | 12 | import java.util.ArrayList; |
| 13 | import java.util.List; | 13 | import java.util.List; |
| 14 | -import java.util.stream.Collectors; | ||
| 15 | 14 | ||
| 16 | /** | 15 | /** |
| 17 | * 系统菜单Service实现类 | 16 | * 系统菜单Service实现类 |
| ... | @@ -77,7 +76,7 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl | ... | @@ -77,7 +76,7 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl |
| 77 | wrapper.eq(SysMenu::getDelFlag, "0"); | 76 | wrapper.eq(SysMenu::getDelFlag, "0"); |
| 78 | 77 | ||
| 79 | // 排序:按父菜单ID和排序号升序 | 78 | // 排序:按父菜单ID和排序号升序 |
| 80 | - wrapper.orderByAsc(SysMenu::getParentId, SysMenu::getSort); | 79 | + wrapper.orderByAsc(SysMenu::getParentId).orderByAsc(SysMenu::getSort); |
| 81 | 80 | ||
| 82 | return wrapper; | 81 | return wrapper; |
| 83 | } | 82 | } | ... | ... |
| ... | @@ -198,33 +198,6 @@ public class DictValueConverter { | ... | @@ -198,33 +198,6 @@ public class DictValueConverter { |
| 198 | } | 198 | } |
| 199 | } | 199 | } |
| 200 | 200 | ||
| 201 | - /** | ||
| 202 | - * 加载指定字典类型到Redis | ||
| 203 | - */ | ||
| 204 | - private static void loadDictToRedis(String dictType) { | ||
| 205 | - if (staticDictItemService == null) { | ||
| 206 | - return; | ||
| 207 | - } | ||
| 208 | - | ||
| 209 | - try { | ||
| 210 | - // 根据字典类型获取字典项 | ||
| 211 | - List<SysDictItem> dictItems = staticDictItemService.getDictItemsByType(dictType); | ||
| 212 | - | ||
| 213 | - Map<String, String> mapping = new HashMap<>(); | ||
| 214 | - for (SysDictItem item : dictItems) { | ||
| 215 | - if (item.getDelFlag() == null || "0".equals(item.getDelFlag())) { | ||
| 216 | - mapping.put(item.getDictValue(), item.getDictLabel()); | ||
| 217 | - } | ||
| 218 | - } | ||
| 219 | - | ||
| 220 | - // 存储到Redis | ||
| 221 | - String cacheKey = DICT_CACHE_PREFIX + dictType; | ||
| 222 | - staticRedisTemplate.opsForValue().set(cacheKey, mapping, CACHE_EXPIRE_HOURS, TimeUnit.HOURS); | ||
| 223 | - | ||
| 224 | - } catch (Exception e) { | ||
| 225 | - System.err.println("加载字典到Redis失败: " + e.getMessage()); | ||
| 226 | - } | ||
| 227 | - } | ||
| 228 | 201 | ||
| 229 | /** | 202 | /** |
| 230 | * 根据字典类型ID获取字典类型编码 | 203 | * 根据字典类型ID获取字典类型编码 | ... | ... |
| ... | @@ -150,4 +150,76 @@ | ... | @@ -150,4 +150,76 @@ |
| 150 | AND del_flag = '0' | 150 | AND del_flag = '0' |
| 151 | </update> | 151 | </update> |
| 152 | 152 | ||
| 153 | + <!-- 获取异常工单列表用于导出 --> | ||
| 154 | + <select id="selectExceptionWorkorderListForExport" resultType="com.apple.erp.dto.ExceptionWorkorderRes"> | ||
| 155 | + SELECT | ||
| 156 | + ew.workorder_id, | ||
| 157 | + ew.workorder_no, | ||
| 158 | + ew.order_no, | ||
| 159 | + ew.dealer_code, | ||
| 160 | + ew.dealer_name, | ||
| 161 | + ew.exception_type, | ||
| 162 | + CASE ew.exception_type | ||
| 163 | + WHEN 1 THEN '逻辑验证异常' | ||
| 164 | + WHEN 2 THEN '源头验证异常' | ||
| 165 | + WHEN 3 THEN '交叉验证异常' | ||
| 166 | + ELSE '未知类型' | ||
| 167 | + END AS exception_type_name, | ||
| 168 | + ew.severity_level, | ||
| 169 | + CASE ew.severity_level | ||
| 170 | + WHEN 1 THEN '高' | ||
| 171 | + WHEN 2 THEN '中' | ||
| 172 | + WHEN 3 THEN '低' | ||
| 173 | + ELSE '未知' | ||
| 174 | + END AS severity_level_name, | ||
| 175 | + ew.workorder_status, | ||
| 176 | + CASE ew.workorder_status | ||
| 177 | + WHEN 1 THEN '待处理' | ||
| 178 | + WHEN 2 THEN '处理中' | ||
| 179 | + WHEN 3 THEN '已解决' | ||
| 180 | + WHEN 4 THEN '已关闭' | ||
| 181 | + ELSE '未知状态' | ||
| 182 | + END AS workorder_status_name, | ||
| 183 | + ew.exception_desc, | ||
| 184 | + ew.handler_user, | ||
| 185 | + ew.create_by, | ||
| 186 | + ew.create_time, | ||
| 187 | + ew.update_time | ||
| 188 | + FROM t_exception_workorder ew | ||
| 189 | + <where> | ||
| 190 | + ew.del_flag = '0' | ||
| 191 | + <if test="query.workorderNo != null and query.workorderNo != ''"> | ||
| 192 | + AND ew.workorder_no LIKE CONCAT('%', #{query.workorderNo}, '%') | ||
| 193 | + </if> | ||
| 194 | + <if test="query.orderNo != null and query.orderNo != ''"> | ||
| 195 | + AND ew.order_no LIKE CONCAT('%', #{query.orderNo}, '%') | ||
| 196 | + </if> | ||
| 197 | + <if test="query.dealerCode != null and query.dealerCode != ''"> | ||
| 198 | + AND ew.dealer_code = #{query.dealerCode} | ||
| 199 | + </if> | ||
| 200 | + <if test="query.dealerName != null and query.dealerName != ''"> | ||
| 201 | + AND ew.dealer_name LIKE CONCAT('%', #{query.dealerName}, '%') | ||
| 202 | + </if> | ||
| 203 | + <if test="query.workorderStatus != null"> | ||
| 204 | + AND ew.workorder_status = #{query.workorderStatus} | ||
| 205 | + </if> | ||
| 206 | + <if test="query.exceptionType != null"> | ||
| 207 | + AND ew.exception_type = #{query.exceptionType} | ||
| 208 | + </if> | ||
| 209 | + <if test="query.severityLevel != null"> | ||
| 210 | + AND ew.severity_level = #{query.severityLevel} | ||
| 211 | + </if> | ||
| 212 | + <if test="query.handlerUser != null and query.handlerUser != ''"> | ||
| 213 | + AND ew.handler_user LIKE CONCAT('%', #{query.handlerUser}, '%') | ||
| 214 | + </if> | ||
| 215 | + <if test="query.startTime != null"> | ||
| 216 | + AND ew.create_time >= #{query.startTime} | ||
| 217 | + </if> | ||
| 218 | + <if test="query.endTime != null"> | ||
| 219 | + AND ew.create_time <= #{query.endTime} | ||
| 220 | + </if> | ||
| 221 | + </where> | ||
| 222 | + ORDER BY ew.create_time DESC | ||
| 223 | + </select> | ||
| 224 | + | ||
| 153 | </mapper> | 225 | </mapper> | ... | ... |
frontend/src/api/dashboard.ts
0 → 100644
| 1 | +import { request } from '@/utils/request' | ||
| 2 | + | ||
| 3 | +/** | ||
| 4 | + * 首页统计数据响应接口 | ||
| 5 | + */ | ||
| 6 | +export interface DashboardStats { | ||
| 7 | + todayOrderCount: number | ||
| 8 | + pendingDeliveryCount: number | ||
| 9 | + pendingInvoiceCount: number | ||
| 10 | + todaySalesAmount: number | ||
| 11 | + onlineUserCount: number | ||
| 12 | + systemStatus: string | ||
| 13 | + systemVersion: string | ||
| 14 | + orderGrowthRate: string | ||
| 15 | + salesGrowthRate: string | ||
| 16 | +} | ||
| 17 | + | ||
| 18 | +export interface RecentActivity { | ||
| 19 | + id: number | ||
| 20 | + type: string | ||
| 21 | + title: string | ||
| 22 | + time: string | ||
| 23 | +} | ||
| 24 | + | ||
| 25 | +/** | ||
| 26 | + * 首页API | ||
| 27 | + */ | ||
| 28 | +export const dashboardApi = { | ||
| 29 | + /** | ||
| 30 | + * 获取首页统计数据 | ||
| 31 | + */ | ||
| 32 | + getDashboardStats: (): Promise<DashboardStats> => { | ||
| 33 | + return request.get('/api/dashboard/stats') | ||
| 34 | + }, | ||
| 35 | + | ||
| 36 | + /** | ||
| 37 | + * 获取最近活动 | ||
| 38 | + */ | ||
| 39 | + getRecentActivities: (): Promise<RecentActivity[]> => { | ||
| 40 | + return request.get('/api/dashboard/activities') | ||
| 41 | + }, | ||
| 42 | + | ||
| 43 | + /** | ||
| 44 | + * 获取测试数据 | ||
| 45 | + */ | ||
| 46 | + getTestStats: (): Promise<DashboardStats> => { | ||
| 47 | + return request.get('/api/dashboard/test') | ||
| 48 | + } | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +export default dashboardApi |
| ... | @@ -146,6 +146,13 @@ export const exceptionWorkorderApi = { | ... | @@ -146,6 +146,13 @@ export const exceptionWorkorderApi = { |
| 146 | // 获取异常工单统计信息 | 146 | // 获取异常工单统计信息 |
| 147 | getExceptionWorkorderStats: () => { | 147 | getExceptionWorkorderStats: () => { |
| 148 | return request.get('/api/exception-workorder/stats') | 148 | return request.get('/api/exception-workorder/stats') |
| 149 | + }, | ||
| 150 | + | ||
| 151 | + // 导出异常工单数据 | ||
| 152 | + exportExceptionWorkorders: (params: ExceptionWorkorderQueryReq) => { | ||
| 153 | + return request.post('/api/exception-workorder/export', params, { | ||
| 154 | + responseType: 'blob' | ||
| 155 | + }) | ||
| 149 | } | 156 | } |
| 150 | } | 157 | } |
| 151 | 158 | ... | ... |
| ... | @@ -32,6 +32,8 @@ export interface RebateSearchParams { | ... | @@ -32,6 +32,8 @@ export interface RebateSearchParams { |
| 32 | productCode?: string | 32 | productCode?: string |
| 33 | operateType?: number | 33 | operateType?: number |
| 34 | calcFlag?: number | 34 | calcFlag?: number |
| 35 | + auditStatus?: number | ||
| 36 | + dataSource?: string | ||
| 35 | rebateStartDate?: string | 37 | rebateStartDate?: string |
| 36 | rebateEndDate?: string | 38 | rebateEndDate?: string |
| 37 | } | 39 | } | ... | ... |
| 1 | <template> | 1 | <template> |
| 2 | <div class="dashboard-container"> | 2 | <div class="dashboard-container"> |
| 3 | - <div class="dashboard-header"> | 3 | + <!-- 欢迎区域 --> |
| 4 | - <h2>系统概览</h2> | 4 | + <div class="welcome-section"> |
| 5 | - <p v-if="loading">正在加载用户信息...</p> | 5 | + <div class="welcome-content"> |
| 6 | - <p v-else>欢迎回来,{{ userInfo?.username || '未知' }}!</p> | 6 | + <h1 class="welcome-title">欢迎使用 Apple经销商ERP系统</h1> |
| 7 | - </div> | 7 | + <p class="welcome-subtitle" v-if="loading">正在加载用户信息...</p> |
| 8 | - | 8 | + <p class="welcome-subtitle" v-else>您好,{{ userInfo?.username || userInfo?.realName || '用户' }}!今天是 {{ currentDate }}</p> |
| 9 | - <div class="dashboard-stats"> | ||
| 10 | - <div class="stat-card"> | ||
| 11 | - <div class="stat-icon">👤</div> | ||
| 12 | - <div class="stat-content"> | ||
| 13 | - <h3>用户总数</h3> | ||
| 14 | - <p class="stat-number">1,234</p> | ||
| 15 | - </div> | ||
| 16 | </div> | 9 | </div> |
| 17 | - | 10 | + <div class="user-info"> |
| 18 | - <div class="stat-card"> | 11 | + <div class="user-avatar"> |
| 19 | - <div class="stat-icon">📈</div> | 12 | + <span>{{ getUserInitials() }}</span> |
| 20 | - <div class="stat-content"> | ||
| 21 | - <h3>今日访问</h3> | ||
| 22 | - <p class="stat-number">567</p> | ||
| 23 | </div> | 13 | </div> |
| 24 | - </div> | 14 | + <div class="user-details"> |
| 25 | - | 15 | + <div class="user-name">{{ userInfo?.username || userInfo?.realName || '未知用户' }}</div> |
| 26 | - <div class="stat-card"> | 16 | + <div class="user-role">{{ getRoleName(userInfo) || '普通用户' }}</div> |
| 27 | - <div class="stat-icon">✅</div> | ||
| 28 | - <div class="stat-content"> | ||
| 29 | - <h3>系统状态</h3> | ||
| 30 | - <p class="stat-number">正常</p> | ||
| 31 | </div> | 17 | </div> |
| 32 | </div> | 18 | </div> |
| 33 | - | 19 | + </div> |
| 34 | - <div class="stat-card"> | 20 | + |
| 35 | - <div class="stat-icon">⏱️</div> | 21 | + <!-- 核心业务统计 --> |
| 36 | - <div class="stat-content"> | 22 | + <div class="stats-section"> |
| 37 | - <h3>运行时间</h3> | 23 | + <h2 class="section-title">核心业务概览</h2> |
| 38 | - <p class="stat-number">{{ currentTime }}</p> | 24 | + <div class="stats-grid"> |
| 25 | + <div class="stat-card primary"> | ||
| 26 | + <div class="stat-icon">📦</div> | ||
| 27 | + <div class="stat-content"> | ||
| 28 | + <div class="stat-label">今日订单</div> | ||
| 29 | + <div class="stat-value">{{ statsLoading ? '...' : (todayStats.todayOrderCount || 0) }}</div> | ||
| 30 | + <div class="stat-change positive">{{ todayStats.orderGrowthRate || '+12.5%' }}</div> | ||
| 31 | + </div> | ||
| 32 | + </div> | ||
| 33 | + | ||
| 34 | + <div class="stat-card success"> | ||
| 35 | + <div class="stat-icon">🚚</div> | ||
| 36 | + <div class="stat-content"> | ||
| 37 | + <div class="stat-label">待出库</div> | ||
| 38 | + <div class="stat-value">{{ statsLoading ? '...' : (todayStats.pendingDeliveryCount || 0) }}</div> | ||
| 39 | + <div class="stat-change">需要处理</div> | ||
| 40 | + </div> | ||
| 41 | + </div> | ||
| 42 | + | ||
| 43 | + <div class="stat-card warning"> | ||
| 44 | + <div class="stat-icon">🧾</div> | ||
| 45 | + <div class="stat-content"> | ||
| 46 | + <div class="stat-label">待开票</div> | ||
| 47 | + <div class="stat-value">{{ statsLoading ? '...' : (todayStats.pendingInvoiceCount || 0) }}</div> | ||
| 48 | + <div class="stat-change">待处理</div> | ||
| 49 | + </div> | ||
| 50 | + </div> | ||
| 51 | + | ||
| 52 | + <div class="stat-card info"> | ||
| 53 | + <div class="stat-icon">💰</div> | ||
| 54 | + <div class="stat-content"> | ||
| 55 | + <div class="stat-label">今日销售额</div> | ||
| 56 | + <div class="stat-value">¥{{ statsLoading ? '...' : formatAmount(todayStats.todaySalesAmount || 0) }}</div> | ||
| 57 | + <div class="stat-change positive">{{ todayStats.salesGrowthRate || '+8.2%' }}</div> | ||
| 58 | + </div> | ||
| 39 | </div> | 59 | </div> |
| 40 | </div> | 60 | </div> |
| 41 | </div> | 61 | </div> |
| 42 | - | 62 | + |
| 43 | - <div class="dashboard-content"> | 63 | + <!-- 主要内容区域 --> |
| 44 | - <div class="dashboard-card"> | 64 | + <div class="main-content"> |
| 45 | - <h3>系统信息</h3> | 65 | + <!-- 快速操作 --> |
| 46 | - <div class="info-grid"> | 66 | + <div class="quick-actions-card"> |
| 67 | + <h3 class="card-title">快速操作</h3> | ||
| 68 | + <div class="actions-grid"> | ||
| 69 | + <button class="action-btn primary" @click="navigateTo('/main/order')"> | ||
| 70 | + <div class="action-icon">📋</div> | ||
| 71 | + <div class="action-text"> | ||
| 72 | + <div class="action-title">订单管理</div> | ||
| 73 | + <div class="action-desc">查看和管理订单</div> | ||
| 74 | + </div> | ||
| 75 | + </button> | ||
| 76 | + | ||
| 77 | + <button class="action-btn success" @click="navigateTo('/main/delivery')"> | ||
| 78 | + <div class="action-icon">🚚</div> | ||
| 79 | + <div class="action-text"> | ||
| 80 | + <div class="action-title">出库管理</div> | ||
| 81 | + <div class="action-desc">处理出库业务</div> | ||
| 82 | + </div> | ||
| 83 | + </button> | ||
| 84 | + | ||
| 85 | + <button class="action-btn warning" @click="navigateTo('/main/invoice')"> | ||
| 86 | + <div class="action-icon">🧾</div> | ||
| 87 | + <div class="action-text"> | ||
| 88 | + <div class="action-title">发票管理</div> | ||
| 89 | + <div class="action-desc">开具和管理发票</div> | ||
| 90 | + </div> | ||
| 91 | + </button> | ||
| 92 | + | ||
| 93 | + <button class="action-btn info" @click="navigateTo('/main/rebate')"> | ||
| 94 | + <div class="action-icon">💰</div> | ||
| 95 | + <div class="action-text"> | ||
| 96 | + <div class="action-title">返利管理</div> | ||
| 97 | + <div class="action-desc">返利计算和发放</div> | ||
| 98 | + </div> | ||
| 99 | + </button> | ||
| 100 | + | ||
| 101 | + <button class="action-btn secondary" @click="navigateTo('/main/product')"> | ||
| 102 | + <div class="action-icon">📱</div> | ||
| 103 | + <div class="action-text"> | ||
| 104 | + <div class="action-title">产品管理</div> | ||
| 105 | + <div class="action-desc">管理产品信息</div> | ||
| 106 | + </div> | ||
| 107 | + </button> | ||
| 108 | + | ||
| 109 | + <button class="action-btn secondary" @click="navigateTo('/main/dealer')"> | ||
| 110 | + <div class="action-icon">🏢</div> | ||
| 111 | + <div class="action-text"> | ||
| 112 | + <div class="action-title">经销商管理</div> | ||
| 113 | + <div class="action-desc">管理经销商信息</div> | ||
| 114 | + </div> | ||
| 115 | + </button> | ||
| 116 | + </div> | ||
| 117 | + </div> | ||
| 118 | + | ||
| 119 | + <!-- 系统信息 --> | ||
| 120 | + <div class="system-info-card"> | ||
| 121 | + <h3 class="card-title">系统信息</h3> | ||
| 122 | + <div class="info-list"> | ||
| 47 | <div class="info-item"> | 123 | <div class="info-item"> |
| 48 | - <label>用户名:</label> | 124 | + <div class="info-label">系统版本</div> |
| 49 | - <span v-if="loading">加载中...</span> | 125 | + <div class="info-value">{{ todayStats.systemVersion }}</div> |
| 50 | - <span v-else>{{ userInfo?.username || '未知' }}</span> | ||
| 51 | </div> | 126 | </div> |
| 52 | <div class="info-item"> | 127 | <div class="info-item"> |
| 53 | - <label>角色:</label> | 128 | + <div class="info-label">当前时间</div> |
| 54 | - <span v-if="loading">加载中...</span> | 129 | + <div class="info-value">{{ currentTime }}</div> |
| 55 | - <span v-else>{{ getRoleName(userInfo) || '普通用户' }}</span> | ||
| 56 | </div> | 130 | </div> |
| 57 | <div class="info-item"> | 131 | <div class="info-item"> |
| 58 | - <label>登录时间:</label> | 132 | + <div class="info-label">系统状态</div> |
| 59 | - <span>{{ currentTime }}</span> | 133 | + <div class="info-value" :class="{ 'status-normal': todayStats.systemStatus === '正常运行', 'status-error': todayStats.systemStatus !== '正常运行' }"> |
| 134 | + {{ todayStats.systemStatus }} | ||
| 135 | + </div> | ||
| 60 | </div> | 136 | </div> |
| 61 | <div class="info-item"> | 137 | <div class="info-item"> |
| 62 | - <label>系统版本:</label> | 138 | + <div class="info-label">在线用户</div> |
| 63 | - <span>v1.0.0</span> | 139 | + <div class="info-value">{{ todayStats.onlineUserCount }}</div> |
| 64 | </div> | 140 | </div> |
| 65 | </div> | 141 | </div> |
| 66 | - </div> | 142 | + </div> |
| 67 | - | 143 | + </div> |
| 68 | - <div class="dashboard-card"> | 144 | + |
| 69 | - <h3>快速操作</h3> | 145 | + <!-- 最近活动 --> |
| 70 | - <div class="quick-actions"> | 146 | + <div class="recent-activities"> |
| 71 | - <button class="action-btn" @click="navigateTo('/main/users')">👤 用户管理</button> | 147 | + <h3 class="card-title">最近活动</h3> |
| 72 | - <button class="action-btn" @click="navigateTo('/main/sys/role')">🛡️ 角色管理</button> | 148 | + <div class="activity-list" v-if="!activitiesLoading"> |
| 73 | - <button class="action-btn" @click="navigateTo('/main/settings')">⚙️ 系统设置</button> | 149 | + <div class="activity-item" v-for="activity in recentActivities" :key="activity.id"> |
| 74 | - <button class="action-btn" @click="navigateTo('/main/sys/log')">📊 查看日志</button> | 150 | + <div class="activity-icon" :class="activity.type"> |
| 151 | + {{ getActivityIcon(activity.type) }} | ||
| 152 | + </div> | ||
| 153 | + <div class="activity-content"> | ||
| 154 | + <div class="activity-title">{{ activity.title }}</div> | ||
| 155 | + <div class="activity-time">{{ activity.time }}</div> | ||
| 156 | + </div> | ||
| 75 | </div> | 157 | </div> |
| 76 | - </div> | 158 | + <div v-if="recentActivities.length === 0" class="no-activities"> |
| 159 | + 暂无最近活动 | ||
| 160 | + </div> | ||
| 161 | + </div> | ||
| 162 | + <div v-else class="loading-activities"> | ||
| 163 | + 正在加载活动数据... | ||
| 164 | + </div> | ||
| 77 | </div> | 165 | </div> |
| 78 | </div> | 166 | </div> |
| 79 | </template> | 167 | </template> |
| 80 | 168 | ||
| 81 | <script setup lang="ts"> | 169 | <script setup lang="ts"> |
| 82 | -import { ref, onMounted } from 'vue' | 170 | +import { ref, onMounted, computed } from 'vue' |
| 83 | import { useRouter } from 'vue-router' | 171 | import { useRouter } from 'vue-router' |
| 84 | import { request } from '@/utils/request' | 172 | import { request } from '@/utils/request' |
| 173 | +import dashboardApi, { type DashboardStats, type RecentActivity } from '@/api/dashboard' | ||
| 85 | 174 | ||
| 86 | const router = useRouter() | 175 | const router = useRouter() |
| 87 | const currentTime = ref('') | 176 | const currentTime = ref('') |
| 177 | +const currentDate = ref('') | ||
| 88 | const userInfo = ref<any>(null) | 178 | const userInfo = ref<any>(null) |
| 89 | const loading = ref(false) | 179 | const loading = ref(false) |
| 180 | +const statsLoading = ref(false) | ||
| 181 | + | ||
| 182 | +// 今日统计数据 | ||
| 183 | +const todayStats = ref<DashboardStats>({ | ||
| 184 | + todayOrderCount: 0, | ||
| 185 | + pendingDeliveryCount: 0, | ||
| 186 | + pendingInvoiceCount: 0, | ||
| 187 | + todaySalesAmount: 0, | ||
| 188 | + onlineUserCount: 0, | ||
| 189 | + systemStatus: '正常运行', | ||
| 190 | + systemVersion: 'v1.0.0', | ||
| 191 | + orderGrowthRate: '+12.5%', | ||
| 192 | + salesGrowthRate: '+8.2%' | ||
| 193 | +}) | ||
| 194 | + | ||
| 195 | +// 最近活动数据 | ||
| 196 | +const recentActivities = ref<RecentActivity[]>([]) | ||
| 197 | +const activitiesLoading = ref(false) | ||
| 198 | + | ||
| 199 | +// 获取用户姓名首字母 | ||
| 200 | +const getUserInitials = () => { | ||
| 201 | + const name = userInfo.value?.username || userInfo.value?.realName || 'U' | ||
| 202 | + return name.charAt(0).toUpperCase() | ||
| 203 | +} | ||
| 90 | 204 | ||
| 91 | // 获取角色名称 | 205 | // 获取角色名称 |
| 92 | const getRoleName = (userInfo: any) => { | 206 | const getRoleName = (userInfo: any) => { |
| ... | @@ -119,6 +233,71 @@ const getRoleName = (userInfo: any) => { | ... | @@ -119,6 +233,71 @@ const getRoleName = (userInfo: any) => { |
| 119 | return '普通用户' | 233 | return '普通用户' |
| 120 | } | 234 | } |
| 121 | 235 | ||
| 236 | +// 格式化金额 | ||
| 237 | +const formatAmount = (amount: number) => { | ||
| 238 | + return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2 }) | ||
| 239 | +} | ||
| 240 | + | ||
| 241 | +// 获取活动图标 | ||
| 242 | +const getActivityIcon = (type: string) => { | ||
| 243 | + const iconMap: { [key: string]: string } = { | ||
| 244 | + 'order': '📋', | ||
| 245 | + 'delivery': '🚚', | ||
| 246 | + 'invoice': '🧾', | ||
| 247 | + 'rebate': '💰', | ||
| 248 | + 'system': '⚙️' | ||
| 249 | + } | ||
| 250 | + return iconMap[type] || '📄' | ||
| 251 | +} | ||
| 252 | + | ||
| 253 | +// 获取今日统计数据 | ||
| 254 | +const fetchTodayStats = async () => { | ||
| 255 | + try { | ||
| 256 | + statsLoading.value = true | ||
| 257 | + console.log('开始获取首页统计数据...') | ||
| 258 | + | ||
| 259 | + const response = await dashboardApi.getDashboardStats() | ||
| 260 | + todayStats.value = response | ||
| 261 | + console.log('首页统计数据获取成功:', response) | ||
| 262 | + } catch (error: any) { | ||
| 263 | + console.error('获取统计数据失败:', error) | ||
| 264 | + console.error('错误详情:', error?.message) | ||
| 265 | + // 如果API调用失败,使用默认值 | ||
| 266 | + todayStats.value = { | ||
| 267 | + todayOrderCount: 0, | ||
| 268 | + pendingDeliveryCount: 0, | ||
| 269 | + pendingInvoiceCount: 0, | ||
| 270 | + todaySalesAmount: 0, | ||
| 271 | + onlineUserCount: 0, | ||
| 272 | + systemStatus: '系统异常', | ||
| 273 | + systemVersion: 'v1.0.0', | ||
| 274 | + orderGrowthRate: '0%', | ||
| 275 | + salesGrowthRate: '0%' | ||
| 276 | + } | ||
| 277 | + } finally { | ||
| 278 | + statsLoading.value = false | ||
| 279 | + } | ||
| 280 | +} | ||
| 281 | + | ||
| 282 | +// 获取最近活动 | ||
| 283 | +const fetchRecentActivities = async () => { | ||
| 284 | + try { | ||
| 285 | + activitiesLoading.value = true | ||
| 286 | + console.log('开始获取最近活动数据...') | ||
| 287 | + | ||
| 288 | + const response = await dashboardApi.getRecentActivities() | ||
| 289 | + recentActivities.value = response | ||
| 290 | + console.log('最近活动数据获取成功:', response) | ||
| 291 | + } catch (error: any) { | ||
| 292 | + console.error('获取最近活动失败:', error) | ||
| 293 | + console.error('错误详情:', error?.message) | ||
| 294 | + // 如果API调用失败,使用默认值 | ||
| 295 | + recentActivities.value = [] | ||
| 296 | + } finally { | ||
| 297 | + activitiesLoading.value = false | ||
| 298 | + } | ||
| 299 | +} | ||
| 300 | + | ||
| 122 | // 获取用户信息 | 301 | // 获取用户信息 |
| 123 | const fetchUserInfo = async () => { | 302 | const fetchUserInfo = async () => { |
| 124 | try { | 303 | try { |
| ... | @@ -138,9 +317,29 @@ const fetchUserInfo = async () => { | ... | @@ -138,9 +317,29 @@ const fetchUserInfo = async () => { |
| 138 | } | 317 | } |
| 139 | } | 318 | } |
| 140 | 319 | ||
| 320 | +// 更新时间 | ||
| 321 | +const updateTime = () => { | ||
| 322 | + const now = new Date() | ||
| 323 | + currentTime.value = now.toLocaleString('zh-CN', { | ||
| 324 | + year: 'numeric', | ||
| 325 | + month: '2-digit', | ||
| 326 | + day: '2-digit', | ||
| 327 | + hour: '2-digit', | ||
| 328 | + minute: '2-digit', | ||
| 329 | + second: '2-digit' | ||
| 330 | + }) | ||
| 331 | + currentDate.value = now.toLocaleDateString('zh-CN', { | ||
| 332 | + year: 'numeric', | ||
| 333 | + month: 'long', | ||
| 334 | + day: 'numeric', | ||
| 335 | + weekday: 'long' | ||
| 336 | + }) | ||
| 337 | +} | ||
| 338 | + | ||
| 141 | onMounted(() => { | 339 | onMounted(() => { |
| 142 | - // 获取当前时间 | 340 | + // 更新当前时间 |
| 143 | - currentTime.value = new Date().toLocaleString() | 341 | + updateTime() |
| 342 | + setInterval(updateTime, 1000) | ||
| 144 | 343 | ||
| 145 | // 检查是否已登录 | 344 | // 检查是否已登录 |
| 146 | const token = localStorage.getItem('token') | 345 | const token = localStorage.getItem('token') |
| ... | @@ -149,8 +348,10 @@ onMounted(() => { | ... | @@ -149,8 +348,10 @@ onMounted(() => { |
| 149 | return | 348 | return |
| 150 | } | 349 | } |
| 151 | 350 | ||
| 152 | - // 获取用户信息 | 351 | + // 获取用户信息、统计数据和最近活动 |
| 153 | fetchUserInfo() | 352 | fetchUserInfo() |
| 353 | + fetchTodayStats() | ||
| 354 | + fetchRecentActivities() | ||
| 154 | }) | 355 | }) |
| 155 | 356 | ||
| 156 | // 页面跳转函数 | 357 | // 页面跳转函数 |
| ... | @@ -170,151 +371,449 @@ const logout = () => { | ... | @@ -170,151 +371,449 @@ const logout = () => { |
| 170 | } | 371 | } |
| 171 | </script> | 372 | </script> |
| 172 | 373 | ||
| 173 | -<style scoped> | 374 | +<style scoped lang="scss"> |
| 174 | .dashboard-container { | 375 | .dashboard-container { |
| 175 | - padding: 20px; | 376 | + padding: 24px; |
| 176 | min-height: 100vh; | 377 | min-height: 100vh; |
| 378 | + background: #f5f7fa; | ||
| 177 | } | 379 | } |
| 178 | 380 | ||
| 179 | -.dashboard-header { | 381 | +// 欢迎区域 |
| 180 | - margin-bottom: 30px; | 382 | +.welcome-section { |
| 383 | + display: flex; | ||
| 384 | + justify-content: space-between; | ||
| 385 | + align-items: center; | ||
| 386 | + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | ||
| 387 | + color: white; | ||
| 388 | + padding: 32px; | ||
| 389 | + border-radius: 12px; | ||
| 390 | + margin-bottom: 32px; | ||
| 391 | + box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3); | ||
| 392 | + | ||
| 393 | + .welcome-content { | ||
| 394 | + .welcome-title { | ||
| 395 | + font-size: 28px; | ||
| 396 | + font-weight: 700; | ||
| 397 | + margin: 0 0 8px 0; | ||
| 398 | + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | ||
| 399 | + } | ||
| 400 | + | ||
| 401 | + .welcome-subtitle { | ||
| 402 | + font-size: 16px; | ||
| 403 | + margin: 0; | ||
| 404 | + opacity: 0.9; | ||
| 405 | + } | ||
| 406 | + } | ||
| 407 | + | ||
| 408 | + .user-info { | ||
| 409 | + display: flex; | ||
| 410 | + align-items: center; | ||
| 411 | + gap: 16px; | ||
| 412 | + | ||
| 413 | + .user-avatar { | ||
| 414 | + width: 60px; | ||
| 415 | + height: 60px; | ||
| 416 | + background: rgba(255, 255, 255, 0.2); | ||
| 417 | + border-radius: 50%; | ||
| 418 | + display: flex; | ||
| 419 | + align-items: center; | ||
| 420 | + justify-content: center; | ||
| 421 | + font-size: 24px; | ||
| 422 | + font-weight: bold; | ||
| 423 | + backdrop-filter: blur(10px); | ||
| 424 | + } | ||
| 425 | + | ||
| 426 | + .user-details { | ||
| 427 | + .user-name { | ||
| 428 | + font-size: 18px; | ||
| 429 | + font-weight: 600; | ||
| 430 | + margin-bottom: 4px; | ||
| 431 | + } | ||
| 432 | + | ||
| 433 | + .user-role { | ||
| 434 | + font-size: 14px; | ||
| 435 | + opacity: 0.8; | ||
| 436 | + } | ||
| 437 | + } | ||
| 438 | + } | ||
| 181 | } | 439 | } |
| 182 | 440 | ||
| 183 | -.dashboard-header h2 { | 441 | +// 统计区域 |
| 184 | - font-size: 20px; | 442 | +.stats-section { |
| 185 | - color: #333; | 443 | + margin-bottom: 32px; |
| 186 | - margin: 0 0 8px 0; | 444 | + |
| 187 | - } | 445 | + .section-title { |
| 188 | - | 446 | + font-size: 20px; |
| 189 | -.dashboard-header p { | 447 | + font-weight: 600; |
| 190 | - color: #666; | 448 | + color: #333; |
| 191 | - margin: 0; | 449 | + margin: 0 0 20px 0; |
| 450 | + } | ||
| 451 | + | ||
| 452 | + .stats-grid { | ||
| 453 | + display: grid; | ||
| 454 | + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | ||
| 455 | + gap: 20px; | ||
| 456 | + } | ||
| 457 | + | ||
| 458 | + .stat-card { | ||
| 459 | + background: white; | ||
| 460 | + padding: 24px; | ||
| 461 | + border-radius: 12px; | ||
| 462 | + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); | ||
| 463 | + display: flex; | ||
| 464 | + align-items: center; | ||
| 465 | + gap: 20px; | ||
| 466 | + transition: all 0.3s ease; | ||
| 467 | + border-left: 4px solid; | ||
| 468 | + | ||
| 469 | + &:hover { | ||
| 470 | + transform: translateY(-4px); | ||
| 471 | + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); | ||
| 472 | + } | ||
| 473 | + | ||
| 474 | + &.primary { | ||
| 475 | + border-left-color: #3498db; | ||
| 476 | + } | ||
| 477 | + | ||
| 478 | + &.success { | ||
| 479 | + border-left-color: #2ecc71; | ||
| 480 | + } | ||
| 481 | + | ||
| 482 | + &.warning { | ||
| 483 | + border-left-color: #f39c12; | ||
| 484 | + } | ||
| 485 | + | ||
| 486 | + &.info { | ||
| 487 | + border-left-color: #9b59b6; | ||
| 488 | + } | ||
| 489 | + | ||
| 490 | + .stat-icon { | ||
| 491 | + font-size: 32px; | ||
| 492 | + width: 60px; | ||
| 493 | + height: 60px; | ||
| 494 | + display: flex; | ||
| 495 | + align-items: center; | ||
| 496 | + justify-content: center; | ||
| 497 | + border-radius: 12px; | ||
| 498 | + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); | ||
| 499 | + } | ||
| 500 | + | ||
| 501 | + .stat-content { | ||
| 502 | + flex: 1; | ||
| 503 | + | ||
| 504 | + .stat-label { | ||
| 505 | + font-size: 14px; | ||
| 506 | + color: #666; | ||
| 507 | + margin-bottom: 8px; | ||
| 508 | + font-weight: 500; | ||
| 509 | + } | ||
| 510 | + | ||
| 511 | + .stat-value { | ||
| 512 | + font-size: 28px; | ||
| 513 | + font-weight: 700; | ||
| 514 | + color: #333; | ||
| 515 | + margin-bottom: 4px; | ||
| 516 | + } | ||
| 517 | + | ||
| 518 | + .stat-change { | ||
| 519 | + font-size: 12px; | ||
| 520 | + font-weight: 500; | ||
| 521 | + | ||
| 522 | + &.positive { | ||
| 523 | + color: #2ecc71; | ||
| 524 | + } | ||
| 525 | + | ||
| 526 | + &:not(.positive) { | ||
| 527 | + color: #666; | ||
| 528 | + } | ||
| 529 | + } | ||
| 530 | + } | ||
| 531 | + } | ||
| 192 | } | 532 | } |
| 193 | 533 | ||
| 194 | -.dashboard-stats { | 534 | +// 主要内容区域 |
| 535 | +.main-content { | ||
| 195 | display: grid; | 536 | display: grid; |
| 196 | - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | 537 | + grid-template-columns: 2fr 1fr; |
| 197 | - gap: 20px; | 538 | + gap: 24px; |
| 198 | - margin-bottom: 30px; | 539 | + margin-bottom: 32px; |
| 199 | } | 540 | } |
| 200 | - | 541 | + |
| 201 | - .stat-card { | 542 | +// 快速操作卡片 |
| 543 | +.quick-actions-card { | ||
| 202 | background: white; | 544 | background: white; |
| 203 | - padding: 20px; | 545 | + padding: 24px; |
| 204 | - border-radius: 8px; | 546 | + border-radius: 12px; |
| 205 | - box-shadow: 0 2px 4px rgba(0,0,0,0.1); | 547 | + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); |
| 206 | - display: flex; | ||
| 207 | - align-items: center; | ||
| 208 | - gap: 15px; | ||
| 209 | - transition: transform 0.3s ease; | ||
| 210 | -} | ||
| 211 | 548 | ||
| 212 | -.stat-card:hover { | 549 | + .card-title { |
| 213 | - transform: translateY(-2px); | 550 | + font-size: 18px; |
| 214 | -} | 551 | + font-weight: 600; |
| 552 | + color: #333; | ||
| 553 | + margin: 0 0 20px 0; | ||
| 554 | + } | ||
| 215 | 555 | ||
| 216 | -.stat-icon { | 556 | + .actions-grid { |
| 217 | - font-size: 20px; | 557 | + display: grid; |
| 218 | - width: 32px; | 558 | + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
| 219 | - height: 32px; | 559 | + gap: 16px; |
| 220 | - display: flex; | 560 | + } |
| 221 | - align-items: center; | ||
| 222 | - justify-content: center; | ||
| 223 | - background: #f8f9fa; | ||
| 224 | - border-radius: 6px; | ||
| 225 | -} | ||
| 226 | 561 | ||
| 227 | -.stat-content h3 { | 562 | + .action-btn { |
| 228 | - margin: 0 0 5px 0; | 563 | + background: white; |
| 229 | - font-size: 12px; | 564 | + border: 2px solid #e9ecef; |
| 230 | - color: #666; | 565 | + border-radius: 12px; |
| 231 | - font-weight: 500; | 566 | + padding: 20px; |
| 232 | -} | 567 | + cursor: pointer; |
| 568 | + transition: all 0.3s ease; | ||
| 569 | + display: flex; | ||
| 570 | + align-items: center; | ||
| 571 | + gap: 16px; | ||
| 572 | + text-align: left; | ||
| 233 | 573 | ||
| 234 | -.stat-number { | 574 | + &:hover { |
| 235 | - margin: 0; | 575 | + transform: translateY(-2px); |
| 236 | - font-size: 18px; | 576 | + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); |
| 237 | - font-weight: bold; | 577 | + } |
| 238 | - color: #333; | ||
| 239 | -} | ||
| 240 | 578 | ||
| 241 | -.dashboard-content { | 579 | + &.primary { |
| 242 | - display: grid; | 580 | + border-color: #3498db; |
| 243 | - grid-template-columns: 1fr 1fr; | 581 | + background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); |
| 244 | - gap: 20px; | 582 | + color: white; |
| 583 | + | ||
| 584 | + &:hover { | ||
| 585 | + background: linear-gradient(135deg, #2980b9 0%, #1f618d 100%); | ||
| 586 | + } | ||
| 587 | + } | ||
| 588 | + | ||
| 589 | + &.success { | ||
| 590 | + border-color: #2ecc71; | ||
| 591 | + background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%); | ||
| 592 | + color: white; | ||
| 593 | + | ||
| 594 | + &:hover { | ||
| 595 | + background: linear-gradient(135deg, #27ae60 0%, #229954 100%); | ||
| 596 | + } | ||
| 597 | + } | ||
| 598 | + | ||
| 599 | + &.warning { | ||
| 600 | + border-color: #f39c12; | ||
| 601 | + background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%); | ||
| 602 | + color: white; | ||
| 603 | + | ||
| 604 | + &:hover { | ||
| 605 | + background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); | ||
| 606 | + } | ||
| 607 | + } | ||
| 608 | + | ||
| 609 | + &.info { | ||
| 610 | + border-color: #9b59b6; | ||
| 611 | + background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%); | ||
| 612 | + color: white; | ||
| 613 | + | ||
| 614 | + &:hover { | ||
| 615 | + background: linear-gradient(135deg, #8e44ad 0%, #7d3c98 100%); | ||
| 616 | + } | ||
| 617 | + } | ||
| 618 | + | ||
| 619 | + &.secondary { | ||
| 620 | + border-color: #95a5a6; | ||
| 621 | + background: linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%); | ||
| 622 | + color: white; | ||
| 623 | + | ||
| 624 | + &:hover { | ||
| 625 | + background: linear-gradient(135deg, #7f8c8d 0%, #6c7b7d 100%); | ||
| 626 | + } | ||
| 627 | + } | ||
| 628 | + | ||
| 629 | + .action-icon { | ||
| 630 | + font-size: 24px; | ||
| 631 | + width: 40px; | ||
| 632 | + height: 40px; | ||
| 633 | + display: flex; | ||
| 634 | + align-items: center; | ||
| 635 | + justify-content: center; | ||
| 636 | + background: rgba(255, 255, 255, 0.2); | ||
| 637 | + border-radius: 8px; | ||
| 638 | + } | ||
| 639 | + | ||
| 640 | + .action-text { | ||
| 641 | + .action-title { | ||
| 642 | + font-size: 16px; | ||
| 643 | + font-weight: 600; | ||
| 644 | + margin-bottom: 4px; | ||
| 645 | + } | ||
| 646 | + | ||
| 647 | + .action-desc { | ||
| 648 | + font-size: 12px; | ||
| 649 | + opacity: 0.8; | ||
| 650 | + } | ||
| 651 | + } | ||
| 652 | + } | ||
| 245 | } | 653 | } |
| 246 | 654 | ||
| 247 | -.dashboard-card { | 655 | +// 系统信息卡片 |
| 656 | +.system-info-card { | ||
| 248 | background: white; | 657 | background: white; |
| 249 | padding: 24px; | 658 | padding: 24px; |
| 250 | - border-radius: 8px; | 659 | + border-radius: 12px; |
| 251 | - box-shadow: 0 2px 4px rgba(0,0,0,0.1); | 660 | + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); |
| 252 | -} | ||
| 253 | 661 | ||
| 254 | -.dashboard-card h3 { | 662 | + .card-title { |
| 255 | - margin: 0 0 16px 0; | 663 | + font-size: 18px; |
| 256 | - color: #333; | 664 | + font-weight: 600; |
| 257 | - font-size: 16px; | 665 | + color: #333; |
| 258 | - font-weight: 600; | 666 | + margin: 0 0 20px 0; |
| 259 | -} | 667 | + } |
| 260 | 668 | ||
| 261 | -.info-grid { | 669 | + .info-list { |
| 262 | - display: grid; | 670 | + .info-item { |
| 263 | - gap: 12px; | 671 | + display: flex; |
| 672 | + justify-content: space-between; | ||
| 673 | + align-items: center; | ||
| 674 | + padding: 12px 0; | ||
| 675 | + border-bottom: 1px solid #f0f0f0; | ||
| 676 | + | ||
| 677 | + &:last-child { | ||
| 678 | + border-bottom: none; | ||
| 679 | + } | ||
| 680 | + | ||
| 681 | + .info-label { | ||
| 682 | + font-weight: 500; | ||
| 683 | + color: #666; | ||
| 684 | + font-size: 14px; | ||
| 685 | + } | ||
| 686 | + | ||
| 687 | + .info-value { | ||
| 688 | + color: #333; | ||
| 689 | + font-weight: 500; | ||
| 690 | + | ||
| 691 | + &.status-normal { | ||
| 692 | + color: #2ecc71; | ||
| 693 | + } | ||
| 694 | + | ||
| 695 | + &.status-error { | ||
| 696 | + color: #e74c3c; | ||
| 697 | + } | ||
| 698 | + } | ||
| 699 | + } | ||
| 700 | + } | ||
| 264 | } | 701 | } |
| 265 | 702 | ||
| 266 | - .info-item { | 703 | +// 最近活动 |
| 704 | +.recent-activities { | ||
| 705 | + background: white; | ||
| 706 | + padding: 24px; | ||
| 707 | + border-radius: 12px; | ||
| 708 | + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); | ||
| 709 | + | ||
| 710 | + .card-title { | ||
| 711 | + font-size: 18px; | ||
| 712 | + font-weight: 600; | ||
| 713 | + color: #333; | ||
| 714 | + margin: 0 0 20px 0; | ||
| 715 | + } | ||
| 716 | + | ||
| 717 | + .activity-list { | ||
| 718 | + .activity-item { | ||
| 719 | + display: flex; | ||
| 720 | + align-items: center; | ||
| 721 | + gap: 16px; | ||
| 722 | + padding: 12px 0; | ||
| 723 | + border-bottom: 1px solid #f0f0f0; | ||
| 724 | + | ||
| 725 | + &:last-child { | ||
| 726 | + border-bottom: none; | ||
| 727 | + } | ||
| 728 | + | ||
| 729 | + .activity-icon { | ||
| 730 | + width: 40px; | ||
| 731 | + height: 40px; | ||
| 732 | + border-radius: 8px; | ||
| 267 | display: flex; | 733 | display: flex; |
| 268 | - justify-content: space-between; | ||
| 269 | align-items: center; | 734 | align-items: center; |
| 270 | - padding: 8px 0; | 735 | + justify-content: center; |
| 271 | - border-bottom: 1px solid #f0f0f0; | 736 | + font-size: 16px; |
| 272 | -} | 737 | + |
| 273 | - | 738 | + &.order { |
| 274 | -.info-item:last-child { | 739 | + background: #e3f2fd; |
| 275 | - border-bottom: none; | 740 | + color: #1976d2; |
| 276 | } | 741 | } |
| 277 | - | ||
| 278 | -.info-item label { | ||
| 279 | - font-weight: 500; | ||
| 280 | - color: #666; | ||
| 281 | -} | ||
| 282 | 742 | ||
| 283 | -.info-item span { | 743 | + &.delivery { |
| 284 | - color: #333; | 744 | + background: #e8f5e8; |
| 285 | -} | 745 | + color: #2e7d32; |
| 746 | + } | ||
| 286 | 747 | ||
| 287 | -.quick-actions { | 748 | + &.invoice { |
| 288 | - display: grid; | 749 | + background: #fff3e0; |
| 289 | - grid-template-columns: 1fr 1fr; | 750 | + color: #f57c00; |
| 290 | - gap: 12px; | 751 | + } |
| 291 | -} | ||
| 292 | 752 | ||
| 293 | -.action-btn { | 753 | + &.rebate { |
| 294 | - background: #3498db; | 754 | + background: #f3e5f5; |
| 295 | - color: white; | 755 | + color: #7b1fa2; |
| 296 | - border: none; | 756 | + } |
| 297 | - padding: 8px 12px; | 757 | + |
| 298 | - border-radius: 4px; | 758 | + &.system { |
| 299 | - cursor: pointer; | 759 | + background: #f1f8e9; |
| 300 | - font-size: 12px; | 760 | + color: #558b2f; |
| 301 | - transition: background-color 0.3s; | 761 | + } |
| 762 | + } | ||
| 763 | + | ||
| 764 | + .activity-content { | ||
| 765 | + flex: 1; | ||
| 766 | + | ||
| 767 | + .activity-title { | ||
| 768 | + font-size: 14px; | ||
| 769 | + color: #333; | ||
| 770 | + margin-bottom: 4px; | ||
| 771 | + font-weight: 500; | ||
| 772 | + } | ||
| 773 | + | ||
| 774 | + .activity-time { | ||
| 775 | + font-size: 12px; | ||
| 776 | + color: #666; | ||
| 777 | + } | ||
| 778 | + } | ||
| 779 | + } | ||
| 780 | + } | ||
| 781 | + | ||
| 782 | + .no-activities, .loading-activities { | ||
| 783 | + text-align: center; | ||
| 784 | + color: #666; | ||
| 785 | + padding: 20px; | ||
| 786 | + font-size: 14px; | ||
| 787 | + } | ||
| 302 | } | 788 | } |
| 303 | 789 | ||
| 304 | -.action-btn:hover { | 790 | +// 响应式设计 |
| 305 | - background: #2980b9; | 791 | +@media (max-width: 1200px) { |
| 792 | + .main-content { | ||
| 793 | + grid-template-columns: 1fr; | ||
| 794 | + } | ||
| 306 | } | 795 | } |
| 307 | 796 | ||
| 308 | @media (max-width: 768px) { | 797 | @media (max-width: 768px) { |
| 309 | - .dashboard-stats { | 798 | + .dashboard-container { |
| 310 | - grid-template-columns: 1fr; | 799 | + padding: 16px; |
| 311 | } | 800 | } |
| 312 | - | 801 | + |
| 313 | - .dashboard-content { | 802 | + .welcome-section { |
| 314 | - grid-template-columns: 1fr; | 803 | + flex-direction: column; |
| 804 | + text-align: center; | ||
| 805 | + gap: 20px; | ||
| 806 | + | ||
| 807 | + .user-info { | ||
| 808 | + justify-content: center; | ||
| 315 | } | 809 | } |
| 316 | - | 810 | + } |
| 317 | - .quick-actions { | 811 | + |
| 812 | + .stats-grid { | ||
| 813 | + grid-template-columns: 1fr; | ||
| 814 | + } | ||
| 815 | + | ||
| 816 | + .actions-grid { | ||
| 318 | grid-template-columns: 1fr; | 817 | grid-template-columns: 1fr; |
| 319 | } | 818 | } |
| 320 | } | 819 | } | ... | ... |
| ... | @@ -734,8 +734,38 @@ const handleBatchProcess = () => { | ... | @@ -734,8 +734,38 @@ const handleBatchProcess = () => { |
| 734 | } | 734 | } |
| 735 | 735 | ||
| 736 | // 导出 | 736 | // 导出 |
| 737 | -const handleExport = () => { | 737 | +const handleExport = async () => { |
| 738 | - ElMessage.info('导出功能开发中...') | 738 | + try { |
| 739 | + await ElMessageBox.confirm('确定要导出异常工单数据吗?', '确认导出', { | ||
| 740 | + confirmButtonText: '确定', | ||
| 741 | + cancelButtonText: '取消', | ||
| 742 | + type: 'warning' | ||
| 743 | + }) | ||
| 744 | + | ||
| 745 | + ElMessage.info('正在导出数据,请稍候...') | ||
| 746 | + | ||
| 747 | + const response = await exceptionWorkorderApi.exportExceptionWorkorders(searchParams.value) | ||
| 748 | + | ||
| 749 | + // 创建下载链接 | ||
| 750 | + const blob = new Blob([response as unknown as BlobPart], { | ||
| 751 | + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' | ||
| 752 | + }) | ||
| 753 | + const url = window.URL.createObjectURL(blob) | ||
| 754 | + const link = document.createElement('a') | ||
| 755 | + link.href = url | ||
| 756 | + link.download = `异常工单数据_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.xlsx` | ||
| 757 | + document.body.appendChild(link) | ||
| 758 | + link.click() | ||
| 759 | + document.body.removeChild(link) | ||
| 760 | + window.URL.revokeObjectURL(url) | ||
| 761 | + | ||
| 762 | + ElMessage.success('导出成功') | ||
| 763 | + } catch (error: any) { | ||
| 764 | + if (error !== 'cancel') { | ||
| 765 | + console.error('导出失败:', error) | ||
| 766 | + ElMessage.error('导出失败: ' + (error?.message || '未知错误')) | ||
| 767 | + } | ||
| 768 | + } | ||
| 739 | } | 769 | } |
| 740 | 770 | ||
| 741 | // 提交新增 | 771 | // 提交新增 | ... | ... |
| ... | @@ -2,51 +2,64 @@ | ... | @@ -2,51 +2,64 @@ |
| 2 | <div class="rebate-page"> | 2 | <div class="rebate-page"> |
| 3 | <!-- 搜索区域 --> | 3 | <!-- 搜索区域 --> |
| 4 | <div class="search-section"> | 4 | <div class="search-section"> |
| 5 | - <div class="search-form"> | 5 | + <div class="search-row"> |
| 6 | - <div class="search-row"> | 6 | + <div class="search-item"> |
| 7 | - <div class="search-item"> | 7 | + <label>经销商名称:</label> |
| 8 | - <span class="search-label">经销商名称</span> | 8 | + <input |
| 9 | - <el-input | 9 | + v-model="searchForm.dealerName" |
| 10 | - v-model="searchForm.dealerName" | 10 | + class="search-input" |
| 11 | placeholder="请输入经销商名称" | 11 | placeholder="请输入经销商名称" |
| 12 | - clearable | 12 | + /> |
| 13 | - size="small" | 13 | + </div> |
| 14 | - style="width: 160px" | 14 | + <div class="search-item"> |
| 15 | - /> | 15 | + <label>返利编号:</label> |
| 16 | - </div> | 16 | + <input |
| 17 | - <div class="search-item"> | 17 | + v-model="searchForm.rebateNo" |
| 18 | - <span class="search-label">返利编号</span> | 18 | + class="search-input" |
| 19 | - <el-input | 19 | + placeholder="请输入返利编号" |
| 20 | - v-model="searchForm.rebateNo" | 20 | + /> |
| 21 | - placeholder="请输入返利编号" | 21 | + </div> |
| 22 | - clearable | 22 | + <div class="search-item"> |
| 23 | - size="small" | 23 | + <label>操作类型:</label> |
| 24 | - style="width: 160px" | 24 | + <select v-model="searchForm.operateType" class="search-select"> |
| 25 | - /> | 25 | + <option value="">所有</option> |
| 26 | - </div> | 26 | + <option value="1">新增</option> |
| 27 | - <div class="search-item"> | 27 | + <option value="2">修改</option> |
| 28 | - <span class="search-label">日期</span> | 28 | + <option value="3">删除</option> |
| 29 | - <el-date-picker | 29 | + </select> |
| 30 | - v-model="dateRange" | 30 | + </div> |
| 31 | - type="daterange" | 31 | + <div class="search-item"> |
| 32 | - range-separator="至" | 32 | + <label>计算状态:</label> |
| 33 | - start-placeholder="开始日期" | 33 | + <select v-model="searchForm.calcFlag" class="search-select"> |
| 34 | - end-placeholder="结束日期" | 34 | + <option value="">所有</option> |
| 35 | - size="small" | 35 | + <option value="0">未计算</option> |
| 36 | - format="YYYY-MM-DD" | 36 | + <option value="1">已计算</option> |
| 37 | - value-format="YYYY-MM-DD" | 37 | + </select> |
| 38 | - @change="handleDateRangeChange" | 38 | + </div> |
| 39 | - style="width: 240px" | 39 | + </div> |
| 40 | - /> | 40 | + <div class="search-row"> |
| 41 | - </div> | 41 | + <div class="search-item"> |
| 42 | - <div class="search-actions"> | 42 | + <label>审核状态:</label> |
| 43 | - <el-button type="primary" size="small" @click="handleSearch" :loading="loading"> | 43 | + <select v-model="searchForm.auditStatus" class="search-select"> |
| 44 | - 查询 | 44 | + <option value="">所有</option> |
| 45 | - </el-button> | 45 | + <option value="0">待审核</option> |
| 46 | - <el-button size="small" @click="handleReset"> | 46 | + <option value="1">审核通过</option> |
| 47 | - 重置 | 47 | + <option value="2">审核中</option> |
| 48 | - </el-button> | 48 | + <option value="3">审核拒绝</option> |
| 49 | - </div> | 49 | + </select> |
| 50 | + </div> | ||
| 51 | + <div class="search-item"> | ||
| 52 | + <label>数据来源:</label> | ||
| 53 | + <select v-model="searchForm.dataSource" class="search-select"> | ||
| 54 | + <option value="">所有</option> | ||
| 55 | + <option value="ERP系统">ERP系统</option> | ||
| 56 | + <option value="手工录入">手工录入</option> | ||
| 57 | + <option value="API导入">API导入</option> | ||
| 58 | + </select> | ||
| 59 | + </div> | ||
| 60 | + <div class="search-actions"> | ||
| 61 | + <button @click="handleSearch" class="search-btn" :disabled="loading">🔍 搜索</button> | ||
| 62 | + <button @click="handleReset" class="reset-btn">🔄 重置</button> | ||
| 50 | </div> | 63 | </div> |
| 51 | </div> | 64 | </div> |
| 52 | </div> | 65 | </div> |
| ... | @@ -96,95 +109,103 @@ | ... | @@ -96,95 +109,103 @@ |
| 96 | </div> | 109 | </div> |
| 97 | </div> | 110 | </div> |
| 98 | 111 | ||
| 112 | + <!-- 操作按钮区域 --> | ||
| 113 | + <div class="action-section"> | ||
| 114 | + <div class="action-buttons"> | ||
| 115 | + <button @click="handleExport" class="action-btn secondary" :disabled="exportLoading"> | ||
| 116 | + 📋 导出 | ||
| 117 | + </button> | ||
| 118 | + </div> | ||
| 119 | + </div> | ||
| 120 | + | ||
| 99 | <!-- 数据表格 --> | 121 | <!-- 数据表格 --> |
| 100 | <div class="table-section"> | 122 | <div class="table-section"> |
| 101 | <div class="table-header"> | 123 | <div class="table-header"> |
| 102 | - <div class="table-title"> | 124 | + <div class="table-controls"> |
| 103 | - 返利记录 | 125 | + <button @click="handleTableSearch" class="control-btn">🔍</button> |
| 104 | - </div> | 126 | + <button @click="handleTableRefresh" class="control-btn">🔄</button> |
| 105 | - <div class="table-actions"> | 127 | + <button @click="handleTableExport" class="control-btn">📋</button> |
| 106 | - <el-button type="primary" size="small" @click="handleExport" :loading="exportLoading"> | 128 | + <button @click="handleTableViewToggle" class="control-btn">⊞</button> |
| 107 | - <el-icon><Download /></el-icon> | ||
| 108 | - 导出 | ||
| 109 | - </el-button> | ||
| 110 | - <div class="table-info"> | ||
| 111 | - 数据状态: {{ loading ? '加载中...' : `共${rebateList.length}条记录` }} | ||
| 112 | - </div> | ||
| 113 | </div> | 129 | </div> |
| 114 | </div> | 130 | </div> |
| 115 | 131 | ||
| 116 | - <el-table | 132 | + <div class="table-container"> |
| 117 | - v-loading="loading" | 133 | + <div v-if="loading" class="loading-overlay"> |
| 118 | - :data="rebateList" | 134 | + <div class="loading-spinner">加载中...</div> |
| 119 | - style="width: 100%" | 135 | + </div> |
| 120 | - :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 'normal' }" | 136 | + <table class="data-table"> |
| 121 | - :empty-text="rebateList.length === 0 ? '暂无数据' : ''" | 137 | + <thead> |
| 122 | - size="small" | 138 | + <tr> |
| 123 | - > | 139 | + <th>返利编号</th> |
| 124 | - <el-table-column prop="rebateNo" label="返利编号" width="140" align="center" /> | 140 | + <th>订单编号</th> |
| 125 | - <el-table-column prop="orderNo" label="订单编号" width="140" align="center" /> | 141 | + <th>经销商代码</th> |
| 126 | - <el-table-column prop="dealerCode" label="经销商代码" width="120" align="center" /> | 142 | + <th>经销商名称</th> |
| 127 | - <el-table-column prop="dealerName" label="经销商名称" min-width="150" /> | 143 | + <th>产品编码</th> |
| 128 | - <el-table-column prop="productCode" label="产品编码" width="120" align="center" /> | 144 | + <th>返利金额</th> |
| 129 | - <el-table-column prop="rebateAmount" label="返利金额" width="120" align="right"> | 145 | + <th>返利日期</th> |
| 130 | - <template #default="{ row }"> | 146 | + <th>操作类型</th> |
| 131 | - <span class="amount-text">{{ formatAmount(row.rebateAmount) }}</span> | 147 | + <th>计算状态</th> |
| 132 | - </template> | 148 | + <th>更新时间</th> |
| 133 | - </el-table-column> | 149 | + <th>操作</th> |
| 134 | - <el-table-column prop="rebateDate" label="返利日期" width="120" align="center" /> | 150 | + </tr> |
| 135 | - <el-table-column prop="operateTypeText" label="操作类型" width="100" align="center"> | 151 | + </thead> |
| 136 | - <template #default="{ row }"> | 152 | + <tbody> |
| 137 | - <el-tag :type="getOperateTypeTagType(row.operateType)" size="small"> | 153 | + <tr v-if="rebateList.length === 0"> |
| 138 | - {{ row.operateTypeText || getOperateTypeText(row.operateType) }} | 154 | + <td colspan="11" class="empty-data">暂无数据</td> |
| 139 | - </el-tag> | 155 | + </tr> |
| 140 | - </template> | 156 | + <tr v-for="row in rebateList" :key="row.rebateId"> |
| 141 | - </el-table-column> | 157 | + <td>{{ row.rebateNo }}</td> |
| 142 | - <el-table-column prop="calcFlagText" label="计算状态" width="100" align="center"> | 158 | + <td>{{ row.orderNo }}</td> |
| 143 | - <template #default="{ row }"> | 159 | + <td>{{ row.dealerCode }}</td> |
| 144 | - <el-tag :type="getCalcFlagTagType(row.calcFlag)" size="small"> | 160 | + <td>{{ row.dealerName }}</td> |
| 145 | - {{ row.calcFlagText || getCalcFlagText(row.calcFlag) }} | 161 | + <td>{{ row.productCode }}</td> |
| 146 | - </el-tag> | 162 | + <td class="amount-text">{{ formatAmount(row.rebateAmount) }}</td> |
| 147 | - </template> | 163 | + <td>{{ row.rebateDate }}</td> |
| 148 | - </el-table-column> | 164 | + <td> |
| 149 | - <el-table-column prop="updateTime" label="更新时间" width="150" align="center"> | 165 | + <span :class="getOperateTypeClass(row.operateType || 0)"> |
| 150 | - <template #default="{ row }"> | 166 | + {{ row.operateTypeText || getOperateTypeText(row.operateType || 0) }} |
| 151 | - {{ formatDate(row.updateTime) }} | 167 | + </span> |
| 152 | - </template> | 168 | + </td> |
| 153 | - </el-table-column> | 169 | + <td> |
| 154 | - <el-table-column label="操作" width="80" align="center"> | 170 | + <span :class="getCalcFlagClass(row.calcFlag || 0)"> |
| 155 | - <template #default="{ row }"> | 171 | + {{ row.calcFlagText || getCalcFlagText(row.calcFlag || 0) }} |
| 156 | - <el-button type="primary" size="small" link @click="handleViewDetail(row)"> | 172 | + </span> |
| 157 | - 详情 | 173 | + </td> |
| 158 | - </el-button> | 174 | + <td>{{ formatDate(row.updateTime || '') }}</td> |
| 159 | - </template> | 175 | + <td> |
| 160 | - </el-table-column> | 176 | + <button @click="handleViewDetail(row)" class="action-link">详情</button> |
| 161 | - </el-table> | 177 | + </td> |
| 178 | + </tr> | ||
| 179 | + </tbody> | ||
| 180 | + </table> | ||
| 181 | + </div> | ||
| 162 | 182 | ||
| 163 | <!-- 分页 --> | 183 | <!-- 分页 --> |
| 164 | <div class="pagination-wrapper"> | 184 | <div class="pagination-wrapper"> |
| 165 | <div class="pagination-info"> | 185 | <div class="pagination-info"> |
| 166 | 共 {{ pagination.total }} 条,{{ pagination.pageSize }}/页 | 186 | 共 {{ pagination.total }} 条,{{ pagination.pageSize }}/页 |
| 167 | </div> | 187 | </div> |
| 168 | - <el-pagination | 188 | + <div class="pagination-controls"> |
| 169 | - v-model:current-page="pagination.pageNum" | 189 | + <button @click="handlePrevPage" :disabled="pagination.pageNum <= 1" class="page-btn">‹</button> |
| 170 | - v-model:page-size="pagination.pageSize" | 190 | + <button |
| 171 | - :page-sizes="[10, 20, 50, 100]" | 191 | + v-for="page in getPageNumbers()" |
| 172 | - :total="pagination.total" | 192 | + :key="page" |
| 173 | - layout="sizes, prev, pager, next" | 193 | + @click="handlePageChange(page)" |
| 174 | - @size-change="handleSizeChange" | 194 | + :class="['page-btn', { active: page === pagination.pageNum }]" |
| 175 | - @current-change="handleCurrentChange" | 195 | + > |
| 176 | - small | 196 | + {{ page }} |
| 177 | - /> | 197 | + </button> |
| 198 | + <button @click="handleNextPage" :disabled="pagination.pageNum >= getTotalPages()" class="page-btn">›</button> | ||
| 199 | + </div> | ||
| 178 | <div class="pagination-jump"> | 200 | <div class="pagination-jump"> |
| 179 | 前往 | 201 | 前往 |
| 180 | - <el-input | 202 | + <input |
| 181 | v-model="jumpPage" | 203 | v-model="jumpPage" |
| 182 | - size="small" | 204 | + class="jump-input" |
| 183 | - style="width: 50px; margin: 0 8px;" | ||
| 184 | @keyup.enter="handleJumpPage" | 205 | @keyup.enter="handleJumpPage" |
| 185 | /> | 206 | /> |
| 186 | 页 | 207 | 页 |
| 187 | - </div> | 208 | + </div> |
| 188 | </div> | 209 | </div> |
| 189 | </div> | 210 | </div> |
| 190 | 211 | ||
| ... | @@ -435,6 +456,8 @@ const searchForm = reactive<RebateSearchParams>({ | ... | @@ -435,6 +456,8 @@ const searchForm = reactive<RebateSearchParams>({ |
| 435 | productCode: '', | 456 | productCode: '', |
| 436 | operateType: undefined, | 457 | operateType: undefined, |
| 437 | calcFlag: undefined, | 458 | calcFlag: undefined, |
| 459 | + auditStatus: undefined, | ||
| 460 | + dataSource: '', | ||
| 438 | rebateStartDate: '', | 461 | rebateStartDate: '', |
| 439 | rebateEndDate: '' | 462 | rebateEndDate: '' |
| 440 | }) | 463 | }) |
| ... | @@ -730,15 +753,17 @@ const handleReset = () => { | ... | @@ -730,15 +753,17 @@ const handleReset = () => { |
| 730 | Object.assign(searchForm, { | 753 | Object.assign(searchForm, { |
| 731 | pageNum: 1, | 754 | pageNum: 1, |
| 732 | pageSize: 10, | 755 | pageSize: 10, |
| 733 | - rebateNo: undefined, | 756 | + rebateNo: '', |
| 734 | - dealerName: undefined, | 757 | + dealerCode: '', |
| 735 | - productName: undefined, | 758 | + dealerName: '', |
| 736 | - rebateType: undefined, | 759 | + productCode: '', |
| 737 | - status: undefined, | 760 | + operateType: undefined, |
| 738 | - applyStartTime: undefined, | 761 | + calcFlag: undefined, |
| 739 | - applyEndTime: undefined | 762 | + auditStatus: undefined, |
| 763 | + dataSource: '', | ||
| 764 | + rebateStartDate: '', | ||
| 765 | + rebateEndDate: '' | ||
| 740 | }) | 766 | }) |
| 741 | - applyDateRange.value = null | ||
| 742 | pagination.pageNum = 1 | 767 | pagination.pageNum = 1 |
| 743 | getRebateList() | 768 | getRebateList() |
| 744 | } | 769 | } |
| ... | @@ -748,6 +773,104 @@ const handleRefresh = () => { | ... | @@ -748,6 +773,104 @@ const handleRefresh = () => { |
| 748 | getRebateList() | 773 | getRebateList() |
| 749 | } | 774 | } |
| 750 | 775 | ||
| 776 | +// 表格控制按钮 | ||
| 777 | +const handleTableSearch = () => { | ||
| 778 | + handleSearch() | ||
| 779 | +} | ||
| 780 | + | ||
| 781 | +const handleTableRefresh = () => { | ||
| 782 | + handleRefresh() | ||
| 783 | +} | ||
| 784 | + | ||
| 785 | +const handleTableExport = () => { | ||
| 786 | + handleExport() | ||
| 787 | +} | ||
| 788 | + | ||
| 789 | +const handleTableViewToggle = () => { | ||
| 790 | + // 切换视图模式 | ||
| 791 | + console.log('切换视图模式') | ||
| 792 | +} | ||
| 793 | + | ||
| 794 | +// 分页控制 | ||
| 795 | +const handlePrevPage = () => { | ||
| 796 | + if (pagination.pageNum > 1) { | ||
| 797 | + pagination.pageNum-- | ||
| 798 | + getRebateList() | ||
| 799 | + } | ||
| 800 | +} | ||
| 801 | + | ||
| 802 | +const handleNextPage = () => { | ||
| 803 | + if (pagination.pageNum < getTotalPages()) { | ||
| 804 | + pagination.pageNum++ | ||
| 805 | + getRebateList() | ||
| 806 | + } | ||
| 807 | +} | ||
| 808 | + | ||
| 809 | +const handlePageChange = (page: number | string) => { | ||
| 810 | + if (typeof page === 'number') { | ||
| 811 | + pagination.pageNum = page | ||
| 812 | + getRebateList() | ||
| 813 | + } | ||
| 814 | +} | ||
| 815 | + | ||
| 816 | +const getPageNumbers = () => { | ||
| 817 | + const totalPages = getTotalPages() | ||
| 818 | + const current = pagination.pageNum | ||
| 819 | + const pages = [] | ||
| 820 | + | ||
| 821 | + if (totalPages <= 7) { | ||
| 822 | + for (let i = 1; i <= totalPages; i++) { | ||
| 823 | + pages.push(i) | ||
| 824 | + } | ||
| 825 | + } else { | ||
| 826 | + if (current <= 4) { | ||
| 827 | + for (let i = 1; i <= 5; i++) { | ||
| 828 | + pages.push(i) | ||
| 829 | + } | ||
| 830 | + pages.push('...') | ||
| 831 | + pages.push(totalPages) | ||
| 832 | + } else if (current >= totalPages - 3) { | ||
| 833 | + pages.push(1) | ||
| 834 | + pages.push('...') | ||
| 835 | + for (let i = totalPages - 4; i <= totalPages; i++) { | ||
| 836 | + pages.push(i) | ||
| 837 | + } | ||
| 838 | + } else { | ||
| 839 | + pages.push(1) | ||
| 840 | + pages.push('...') | ||
| 841 | + for (let i = current - 1; i <= current + 1; i++) { | ||
| 842 | + pages.push(i) | ||
| 843 | + } | ||
| 844 | + pages.push('...') | ||
| 845 | + pages.push(totalPages) | ||
| 846 | + } | ||
| 847 | + } | ||
| 848 | + | ||
| 849 | + return pages | ||
| 850 | +} | ||
| 851 | + | ||
| 852 | +const getTotalPages = () => { | ||
| 853 | + return Math.ceil(pagination.total / pagination.pageSize) | ||
| 854 | +} | ||
| 855 | + | ||
| 856 | +// 状态样式类 | ||
| 857 | +const getOperateTypeClass = (type: number) => { | ||
| 858 | + const classes = { | ||
| 859 | + 1: 'status-tag success', | ||
| 860 | + 2: 'status-tag warning', | ||
| 861 | + 3: 'status-tag danger' | ||
| 862 | + } | ||
| 863 | + return classes[type as keyof typeof classes] || 'status-tag' | ||
| 864 | +} | ||
| 865 | + | ||
| 866 | +const getCalcFlagClass = (flag: number) => { | ||
| 867 | + const classes = { | ||
| 868 | + 0: 'status-tag warning', | ||
| 869 | + 1: 'status-tag success' | ||
| 870 | + } | ||
| 871 | + return classes[flag as keyof typeof classes] || 'status-tag' | ||
| 872 | +} | ||
| 873 | + | ||
| 751 | // 分页变化 | 874 | // 分页变化 |
| 752 | const handleSizeChange = (size: number) => { | 875 | const handleSizeChange = (size: number) => { |
| 753 | pagination.pageSize = size | 876 | pagination.pageSize = size |
| ... | @@ -784,11 +907,11 @@ const handleViewDetail = async (row: Rebate) => { | ... | @@ -784,11 +907,11 @@ const handleViewDetail = async (row: Rebate) => { |
| 784 | const response = await rebateApi.getRebateById(row.rebateId) | 907 | const response = await rebateApi.getRebateById(row.rebateId) |
| 785 | console.log('详情接口响应:', response) | 908 | console.log('详情接口响应:', response) |
| 786 | 909 | ||
| 787 | - // 根据request.ts拦截器的处理,response.data已经是实际数据 | 910 | + // 根据request.ts拦截器的处理,response直接就是数据对象 |
| 788 | - if (response && response.data) { | 911 | + if (response) { |
| 789 | - rebateDetail.value = response.data | 912 | + rebateDetail.value = response as unknown as Rebate |
| 790 | - detailDialogVisible.value = true | 913 | + detailDialogVisible.value = true |
| 791 | - console.log('详情数据:', response.data) | 914 | + console.log('详情数据:', response) |
| 792 | } else { | 915 | } else { |
| 793 | ElMessage.error('获取返利详情失败') | 916 | ElMessage.error('获取返利详情失败') |
| 794 | } | 917 | } |
| ... | @@ -1478,30 +1601,154 @@ onMounted(async () => { | ... | @@ -1478,30 +1601,154 @@ onMounted(async () => { |
| 1478 | border-radius: 4px; | 1601 | border-radius: 4px; |
| 1479 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); | 1602 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); |
| 1480 | 1603 | ||
| 1481 | - .search-form { | 1604 | + .search-row { |
| 1482 | - .search-row { | 1605 | + display: flex; |
| 1606 | + align-items: center; | ||
| 1607 | + gap: 24px; | ||
| 1608 | + flex-wrap: wrap; | ||
| 1609 | + margin-bottom: 12px; | ||
| 1610 | + | ||
| 1611 | + &:last-child { | ||
| 1612 | + margin-bottom: 0; | ||
| 1613 | + } | ||
| 1614 | + | ||
| 1615 | + .search-item { | ||
| 1483 | display: flex; | 1616 | display: flex; |
| 1484 | align-items: center; | 1617 | align-items: center; |
| 1485 | - gap: 24px; | 1618 | + gap: 8px; |
| 1486 | - flex-wrap: wrap; | ||
| 1487 | 1619 | ||
| 1488 | - .search-item { | 1620 | + label { |
| 1489 | - display: flex; | 1621 | + font-size: 14px; |
| 1490 | - align-items: center; | 1622 | + color: #333; |
| 1491 | - gap: 8px; | 1623 | + white-space: nowrap; |
| 1624 | + min-width: 80px; | ||
| 1625 | + } | ||
| 1492 | 1626 | ||
| 1493 | - .search-label { | 1627 | + .search-input { |
| 1494 | - font-size: 14px; | 1628 | + padding: 8px 12px; |
| 1495 | - color: #333; | 1629 | + border: 1px solid #ddd; |
| 1496 | - white-space: nowrap; | 1630 | + border-radius: 4px; |
| 1497 | - min-width: 70px; | 1631 | + font-size: 14px; |
| 1632 | + width: 160px; | ||
| 1633 | + transition: border-color 0.3s; | ||
| 1634 | + | ||
| 1635 | + &:focus { | ||
| 1636 | + outline: none; | ||
| 1637 | + border-color: #409eff; | ||
| 1498 | } | 1638 | } |
| 1499 | } | 1639 | } |
| 1500 | 1640 | ||
| 1501 | - .search-actions { | 1641 | + .search-select { |
| 1502 | - margin-left: auto; | 1642 | + padding: 8px 12px; |
| 1503 | - display: flex; | 1643 | + border: 1px solid #ddd; |
| 1504 | - gap: 8px; | 1644 | + border-radius: 4px; |
| 1645 | + font-size: 14px; | ||
| 1646 | + width: 120px; | ||
| 1647 | + background: white; | ||
| 1648 | + cursor: pointer; | ||
| 1649 | + transition: border-color 0.3s; | ||
| 1650 | + | ||
| 1651 | + &:focus { | ||
| 1652 | + outline: none; | ||
| 1653 | + border-color: #409eff; | ||
| 1654 | + } | ||
| 1655 | + } | ||
| 1656 | + } | ||
| 1657 | + | ||
| 1658 | + .search-actions { | ||
| 1659 | + margin-left: auto; | ||
| 1660 | + display: flex; | ||
| 1661 | + gap: 8px; | ||
| 1662 | + | ||
| 1663 | + .search-btn, .reset-btn { | ||
| 1664 | + padding: 8px 16px; | ||
| 1665 | + border: 1px solid #ddd; | ||
| 1666 | + border-radius: 4px; | ||
| 1667 | + font-size: 14px; | ||
| 1668 | + cursor: pointer; | ||
| 1669 | + transition: all 0.3s; | ||
| 1670 | + background: white; | ||
| 1671 | + | ||
| 1672 | + &:hover { | ||
| 1673 | + background: #f5f7fa; | ||
| 1674 | + } | ||
| 1675 | + | ||
| 1676 | + &:disabled { | ||
| 1677 | + opacity: 0.6; | ||
| 1678 | + cursor: not-allowed; | ||
| 1679 | + } | ||
| 1680 | + } | ||
| 1681 | + | ||
| 1682 | + .search-btn { | ||
| 1683 | + background: #409eff; | ||
| 1684 | + color: white; | ||
| 1685 | + border-color: #409eff; | ||
| 1686 | + | ||
| 1687 | + &:hover { | ||
| 1688 | + background: #66b1ff; | ||
| 1689 | + } | ||
| 1690 | + } | ||
| 1691 | + } | ||
| 1692 | + } | ||
| 1693 | +} | ||
| 1694 | + | ||
| 1695 | +.action-section { | ||
| 1696 | + background: white; | ||
| 1697 | + padding: 12px 20px; | ||
| 1698 | + margin-bottom: 16px; | ||
| 1699 | + border-radius: 4px; | ||
| 1700 | + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); | ||
| 1701 | + | ||
| 1702 | + .action-buttons { | ||
| 1703 | + display: flex; | ||
| 1704 | + gap: 12px; | ||
| 1705 | + | ||
| 1706 | + .action-btn { | ||
| 1707 | + padding: 8px 16px; | ||
| 1708 | + border: 1px solid #ddd; | ||
| 1709 | + border-radius: 4px; | ||
| 1710 | + font-size: 14px; | ||
| 1711 | + cursor: pointer; | ||
| 1712 | + transition: all 0.3s; | ||
| 1713 | + background: white; | ||
| 1714 | + | ||
| 1715 | + &:hover { | ||
| 1716 | + background: #f5f7fa; | ||
| 1717 | + } | ||
| 1718 | + | ||
| 1719 | + &:disabled { | ||
| 1720 | + opacity: 0.6; | ||
| 1721 | + cursor: not-allowed; | ||
| 1722 | + } | ||
| 1723 | + | ||
| 1724 | + &.primary { | ||
| 1725 | + background: #409eff; | ||
| 1726 | + color: white; | ||
| 1727 | + border-color: #409eff; | ||
| 1728 | + | ||
| 1729 | + &:hover { | ||
| 1730 | + background: #66b1ff; | ||
| 1731 | + } | ||
| 1732 | + } | ||
| 1733 | + | ||
| 1734 | + &.secondary { | ||
| 1735 | + background: #67c23a; | ||
| 1736 | + color: white; | ||
| 1737 | + border-color: #67c23a; | ||
| 1738 | + | ||
| 1739 | + &:hover { | ||
| 1740 | + background: #85ce61; | ||
| 1741 | + } | ||
| 1742 | + } | ||
| 1743 | + | ||
| 1744 | + &.danger { | ||
| 1745 | + background: #f56c6c; | ||
| 1746 | + color: white; | ||
| 1747 | + border-color: #f56c6c; | ||
| 1748 | + | ||
| 1749 | + &:hover { | ||
| 1750 | + background: #f78989; | ||
| 1751 | + } | ||
| 1505 | } | 1752 | } |
| 1506 | } | 1753 | } |
| 1507 | } | 1754 | } |
| ... | @@ -1580,61 +1827,113 @@ onMounted(async () => { | ... | @@ -1580,61 +1827,113 @@ onMounted(async () => { |
| 1580 | 1827 | ||
| 1581 | .table-section { | 1828 | .table-section { |
| 1582 | background: white; | 1829 | background: white; |
| 1583 | - border-radius: 8px; | 1830 | + border-radius: 4px; |
| 1584 | - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | 1831 | + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); |
| 1585 | overflow: hidden; | 1832 | overflow: hidden; |
| 1586 | 1833 | ||
| 1587 | .table-header { | 1834 | .table-header { |
| 1588 | display: flex; | 1835 | display: flex; |
| 1589 | - justify-content: space-between; | 1836 | + justify-content: flex-end; |
| 1590 | align-items: center; | 1837 | align-items: center; |
| 1591 | - padding: 20px 20px 0 20px; | 1838 | + padding: 12px 20px; |
| 1592 | - margin-bottom: 16px; | 1839 | + border-bottom: 1px solid #f0f0f0; |
| 1840 | + background: #fafafa; | ||
| 1593 | 1841 | ||
| 1594 | - .table-title { | 1842 | + .table-controls { |
| 1595 | - font-size: 16px; | 1843 | + display: flex; |
| 1596 | - font-weight: 500; | 1844 | + gap: 8px; |
| 1597 | - color: #333; | 1845 | + |
| 1846 | + .control-btn { | ||
| 1847 | + padding: 6px 12px; | ||
| 1848 | + border: 1px solid #ddd; | ||
| 1849 | + border-radius: 4px; | ||
| 1850 | + background: white; | ||
| 1851 | + cursor: pointer; | ||
| 1852 | + font-size: 14px; | ||
| 1853 | + transition: all 0.3s; | ||
| 1854 | + | ||
| 1855 | + &:hover { | ||
| 1856 | + background: #f5f7fa; | ||
| 1857 | + border-color: #409eff; | ||
| 1858 | + } | ||
| 1859 | + } | ||
| 1598 | } | 1860 | } |
| 1861 | + } | ||
| 1599 | 1862 | ||
| 1600 | - .table-actions { | 1863 | + .table-container { |
| 1864 | + position: relative; | ||
| 1865 | + | ||
| 1866 | + .loading-overlay { | ||
| 1867 | + position: absolute; | ||
| 1868 | + top: 0; | ||
| 1869 | + left: 0; | ||
| 1870 | + right: 0; | ||
| 1871 | + bottom: 0; | ||
| 1872 | + background: rgba(255, 255, 255, 0.8); | ||
| 1601 | display: flex; | 1873 | display: flex; |
| 1602 | align-items: center; | 1874 | align-items: center; |
| 1603 | - gap: 12px; | 1875 | + justify-content: center; |
| 1876 | + z-index: 10; | ||
| 1604 | 1877 | ||
| 1605 | - .table-info { | 1878 | + .loading-spinner { |
| 1606 | - font-size: 12px; | 1879 | + font-size: 14px; |
| 1607 | - color: #999; | 1880 | + color: #666; |
| 1608 | } | 1881 | } |
| 1609 | } | 1882 | } |
| 1610 | - } | ||
| 1611 | 1883 | ||
| 1612 | - .el-table { | 1884 | + .data-table { |
| 1613 | - border: none; | 1885 | + width: 100%; |
| 1886 | + border-collapse: collapse; | ||
| 1887 | + font-size: 14px; | ||
| 1614 | 1888 | ||
| 1615 | - :deep(.el-table__header) { | 1889 | + thead { |
| 1616 | - th { | 1890 | + background: #fafafa; |
| 1617 | - background-color: #fafafa; | ||
| 1618 | - border: none; | ||
| 1619 | - font-weight: 500; | ||
| 1620 | - color: #333; | ||
| 1621 | - } | ||
| 1622 | - } | ||
| 1623 | 1891 | ||
| 1624 | - :deep(.el-table__body) { | 1892 | + th { |
| 1625 | - tr:hover > td { | 1893 | + padding: 12px 16px; |
| 1626 | - background-color: #f5f7fa; | 1894 | + text-align: left; |
| 1895 | + font-weight: 500; | ||
| 1896 | + color: #333; | ||
| 1897 | + border-bottom: 1px solid #e0e0e0; | ||
| 1898 | + white-space: nowrap; | ||
| 1899 | + } | ||
| 1627 | } | 1900 | } |
| 1628 | 1901 | ||
| 1629 | - td { | 1902 | + tbody { |
| 1630 | - border: none; | 1903 | + tr { |
| 1631 | - border-bottom: 1px solid #f0f0f0; | 1904 | + transition: background-color 0.3s; |
| 1905 | + | ||
| 1906 | + &:hover { | ||
| 1907 | + background: #f5f7fa; | ||
| 1908 | + } | ||
| 1909 | + | ||
| 1910 | + &:nth-child(even) { | ||
| 1911 | + background: #fafafa; | ||
| 1912 | + } | ||
| 1913 | + | ||
| 1914 | + &:nth-child(even):hover { | ||
| 1915 | + background: #f0f2f5; | ||
| 1916 | + } | ||
| 1917 | + | ||
| 1918 | + td { | ||
| 1919 | + padding: 12px 16px; | ||
| 1920 | + border-bottom: 1px solid #f0f0f0; | ||
| 1921 | + vertical-align: middle; | ||
| 1922 | + | ||
| 1923 | + &.empty-data { | ||
| 1924 | + text-align: center; | ||
| 1925 | + color: #999; | ||
| 1926 | + font-style: italic; | ||
| 1927 | + padding: 40px; | ||
| 1928 | + } | ||
| 1929 | + } | ||
| 1930 | + } | ||
| 1632 | } | 1931 | } |
| 1633 | } | 1932 | } |
| 1634 | } | 1933 | } |
| 1635 | 1934 | ||
| 1636 | .pagination-wrapper { | 1935 | .pagination-wrapper { |
| 1637 | - display: flex; | 1936 | + display: flex; |
| 1638 | justify-content: space-between; | 1937 | justify-content: space-between; |
| 1639 | align-items: center; | 1938 | align-items: center; |
| 1640 | padding: 12px 20px; | 1939 | padding: 12px 20px; |
| ... | @@ -1646,15 +1945,100 @@ onMounted(async () => { | ... | @@ -1646,15 +1945,100 @@ onMounted(async () => { |
| 1646 | color: #666; | 1945 | color: #666; |
| 1647 | } | 1946 | } |
| 1648 | 1947 | ||
| 1948 | + .pagination-controls { | ||
| 1949 | + display: flex; | ||
| 1950 | + gap: 4px; | ||
| 1951 | + align-items: center; | ||
| 1952 | + | ||
| 1953 | + .page-btn { | ||
| 1954 | + padding: 6px 12px; | ||
| 1955 | + border: 1px solid #ddd; | ||
| 1956 | + border-radius: 4px; | ||
| 1957 | + background: white; | ||
| 1958 | + cursor: pointer; | ||
| 1959 | + font-size: 14px; | ||
| 1960 | + transition: all 0.3s; | ||
| 1961 | + min-width: 32px; | ||
| 1962 | + | ||
| 1963 | + &:hover:not(:disabled) { | ||
| 1964 | + background: #f5f7fa; | ||
| 1965 | + border-color: #409eff; | ||
| 1966 | + } | ||
| 1967 | + | ||
| 1968 | + &:disabled { | ||
| 1969 | + opacity: 0.5; | ||
| 1970 | + cursor: not-allowed; | ||
| 1971 | + } | ||
| 1972 | + | ||
| 1973 | + &.active { | ||
| 1974 | + background: #409eff; | ||
| 1975 | + color: white; | ||
| 1976 | + border-color: #409eff; | ||
| 1977 | + } | ||
| 1978 | + } | ||
| 1979 | + } | ||
| 1980 | + | ||
| 1649 | .pagination-jump { | 1981 | .pagination-jump { |
| 1650 | display: flex; | 1982 | display: flex; |
| 1651 | align-items: center; | 1983 | align-items: center; |
| 1984 | + gap: 8px; | ||
| 1652 | font-size: 14px; | 1985 | font-size: 14px; |
| 1653 | color: #666; | 1986 | color: #666; |
| 1987 | + | ||
| 1988 | + .jump-input { | ||
| 1989 | + width: 50px; | ||
| 1990 | + padding: 4px 8px; | ||
| 1991 | + border: 1px solid #ddd; | ||
| 1992 | + border-radius: 4px; | ||
| 1993 | + text-align: center; | ||
| 1994 | + font-size: 14px; | ||
| 1995 | + | ||
| 1996 | + &:focus { | ||
| 1997 | + outline: none; | ||
| 1998 | + border-color: #409eff; | ||
| 1999 | + } | ||
| 2000 | + } | ||
| 1654 | } | 2001 | } |
| 1655 | } | 2002 | } |
| 1656 | } | 2003 | } |
| 1657 | 2004 | ||
| 2005 | +.status-tag { | ||
| 2006 | + padding: 2px 8px; | ||
| 2007 | + border-radius: 12px; | ||
| 2008 | + font-size: 12px; | ||
| 2009 | + font-weight: 500; | ||
| 2010 | + | ||
| 2011 | + &.success { | ||
| 2012 | + background: #f0f9ff; | ||
| 2013 | + color: #67c23a; | ||
| 2014 | + } | ||
| 2015 | + | ||
| 2016 | + &.warning { | ||
| 2017 | + background: #fdf6ec; | ||
| 2018 | + color: #e6a23c; | ||
| 2019 | + } | ||
| 2020 | + | ||
| 2021 | + &.danger { | ||
| 2022 | + background: #fef0f0; | ||
| 2023 | + color: #f56c6c; | ||
| 2024 | + } | ||
| 2025 | +} | ||
| 2026 | + | ||
| 2027 | +.action-link { | ||
| 2028 | + color: #409eff; | ||
| 2029 | + text-decoration: none; | ||
| 2030 | + cursor: pointer; | ||
| 2031 | + font-size: 14px; | ||
| 2032 | + background: none; | ||
| 2033 | + border: none; | ||
| 2034 | + padding: 0; | ||
| 2035 | + | ||
| 2036 | + &:hover { | ||
| 2037 | + color: #66b1ff; | ||
| 2038 | + text-decoration: underline; | ||
| 2039 | + } | ||
| 2040 | +} | ||
| 2041 | + | ||
| 1658 | .amount-text { | 2042 | .amount-text { |
| 1659 | color: #f56c6c; | 2043 | color: #f56c6c; |
| 1660 | font-weight: 500; | 2044 | font-weight: 500; | ... | ... |
-
Please register or login to post a comment