Commit 3fbec03db27b59175a7eddab8045a570cf6bd4cd
1 parent
6713600a
新增财务报表功能、会员生日提醒功能、拓客员工统计功能;优化会员画像对话框和会员列表查询
Showing
19 changed files
with
5914 additions
and
525 deletions
antis-ncc-admin/src/api/extend/financialReport.js
0 → 100644
| 1 | +import request from '@/utils/request' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * 获取门店收款渠道收入统计 | |
| 5 | + */ | |
| 6 | +export function getStorePaymentChannelIncome(data) { | |
| 7 | + return request({ | |
| 8 | + url: '/api/Extend/LqFinancialReport/get-store-payment-channel-income', | |
| 9 | + method: 'post', | |
| 10 | + data | |
| 11 | + }) | |
| 12 | +} | |
| 13 | + | |
| 14 | +/** | |
| 15 | + * 获取门店合作机构应付统计 | |
| 16 | + */ | |
| 17 | +export function getStoreCooperationPayable(data) { | |
| 18 | + return request({ | |
| 19 | + url: '/api/Extend/LqFinancialReport/get-store-cooperation-payable', | |
| 20 | + method: 'post', | |
| 21 | + data | |
| 22 | + }) | |
| 23 | +} | |
| 24 | + | |
| 25 | +/** | |
| 26 | + * 获取门店付款医院应收统计 | |
| 27 | + */ | |
| 28 | +export function getStorePaymentHospitalReceivable(data) { | |
| 29 | + return request({ | |
| 30 | + url: '/api/Extend/LqFinancialReport/get-store-payment-hospital-receivable', | |
| 31 | + method: 'post', | |
| 32 | + data | |
| 33 | + }) | |
| 34 | +} | |
| 35 | + | |
| 36 | +/** | |
| 37 | + * 获取门店总收入统计 | |
| 38 | + */ | |
| 39 | +export function getStoreTotalIncome(data) { | |
| 40 | + return request({ | |
| 41 | + url: '/api/Extend/LqFinancialReport/get-store-total-income', | |
| 42 | + method: 'post', | |
| 43 | + data | |
| 44 | + }) | |
| 45 | +} | ... | ... |
antis-ncc-admin/src/components/member-portrait-dialog.vue
| ... | ... | @@ -729,39 +729,56 @@ export default { |
| 729 | 729 | </script> |
| 730 | 730 | |
| 731 | 731 | <style lang="scss" scoped> |
| 732 | - ::v-deep .el-dialog { | |
| 733 | - border-radius: 14px; | |
| 734 | - } | |
| 732 | +::v-deep .el-dialog { | |
| 733 | + border-radius: 20px; | |
| 734 | + overflow: hidden; | |
| 735 | + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15), 0 8px 24px rgba(0, 0, 0, 0.1); | |
| 736 | +} | |
| 737 | + | |
| 735 | 738 | .member-portrait-dialog { |
| 736 | 739 | .el-dialog { |
| 737 | - border-radius: 16px; | |
| 740 | + border-radius: 20px; | |
| 738 | 741 | overflow: hidden; |
| 742 | + border: 1px solid rgba(255, 255, 255, 0.2); | |
| 739 | 743 | } |
| 740 | 744 | |
| 741 | 745 | .el-dialog__header { |
| 742 | - padding: 20px 24px; | |
| 746 | + padding: 18px 24px; | |
| 743 | 747 | border-bottom: 1px solid #e4e7ed; |
| 744 | - background: #409EFF; | |
| 748 | + background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%); | |
| 745 | 749 | color: #fff; |
| 750 | + position: relative; | |
| 751 | + | |
| 752 | + &::after { | |
| 753 | + content: ''; | |
| 754 | + position: absolute; | |
| 755 | + bottom: 0; | |
| 756 | + left: 0; | |
| 757 | + right: 0; | |
| 758 | + height: 3px; | |
| 759 | + background: linear-gradient(90deg, #409EFF 0%, #66b1ff 100%); | |
| 760 | + } | |
| 746 | 761 | |
| 747 | 762 | .el-dialog__title { |
| 748 | 763 | color: #fff; |
| 749 | 764 | font-size: 18px; |
| 750 | 765 | font-weight: 600; |
| 766 | + letter-spacing: 0; | |
| 751 | 767 | } |
| 752 | 768 | |
| 753 | 769 | .el-dialog__close { |
| 754 | - color: #fff; | |
| 770 | + color: rgba(255, 255, 255, 0.8); | |
| 755 | 771 | font-size: 20px; |
| 772 | + transition: all 0.2s ease; | |
| 756 | 773 | |
| 757 | 774 | &:hover { |
| 758 | - color: rgba(255, 255, 255, 0.8); | |
| 775 | + color: #fff; | |
| 759 | 776 | } |
| 760 | 777 | } |
| 761 | 778 | } |
| 762 | 779 | |
| 763 | 780 | .el-dialog__body { |
| 764 | - padding: 24px; | |
| 781 | + padding: 20px; | |
| 765 | 782 | background: #f5f7fa; |
| 766 | 783 | color: #303133; |
| 767 | 784 | overflow-y: auto; |
| ... | ... | @@ -772,30 +789,42 @@ export default { |
| 772 | 789 | .portrait-wrapper { |
| 773 | 790 | // 顶部会员信息卡片 |
| 774 | 791 | .portrait-header { |
| 775 | - background: #fff; | |
| 776 | - border-radius: 12px; | |
| 777 | - padding: 24px; | |
| 778 | - margin-bottom: 20px; | |
| 792 | + background: #ffffff; | |
| 793 | + border-radius: 8px; | |
| 794 | + padding: 20px; | |
| 795 | + margin-bottom: 16px; | |
| 779 | 796 | display: flex; |
| 780 | 797 | align-items: flex-start; |
| 781 | - gap: 24px; | |
| 782 | - box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); | |
| 798 | + gap: 20px; | |
| 799 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); | |
| 783 | 800 | border: 1px solid #e4e7ed; |
| 801 | + transition: all 0.2s ease; | |
| 802 | + | |
| 803 | + &:hover { | |
| 804 | + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
| 805 | + } | |
| 784 | 806 | |
| 785 | 807 | .header-avatar { |
| 786 | 808 | flex-shrink: 0; |
| 787 | 809 | |
| 788 | 810 | .avatar-circle { |
| 789 | - width: 80px; | |
| 790 | - height: 80px; | |
| 791 | - border-radius: 50%; | |
| 792 | - background: #409EFF; | |
| 811 | + width: 72px; | |
| 812 | + height: 72px; | |
| 813 | + border-radius: 8px; | |
| 814 | + background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%); | |
| 793 | 815 | display: flex; |
| 794 | 816 | align-items: center; |
| 795 | 817 | justify-content: center; |
| 796 | 818 | color: #fff; |
| 797 | - font-size: 36px; | |
| 798 | - box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3); | |
| 819 | + font-size: 32px; | |
| 820 | + box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3); | |
| 821 | + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 822 | + | |
| 823 | + &:hover { | |
| 824 | + transform: scale(1.05) rotate(5deg); | |
| 825 | + box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4); | |
| 826 | + background: linear-gradient(135deg, #66b1ff 0%, #409EFF 100%); | |
| 827 | + } | |
| 799 | 828 | } |
| 800 | 829 | } |
| 801 | 830 | |
| ... | ... | @@ -811,7 +840,7 @@ export default { |
| 811 | 840 | |
| 812 | 841 | .member-name { |
| 813 | 842 | font-size: 24px; |
| 814 | - font-weight: 700; | |
| 843 | + font-weight: 600; | |
| 815 | 844 | color: #303133; |
| 816 | 845 | margin: 0; |
| 817 | 846 | line-height: 1.2; |
| ... | ... | @@ -846,32 +875,33 @@ export default { |
| 846 | 875 | } |
| 847 | 876 | } |
| 848 | 877 | |
| 849 | - .header-right { | |
| 878 | + .header-right { | |
| 850 | 879 | display: flex; |
| 851 | 880 | flex-direction: column; |
| 852 | 881 | align-items: flex-end; |
| 853 | - gap: 16px; | |
| 882 | + gap: 12px; | |
| 854 | 883 | flex-shrink: 0; |
| 855 | 884 | |
| 856 | 885 | .header-stats { |
| 857 | 886 | display: flex; |
| 858 | - gap: 16px; | |
| 887 | + gap: 12px; | |
| 859 | 888 | flex-shrink: 0; |
| 860 | 889 | |
| 861 | 890 | .stat-card { |
| 862 | - min-width: 100px; | |
| 863 | - padding: 6px 12px; | |
| 891 | + min-width: 110px; | |
| 892 | + padding: 12px 16px; | |
| 864 | 893 | border-radius: 6px; |
| 865 | 894 | border: 1px solid #e4e7ed; |
| 866 | 895 | display: flex; |
| 867 | - align-items: center; | |
| 868 | - gap: 8px; | |
| 896 | + flex-direction: column; | |
| 897 | + align-items: flex-start; | |
| 898 | + gap: 6px; | |
| 869 | 899 | transition: all 0.2s ease; |
| 900 | + background: #ffffff; | |
| 870 | 901 | |
| 871 | 902 | &:hover { |
| 872 | - opacity: 0.9; | |
| 873 | - transform: translateY(-1px); | |
| 874 | - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); | |
| 903 | + border-color: #c0c4cc; | |
| 904 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
| 875 | 905 | } |
| 876 | 906 | |
| 877 | 907 | .stat-label-tag { |
| ... | ... | @@ -883,53 +913,36 @@ export default { |
| 883 | 913 | } |
| 884 | 914 | |
| 885 | 915 | .stat-value { |
| 886 | - font-size: 13px; | |
| 916 | + font-size: 16px; | |
| 887 | 917 | font-weight: 600; |
| 888 | 918 | color: #303133; |
| 889 | 919 | white-space: nowrap; |
| 890 | 920 | line-height: 1.2; |
| 891 | - flex: 1; | |
| 892 | - min-width: 0; | |
| 893 | 921 | } |
| 894 | 922 | |
| 895 | 923 | &.stat-primary { |
| 896 | - background: #409EFF; | |
| 897 | - | |
| 898 | - .stat-value { | |
| 899 | - color: #fff; | |
| 900 | - } | |
| 924 | + border-left: 3px solid #409EFF; | |
| 925 | + background: linear-gradient(135deg, rgba(64, 158, 255, 0.08) 0%, rgba(102, 177, 255, 0.05) 100%); | |
| 901 | 926 | } |
| 902 | 927 | |
| 903 | 928 | &.stat-success { |
| 904 | - background: #67C23A; | |
| 905 | - | |
| 906 | - .stat-value { | |
| 907 | - color: #fff; | |
| 908 | - } | |
| 929 | + border-left: 3px solid #67C23A; | |
| 930 | + background: linear-gradient(135deg, rgba(103, 194, 58, 0.08) 0%, rgba(133, 206, 97, 0.05) 100%); | |
| 909 | 931 | } |
| 910 | 932 | |
| 911 | 933 | &.stat-info { |
| 912 | - background: #909399; | |
| 913 | - | |
| 914 | - .stat-value { | |
| 915 | - color: #fff; | |
| 916 | - } | |
| 934 | + border-left: 3px solid #909399; | |
| 935 | + background: linear-gradient(135deg, rgba(144, 147, 153, 0.08) 0%, rgba(169, 172, 178, 0.05) 100%); | |
| 917 | 936 | } |
| 918 | 937 | |
| 919 | 938 | &.stat-warning { |
| 920 | - background: #E6A23C; | |
| 921 | - | |
| 922 | - .stat-value { | |
| 923 | - color: #fff; | |
| 924 | - } | |
| 939 | + border-left: 3px solid #E6A23C; | |
| 940 | + background: linear-gradient(135deg, rgba(230, 162, 60, 0.08) 0%, rgba(240, 180, 90, 0.05) 100%); | |
| 925 | 941 | } |
| 926 | 942 | |
| 927 | 943 | &.stat-default { |
| 928 | - background: #606266; | |
| 929 | - | |
| 930 | - .stat-value { | |
| 931 | - color: #fff; | |
| 932 | - } | |
| 944 | + border-left: 3px solid #409EFF; | |
| 945 | + background: linear-gradient(135deg, rgba(64, 158, 255, 0.08) 0%, rgba(102, 177, 255, 0.05) 100%); | |
| 933 | 946 | } |
| 934 | 947 | } |
| 935 | 948 | } |
| ... | ... | @@ -959,19 +972,19 @@ export default { |
| 959 | 972 | flex-wrap: wrap; |
| 960 | 973 | gap: 12px; |
| 961 | 974 | |
| 962 | - .member-type-badge { | |
| 975 | + .member-type-badge { | |
| 963 | 976 | display: flex; |
| 964 | 977 | align-items: center; |
| 965 | 978 | gap: 8px; |
| 966 | 979 | padding: 6px 12px; |
| 967 | - background: #f8f9fa; | |
| 980 | + background: #f5f7fa; | |
| 968 | 981 | border-radius: 6px; |
| 969 | 982 | border: 1px solid #e4e7ed; |
| 970 | 983 | transition: all 0.2s ease; |
| 971 | 984 | |
| 972 | 985 | &:hover { |
| 973 | 986 | background: #f0f2f5; |
| 974 | - border-color: #409EFF; | |
| 987 | + border-color: #c0c4cc; | |
| 975 | 988 | } |
| 976 | 989 | |
| 977 | 990 | .member-type-tag { |
| ... | ... | @@ -1001,10 +1014,10 @@ export default { |
| 1001 | 1014 | // 选项卡样式 |
| 1002 | 1015 | .portrait-tabs { |
| 1003 | 1016 | ::v-deep .el-tabs__header { |
| 1004 | - margin-bottom: 20px; | |
| 1005 | - background: #fff; | |
| 1017 | + margin-bottom: 16px; | |
| 1018 | + background: #ffffff; | |
| 1006 | 1019 | padding: 0 20px; |
| 1007 | - border-radius: 12px; | |
| 1020 | + border-radius: 8px; | |
| 1008 | 1021 | border: 1px solid #e4e7ed; |
| 1009 | 1022 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); |
| 1010 | 1023 | } |
| ... | ... | @@ -1017,8 +1030,14 @@ export default { |
| 1017 | 1030 | font-size: 14px; |
| 1018 | 1031 | font-weight: 500; |
| 1019 | 1032 | padding: 0 24px; |
| 1020 | - height: 50px; | |
| 1021 | - line-height: 50px; | |
| 1033 | + height: 48px; | |
| 1034 | + line-height: 48px; | |
| 1035 | + color: #606266; | |
| 1036 | + transition: all 0.2s ease; | |
| 1037 | + | |
| 1038 | + &:hover { | |
| 1039 | + color: #409EFF; | |
| 1040 | + } | |
| 1022 | 1041 | |
| 1023 | 1042 | &.is-active { |
| 1024 | 1043 | color: #409EFF; |
| ... | ... | @@ -1027,7 +1046,7 @@ export default { |
| 1027 | 1046 | } |
| 1028 | 1047 | |
| 1029 | 1048 | ::v-deep .el-tabs__active-bar { |
| 1030 | - background-color: #409EFF; | |
| 1049 | + background: linear-gradient(90deg, #409EFF 0%, #66b1ff 100%); | |
| 1031 | 1050 | height: 3px; |
| 1032 | 1051 | } |
| 1033 | 1052 | |
| ... | ... | @@ -1035,15 +1054,20 @@ export default { |
| 1035 | 1054 | height: 40vh; |
| 1036 | 1055 | overflow-y: scroll; |
| 1037 | 1056 | .content-card { |
| 1038 | - background: #fff; | |
| 1039 | - border-radius: 12px; | |
| 1057 | + background: #ffffff; | |
| 1058 | + border-radius: 8px; | |
| 1040 | 1059 | border: 1px solid #e4e7ed; |
| 1041 | 1060 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); |
| 1042 | - margin-bottom: 20px; | |
| 1061 | + margin-bottom: 16px; | |
| 1043 | 1062 | overflow: hidden; |
| 1063 | + transition: all 0.2s ease; | |
| 1064 | + | |
| 1065 | + &:hover { | |
| 1066 | + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
| 1067 | + } | |
| 1044 | 1068 | |
| 1045 | 1069 | .card-header { |
| 1046 | - padding: 18px 20px; | |
| 1070 | + padding: 14px 18px; | |
| 1047 | 1071 | background: #f8f9fa; |
| 1048 | 1072 | border-bottom: 1px solid #e4e7ed; |
| 1049 | 1073 | display: flex; |
| ... | ... | @@ -1051,8 +1075,14 @@ export default { |
| 1051 | 1075 | gap: 8px; |
| 1052 | 1076 | |
| 1053 | 1077 | i { |
| 1054 | - font-size: 18px; | |
| 1078 | + font-size: 16px; | |
| 1055 | 1079 | color: #409EFF; |
| 1080 | + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 1081 | + } | |
| 1082 | + | |
| 1083 | + &:hover i { | |
| 1084 | + transform: scale(1.1) rotate(5deg); | |
| 1085 | + color: #66b1ff; | |
| 1056 | 1086 | } |
| 1057 | 1087 | |
| 1058 | 1088 | .card-title { |
| ... | ... | @@ -1063,7 +1093,7 @@ export default { |
| 1063 | 1093 | } |
| 1064 | 1094 | |
| 1065 | 1095 | .card-body { |
| 1066 | - padding: 20px; | |
| 1096 | + padding: 18px; | |
| 1067 | 1097 | } |
| 1068 | 1098 | } |
| 1069 | 1099 | } |
| ... | ... | @@ -1073,35 +1103,43 @@ export default { |
| 1073 | 1103 | .behavior-grid { |
| 1074 | 1104 | display: grid; |
| 1075 | 1105 | grid-template-columns: repeat(3, 1fr); |
| 1076 | - gap: 16px; | |
| 1106 | + gap: 14px; | |
| 1077 | 1107 | |
| 1078 | 1108 | .behavior-item { |
| 1079 | 1109 | background: #f8f9fa; |
| 1080 | - border-radius: 10px; | |
| 1110 | + border-radius: 8px; | |
| 1081 | 1111 | border: 1px solid #e4e7ed; |
| 1082 | - padding: 16px; | |
| 1112 | + padding: 14px; | |
| 1083 | 1113 | display: flex; |
| 1084 | 1114 | align-items: center; |
| 1085 | 1115 | gap: 12px; |
| 1086 | - transition: all 0.3s ease; | |
| 1116 | + transition: all 0.2s ease; | |
| 1087 | 1117 | |
| 1088 | 1118 | &:hover { |
| 1089 | - background: #fff; | |
| 1090 | - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); | |
| 1091 | - transform: translateY(-2px); | |
| 1119 | + background: #ffffff; | |
| 1120 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); | |
| 1121 | + border-color: #c0c4cc; | |
| 1092 | 1122 | } |
| 1093 | 1123 | |
| 1094 | 1124 | .behavior-icon { |
| 1095 | 1125 | width: 40px; |
| 1096 | 1126 | height: 40px; |
| 1097 | - border-radius: 8px; | |
| 1098 | - background: #409EFF; | |
| 1127 | + border-radius: 6px; | |
| 1128 | + background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%); | |
| 1099 | 1129 | display: flex; |
| 1100 | 1130 | align-items: center; |
| 1101 | 1131 | justify-content: center; |
| 1102 | 1132 | color: #fff; |
| 1103 | - font-size: 18px; | |
| 1133 | + font-size: 16px; | |
| 1104 | 1134 | flex-shrink: 0; |
| 1135 | + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 1136 | + box-shadow: 0 2px 6px rgba(64, 158, 255, 0.3); | |
| 1137 | + } | |
| 1138 | + | |
| 1139 | + &:hover .behavior-icon { | |
| 1140 | + transform: scale(1.1) rotate(5deg); | |
| 1141 | + background: linear-gradient(135deg, #66b1ff 0%, #409EFF 100%); | |
| 1142 | + box-shadow: 0 4px 10px rgba(64, 158, 255, 0.4); | |
| 1105 | 1143 | } |
| 1106 | 1144 | |
| 1107 | 1145 | .behavior-content { |
| ... | ... | @@ -1109,13 +1147,14 @@ export default { |
| 1109 | 1147 | min-width: 0; |
| 1110 | 1148 | |
| 1111 | 1149 | .behavior-label { |
| 1112 | - font-size: 12px; | |
| 1150 | + font-size: 13px; | |
| 1113 | 1151 | color: #909399; |
| 1114 | - margin-bottom: 6px; | |
| 1152 | + margin-bottom: 8px; | |
| 1153 | + font-weight: 500; | |
| 1115 | 1154 | } |
| 1116 | 1155 | |
| 1117 | 1156 | .behavior-value { |
| 1118 | - font-size: 15px; | |
| 1157 | + font-size: 16px; | |
| 1119 | 1158 | color: #303133; |
| 1120 | 1159 | font-weight: 600; |
| 1121 | 1160 | white-space: nowrap; |
| ... | ... | @@ -1129,38 +1168,46 @@ export default { |
| 1129 | 1168 | // 消费分析布局 |
| 1130 | 1169 | .analysis-layout { |
| 1131 | 1170 | display: flex; |
| 1132 | - gap: 20px; | |
| 1171 | + gap: 16px; | |
| 1133 | 1172 | flex-wrap: wrap; |
| 1134 | 1173 | |
| 1135 | 1174 | .analysis-item { |
| 1136 | 1175 | flex: 1; |
| 1137 | 1176 | min-width: 200px; |
| 1138 | 1177 | background: #f8f9fa; |
| 1139 | - border-radius: 10px; | |
| 1178 | + border-radius: 8px; | |
| 1140 | 1179 | border: 1px solid #e4e7ed; |
| 1141 | - padding: 16px; | |
| 1180 | + padding: 14px; | |
| 1142 | 1181 | display: flex; |
| 1143 | 1182 | align-items: center; |
| 1144 | 1183 | gap: 12px; |
| 1145 | - transition: all 0.3s ease; | |
| 1184 | + transition: all 0.2s ease; | |
| 1146 | 1185 | |
| 1147 | 1186 | &:hover { |
| 1148 | - background: #fff; | |
| 1149 | - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); | |
| 1150 | - transform: translateY(-2px); | |
| 1187 | + background: #ffffff; | |
| 1188 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); | |
| 1189 | + border-color: #c0c4cc; | |
| 1151 | 1190 | } |
| 1152 | 1191 | |
| 1153 | 1192 | .analysis-icon { |
| 1154 | 1193 | width: 40px; |
| 1155 | 1194 | height: 40px; |
| 1156 | - border-radius: 8px; | |
| 1157 | - background: #67C23A; | |
| 1195 | + border-radius: 6px; | |
| 1196 | + background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%); | |
| 1158 | 1197 | display: flex; |
| 1159 | 1198 | align-items: center; |
| 1160 | 1199 | justify-content: center; |
| 1161 | 1200 | color: #fff; |
| 1162 | - font-size: 18px; | |
| 1201 | + font-size: 16px; | |
| 1163 | 1202 | flex-shrink: 0; |
| 1203 | + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 1204 | + box-shadow: 0 2px 6px rgba(103, 194, 58, 0.3); | |
| 1205 | + } | |
| 1206 | + | |
| 1207 | + &:hover .analysis-icon { | |
| 1208 | + transform: scale(1.1) rotate(5deg); | |
| 1209 | + background: linear-gradient(135deg, #85ce61 0%, #67C23A 100%); | |
| 1210 | + box-shadow: 0 4px 10px rgba(103, 194, 58, 0.4); | |
| 1164 | 1211 | } |
| 1165 | 1212 | |
| 1166 | 1213 | .analysis-content { |
| ... | ... | @@ -1168,13 +1215,14 @@ export default { |
| 1168 | 1215 | min-width: 0; |
| 1169 | 1216 | |
| 1170 | 1217 | .analysis-label { |
| 1171 | - font-size: 12px; | |
| 1218 | + font-size: 13px; | |
| 1172 | 1219 | color: #909399; |
| 1173 | - margin-bottom: 6px; | |
| 1220 | + margin-bottom: 8px; | |
| 1221 | + font-weight: 500; | |
| 1174 | 1222 | } |
| 1175 | 1223 | |
| 1176 | 1224 | .analysis-value { |
| 1177 | - font-size: 15px; | |
| 1225 | + font-size: 16px; | |
| 1178 | 1226 | color: #303133; |
| 1179 | 1227 | font-weight: 600; |
| 1180 | 1228 | } |
| ... | ... | @@ -1185,18 +1233,82 @@ export default { |
| 1185 | 1233 | // 趋势图 |
| 1186 | 1234 | .trend-chart { |
| 1187 | 1235 | width: 100%; |
| 1188 | - height: 400px; | |
| 1236 | + height: 360px; | |
| 1237 | + border-radius: 6px; | |
| 1238 | + background: #fafbfc; | |
| 1239 | + padding: 8px; | |
| 1189 | 1240 | } |
| 1190 | 1241 | |
| 1191 | 1242 | // 分页 |
| 1192 | 1243 | .pagination-bar { |
| 1193 | 1244 | display: flex; |
| 1194 | 1245 | justify-content: flex-end; |
| 1195 | - padding: 16px 0 0 0; | |
| 1196 | - margin-top: 16px; | |
| 1246 | + padding: 14px 0 0 0; | |
| 1247 | + margin-top: 14px; | |
| 1197 | 1248 | border-top: 1px solid #e4e7ed; |
| 1198 | 1249 | } |
| 1199 | 1250 | |
| 1251 | + // 表格样式优化 | |
| 1252 | + ::v-deep .el-table { | |
| 1253 | + border-radius: 6px; | |
| 1254 | + overflow: hidden; | |
| 1255 | + | |
| 1256 | + .el-table__header { | |
| 1257 | + background: linear-gradient(135deg, rgba(64, 158, 255, 0.08) 0%, rgba(102, 177, 255, 0.05) 100%); | |
| 1258 | + | |
| 1259 | + th { | |
| 1260 | + background: transparent !important; | |
| 1261 | + border-bottom: 1px solid rgba(64, 158, 255, 0.2); | |
| 1262 | + color: #303133; | |
| 1263 | + font-weight: 600; | |
| 1264 | + font-size: 13px; | |
| 1265 | + padding: 12px 0; | |
| 1266 | + } | |
| 1267 | + } | |
| 1268 | + | |
| 1269 | + .el-table__body { | |
| 1270 | + tr { | |
| 1271 | + transition: all 0.2s ease; | |
| 1272 | + | |
| 1273 | + &:hover { | |
| 1274 | + background: linear-gradient(90deg, rgba(64, 158, 255, 0.06) 0%, rgba(102, 177, 255, 0.04) 100%) !important; | |
| 1275 | + } | |
| 1276 | + | |
| 1277 | + td { | |
| 1278 | + border-bottom: 1px solid #f0f2f5; | |
| 1279 | + padding: 12px 0; | |
| 1280 | + color: #606266; | |
| 1281 | + font-size: 13px; | |
| 1282 | + } | |
| 1283 | + } | |
| 1284 | + | |
| 1285 | + tr.el-table__row--striped { | |
| 1286 | + background: #fafbfc; | |
| 1287 | + } | |
| 1288 | + } | |
| 1289 | + } | |
| 1290 | + | |
| 1291 | + // 滚动条美化 | |
| 1292 | + ::-webkit-scrollbar { | |
| 1293 | + width: 8px; | |
| 1294 | + height: 8px; | |
| 1295 | + } | |
| 1296 | + | |
| 1297 | + ::-webkit-scrollbar-track { | |
| 1298 | + background: rgba(240, 242, 245, 0.5); | |
| 1299 | + border-radius: 4px; | |
| 1300 | + } | |
| 1301 | + | |
| 1302 | + ::-webkit-scrollbar-thumb { | |
| 1303 | + background: linear-gradient(135deg, #c0c4cc 0%, #909399 100%); | |
| 1304 | + border-radius: 4px; | |
| 1305 | + transition: background 0.3s; | |
| 1306 | + | |
| 1307 | + &:hover { | |
| 1308 | + background: linear-gradient(135deg, #909399 0%, #606266 100%); | |
| 1309 | + } | |
| 1310 | + } | |
| 1311 | + | |
| 1200 | 1312 | .text-muted { |
| 1201 | 1313 | color: #909399; |
| 1202 | 1314 | font-size: 13px; | ... | ... |
antis-ncc-admin/src/views/extend/financialReport/index.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <div class="financial-report-container"> | |
| 3 | + <!-- 筛选区域 --> | |
| 4 | + <el-card class="search-card" shadow="never"> | |
| 5 | + <el-form :inline="true" :model="queryParams" size="small" class="search-form"> | |
| 6 | + <el-form-item label="时间范围"> | |
| 7 | + <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" | |
| 8 | + end-placeholder="结束日期" format="yyyy-MM-dd" value-format="yyyy-MM-dd" style="width: 240px" | |
| 9 | + @change="handleDateRangeChange" /> | |
| 10 | + </el-form-item> | |
| 11 | + <el-form-item label="统计周期"> | |
| 12 | + <el-radio-group v-model="queryParams.periodType" size="small"> | |
| 13 | + <el-radio-button label="day">按日</el-radio-button> | |
| 14 | + <el-radio-button label="month">按月</el-radio-button> | |
| 15 | + </el-radio-group> | |
| 16 | + </el-form-item> | |
| 17 | + <el-form-item label="门店"> | |
| 18 | + <el-select v-model="queryParams.storeIds" multiple placeholder="请选择门店(可多选)" clearable filterable | |
| 19 | + style="width: 300px" :loading="loading"> | |
| 20 | + <el-option v-for="store in storeOptions" :key="store.id || store.F_Id" | |
| 21 | + :label="store.fullName || store.dm || store.name" :value="store.id || store.F_Id" /> | |
| 22 | + </el-select> | |
| 23 | + </el-form-item> | |
| 24 | + <el-form-item> | |
| 25 | + <el-button type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button> | |
| 26 | + <el-button icon="el-icon-refresh-right" @click="handleReset">重置</el-button> | |
| 27 | + </el-form-item> | |
| 28 | + </el-form> | |
| 29 | + </el-card> | |
| 30 | + | |
| 31 | + <!-- 统计卡片区域 --> | |
| 32 | + <el-row :gutter="20" class="stat-cards-section"> | |
| 33 | + <el-col :xs="24" :sm="12" :md="6"> | |
| 34 | + <div class="stat-card income-card"> | |
| 35 | + <div class="stat-icon"> | |
| 36 | + <i class="el-icon-coin"></i> | |
| 37 | + </div> | |
| 38 | + <div class="stat-content"> | |
| 39 | + <div class="stat-label">总收入</div> | |
| 40 | + <div class="stat-value">{{ formatCurrency(totalIncome) }}</div> | |
| 41 | + <div class="stat-meta"> | |
| 42 | + <span>{{ totalBillingCount }} 笔订单</span> | |
| 43 | + </div> | |
| 44 | + </div> | |
| 45 | + </div> | |
| 46 | + </el-col> | |
| 47 | + <el-col :xs="24" :sm="12" :md="6"> | |
| 48 | + <div class="stat-card channel-card"> | |
| 49 | + <div class="stat-icon"> | |
| 50 | + <i class="el-icon-credit-card"></i> | |
| 51 | + </div> | |
| 52 | + <div class="stat-content"> | |
| 53 | + <div class="stat-label">收款渠道收入</div> | |
| 54 | + <div class="stat-value">{{ formatCurrency(channelIncome) }}</div> | |
| 55 | + <div class="stat-meta"> | |
| 56 | + <span>{{ channelCount }} 个渠道</span> | |
| 57 | + </div> | |
| 58 | + </div> | |
| 59 | + </div> | |
| 60 | + </el-col> | |
| 61 | + <el-col :xs="24" :sm="12" :md="6"> | |
| 62 | + <div class="stat-card payable-card"> | |
| 63 | + <div class="stat-icon"> | |
| 64 | + <i class="el-icon-shopping-cart-2"></i> | |
| 65 | + </div> | |
| 66 | + <div class="stat-content"> | |
| 67 | + <div class="stat-label">合作机构应付</div> | |
| 68 | + <div class="stat-value">{{ formatCurrency(cooperationPayable) }}</div> | |
| 69 | + <div class="stat-meta"> | |
| 70 | + <span>{{ cooperationCount }} 个机构</span> | |
| 71 | + </div> | |
| 72 | + </div> | |
| 73 | + </div> | |
| 74 | + </el-col> | |
| 75 | + <el-col :xs="24" :sm="12" :md="6"> | |
| 76 | + <div class="stat-card receivable-card"> | |
| 77 | + <div class="stat-icon"> | |
| 78 | + <i class="el-icon-bank-card"></i> | |
| 79 | + </div> | |
| 80 | + <div class="stat-content"> | |
| 81 | + <div class="stat-label">付款医院应收</div> | |
| 82 | + <div class="stat-value">{{ formatCurrency(hospitalReceivable) }}</div> | |
| 83 | + <div class="stat-meta"> | |
| 84 | + <span>{{ hospitalCount }} 个医院</span> | |
| 85 | + </div> | |
| 86 | + </div> | |
| 87 | + </div> | |
| 88 | + </el-col> | |
| 89 | + </el-row> | |
| 90 | + | |
| 91 | + <!-- 图表区域 --> | |
| 92 | + <el-row :gutter="20" class="charts-section"> | |
| 93 | + <!-- 总收入趋势图 --> | |
| 94 | + <el-col :xs="24" :lg="16"> | |
| 95 | + <el-card class="chart-card" shadow="hover"> | |
| 96 | + <div slot="header" class="chart-header"> | |
| 97 | + <span class="chart-title"> | |
| 98 | + <i class="el-icon-data-line"></i> | |
| 99 | + 门店总收入趋势 | |
| 100 | + </span> | |
| 101 | + </div> | |
| 102 | + <div ref="totalIncomeChart" class="chart-container"></div> | |
| 103 | + </el-card> | |
| 104 | + </el-col> | |
| 105 | + <!-- 收款渠道分布饼图 --> | |
| 106 | + <el-col :xs="24" :lg="8"> | |
| 107 | + <el-card class="chart-card" shadow="hover"> | |
| 108 | + <div slot="header" class="chart-header"> | |
| 109 | + <span class="chart-title"> | |
| 110 | + <i class="el-icon-pie-chart"></i> | |
| 111 | + 收款渠道分布 | |
| 112 | + </span> | |
| 113 | + </div> | |
| 114 | + <div ref="channelPieChart" class="chart-container"></div> | |
| 115 | + </el-card> | |
| 116 | + </el-col> | |
| 117 | + </el-row> | |
| 118 | + | |
| 119 | + <el-row :gutter="20" class="charts-section"> | |
| 120 | + <!-- 合作机构应付趋势 --> | |
| 121 | + <el-col :xs="24" :lg="12"> | |
| 122 | + <el-card class="chart-card" shadow="hover"> | |
| 123 | + <div slot="header" class="chart-header"> | |
| 124 | + <span class="chart-title"> | |
| 125 | + <i class="el-icon-shopping-bag-1"></i> | |
| 126 | + 合作机构应付趋势 | |
| 127 | + </span> | |
| 128 | + </div> | |
| 129 | + <div ref="payableChart" class="chart-container"></div> | |
| 130 | + </el-card> | |
| 131 | + </el-col> | |
| 132 | + <!-- 付款医院应收趋势 --> | |
| 133 | + <el-col :xs="24" :lg="12"> | |
| 134 | + <el-card class="chart-card" shadow="hover"> | |
| 135 | + <div slot="header" class="chart-header"> | |
| 136 | + <span class="chart-title"> | |
| 137 | + <i class="el-icon-wallet"></i> | |
| 138 | + 付款医院应收趋势 | |
| 139 | + </span> | |
| 140 | + </div> | |
| 141 | + <div ref="receivableChart" class="chart-container"></div> | |
| 142 | + </el-card> | |
| 143 | + </el-col> | |
| 144 | + </el-row> | |
| 145 | + | |
| 146 | + <!-- 数据表格区域 --> | |
| 147 | + <el-card class="table-card" shadow="hover"> | |
| 148 | + <div slot="header" class="table-header"> | |
| 149 | + <span class="table-title"> | |
| 150 | + <i class="el-icon-s-grid"></i> | |
| 151 | + 详细数据 | |
| 152 | + </span> | |
| 153 | + <el-tabs v-model="activeTab" @tab-click="handleTabClick" class="table-tabs"> | |
| 154 | + <el-tab-pane label="总收入" name="totalIncome"></el-tab-pane> | |
| 155 | + <el-tab-pane label="收款渠道" name="channel"></el-tab-pane> | |
| 156 | + <el-tab-pane label="合作机构应付" name="payable"></el-tab-pane> | |
| 157 | + <el-tab-pane label="付款医院应收" name="receivable"></el-tab-pane> | |
| 158 | + </el-tabs> | |
| 159 | + </div> | |
| 160 | + <el-table :data="tableData" v-loading="tableLoading" stripe border height="400" class="data-table"> | |
| 161 | + <el-table-column v-for="column in tableColumns" :key="column.prop" :prop="column.prop" | |
| 162 | + :label="column.label" :width="column.width" :formatter="column.formatter" | |
| 163 | + :sortable="column.sortable" /> | |
| 164 | + </el-table> | |
| 165 | + </el-card> | |
| 166 | + </div> | |
| 167 | +</template> | |
| 168 | + | |
| 169 | +<script> | |
| 170 | +import * as echarts from 'echarts' | |
| 171 | +import { | |
| 172 | + getStorePaymentChannelIncome, | |
| 173 | + getStoreCooperationPayable, | |
| 174 | + getStorePaymentHospitalReceivable, | |
| 175 | + getStoreTotalIncome | |
| 176 | +} from '@/api/extend/financialReport' | |
| 177 | +import { getStoreSelector } from '@/api/extend/store' | |
| 178 | + | |
| 179 | +export default { | |
| 180 | + name: 'FinancialReport', | |
| 181 | + data() { | |
| 182 | + return { | |
| 183 | + // 查询参数 | |
| 184 | + queryParams: { | |
| 185 | + startTime: null, | |
| 186 | + endTime: null, | |
| 187 | + periodType: 'day', | |
| 188 | + storeIds: [] | |
| 189 | + }, | |
| 190 | + dateRange: [], | |
| 191 | + // 门店选项 | |
| 192 | + storeOptions: [], | |
| 193 | + // 数据加载状态 | |
| 194 | + loading: false, | |
| 195 | + tableLoading: false, | |
| 196 | + // 统计数据 | |
| 197 | + totalIncome: 0, | |
| 198 | + totalBillingCount: 0, | |
| 199 | + channelIncome: 0, | |
| 200 | + channelCount: 0, | |
| 201 | + cooperationPayable: 0, | |
| 202 | + cooperationCount: 0, | |
| 203 | + hospitalReceivable: 0, | |
| 204 | + hospitalCount: 0, | |
| 205 | + // 原始数据 | |
| 206 | + totalIncomeData: [], | |
| 207 | + channelData: [], | |
| 208 | + payableData: [], | |
| 209 | + receivableData: [], | |
| 210 | + // 图表实例 | |
| 211 | + totalIncomeChart: null, | |
| 212 | + channelPieChart: null, | |
| 213 | + payableChart: null, | |
| 214 | + receivableChart: null, | |
| 215 | + // 表格数据 | |
| 216 | + activeTab: 'totalIncome', | |
| 217 | + tableData: [], | |
| 218 | + tableColumns: [] | |
| 219 | + } | |
| 220 | + }, | |
| 221 | + async mounted() { | |
| 222 | + // 初始化默认时间范围 | |
| 223 | + this.initDefaultDateRange() | |
| 224 | + // 加载门店选项 | |
| 225 | + await this.loadStoreOptions() | |
| 226 | + // 初始化图表 | |
| 227 | + this.$nextTick(() => { | |
| 228 | + this.initCharts() | |
| 229 | + // 自动加载数据(延迟一下确保图表容器已渲染) | |
| 230 | + setTimeout(() => { | |
| 231 | + this.handleSearch() | |
| 232 | + }, 300) | |
| 233 | + }) | |
| 234 | + }, | |
| 235 | + beforeDestroy() { | |
| 236 | + this.disposeCharts() | |
| 237 | + }, | |
| 238 | + methods: { | |
| 239 | + // 初始化默认时间范围(最近30天) | |
| 240 | + initDefaultDateRange() { | |
| 241 | + const end = new Date() | |
| 242 | + const start = new Date() | |
| 243 | + start.setDate(start.getDate() - 30) | |
| 244 | + this.dateRange = [ | |
| 245 | + this.formatDate(start), | |
| 246 | + this.formatDate(end) | |
| 247 | + ] | |
| 248 | + this.handleDateRangeChange(this.dateRange) | |
| 249 | + }, | |
| 250 | + // 格式化日期 | |
| 251 | + formatDate(date) { | |
| 252 | + const year = date.getFullYear() | |
| 253 | + const month = String(date.getMonth() + 1).padStart(2, '0') | |
| 254 | + const day = String(date.getDate()).padStart(2, '0') | |
| 255 | + return `${year}-${month}-${day}` | |
| 256 | + }, | |
| 257 | + // 日期范围变化 | |
| 258 | + handleDateRangeChange(val) { | |
| 259 | + if (val && val.length === 2) { | |
| 260 | + this.queryParams.startTime = val[0] + 'T00:00:00' | |
| 261 | + this.queryParams.endTime = val[1] + 'T23:59:59' | |
| 262 | + } else { | |
| 263 | + this.queryParams.startTime = null | |
| 264 | + this.queryParams.endTime = null | |
| 265 | + } | |
| 266 | + }, | |
| 267 | + // 加载门店选项 | |
| 268 | + async loadStoreOptions() { | |
| 269 | + try { | |
| 270 | + const res = await getStoreSelector() | |
| 271 | + console.log('门店接口返回:', res) | |
| 272 | + if (res.code === 200) { | |
| 273 | + // 门店接口返回格式:{ list: [...] } | |
| 274 | + const data = (res.data && res.data.list) ? res.data.list : (res.data || []) | |
| 275 | + this.storeOptions = Array.isArray(data) ? data : [] | |
| 276 | + console.log('门店选项数量:', this.storeOptions.length) | |
| 277 | + if (this.storeOptions.length === 0) { | |
| 278 | + this.$message.warning('暂无门店数据') | |
| 279 | + } | |
| 280 | + } else { | |
| 281 | + this.$message.warning('加载门店列表失败: ' + (res.msg || '未知错误')) | |
| 282 | + } | |
| 283 | + } catch (error) { | |
| 284 | + console.error('加载门店选项失败:', error) | |
| 285 | + this.$message.error('加载门店列表失败: ' + (error.message || '未知错误')) | |
| 286 | + } | |
| 287 | + }, | |
| 288 | + // 查询 | |
| 289 | + async handleSearch() { | |
| 290 | + if (!this.queryParams.startTime || !this.queryParams.endTime) { | |
| 291 | + this.$message.warning('请选择时间范围') | |
| 292 | + return | |
| 293 | + } | |
| 294 | + this.loading = true | |
| 295 | + this.tableLoading = true | |
| 296 | + try { | |
| 297 | + await Promise.all([ | |
| 298 | + this.loadTotalIncomeData(), | |
| 299 | + this.loadChannelData(), | |
| 300 | + this.loadPayableData(), | |
| 301 | + this.loadReceivableData() | |
| 302 | + ]) | |
| 303 | + this.$nextTick(() => { | |
| 304 | + this.updateCharts() | |
| 305 | + this.updateTable() | |
| 306 | + }) | |
| 307 | + } catch (error) { | |
| 308 | + console.error('加载数据失败:', error) | |
| 309 | + this.$message.error('加载数据失败: ' + (error.message || '未知错误')) | |
| 310 | + } finally { | |
| 311 | + this.loading = false | |
| 312 | + this.tableLoading = false | |
| 313 | + } | |
| 314 | + }, | |
| 315 | + // 重置 | |
| 316 | + handleReset() { | |
| 317 | + this.initDefaultDateRange() | |
| 318 | + this.queryParams.periodType = 'day' | |
| 319 | + this.queryParams.storeIds = [] | |
| 320 | + this.$nextTick(() => { | |
| 321 | + this.handleSearch() | |
| 322 | + }) | |
| 323 | + }, | |
| 324 | + // 加载总收入数据 | |
| 325 | + async loadTotalIncomeData() { | |
| 326 | + try { | |
| 327 | + const res = await getStoreTotalIncome(this.queryParams) | |
| 328 | + console.log('总收入接口返回:', res) | |
| 329 | + if (res.code === 200) { | |
| 330 | + // 后端接口返回的是 List,直接就是数组 | |
| 331 | + const data = Array.isArray(res.data) ? res.data : [] | |
| 332 | + console.log('总收入数据:', data) | |
| 333 | + this.totalIncome = data.reduce((sum, item) => sum + (item.totalIncome || 0), 0) | |
| 334 | + this.totalBillingCount = data.reduce((sum, item) => sum + (item.billingCount || 0), 0) | |
| 335 | + this.totalIncomeData = data | |
| 336 | + console.log('总收入汇总:', this.totalIncome, '笔数:', this.totalBillingCount) | |
| 337 | + } else { | |
| 338 | + console.error('总收入接口返回错误:', res.msg) | |
| 339 | + this.totalIncomeData = [] | |
| 340 | + } | |
| 341 | + } catch (error) { | |
| 342 | + console.error('加载总收入数据失败:', error) | |
| 343 | + this.totalIncomeData = [] | |
| 344 | + } | |
| 345 | + }, | |
| 346 | + // 加载收款渠道数据 | |
| 347 | + async loadChannelData() { | |
| 348 | + try { | |
| 349 | + const res = await getStorePaymentChannelIncome(this.queryParams) | |
| 350 | + console.log('收款渠道接口返回:', res) | |
| 351 | + if (res.code === 200) { | |
| 352 | + const data = Array.isArray(res.data) ? res.data : [] | |
| 353 | + console.log('收款渠道数据:', data) | |
| 354 | + let totalChannelIncome = 0 | |
| 355 | + const channelSet = new Set() | |
| 356 | + data.forEach(store => { | |
| 357 | + totalChannelIncome += store.totalIncome || 0 | |
| 358 | + if (store.paymentChannels && Array.isArray(store.paymentChannels)) { | |
| 359 | + store.paymentChannels.forEach(ch => { | |
| 360 | + channelSet.add(ch.paymentMethod) | |
| 361 | + }) | |
| 362 | + } | |
| 363 | + }) | |
| 364 | + this.channelIncome = totalChannelIncome | |
| 365 | + this.channelCount = channelSet.size | |
| 366 | + this.channelData = data | |
| 367 | + console.log('收款渠道汇总:', this.channelIncome, '渠道数:', this.channelCount) | |
| 368 | + } else { | |
| 369 | + console.error('收款渠道接口返回错误:', res.msg) | |
| 370 | + this.channelData = [] | |
| 371 | + } | |
| 372 | + } catch (error) { | |
| 373 | + console.error('加载收款渠道数据失败:', error) | |
| 374 | + this.channelData = [] | |
| 375 | + } | |
| 376 | + }, | |
| 377 | + // 加载合作机构应付数据 | |
| 378 | + async loadPayableData() { | |
| 379 | + try { | |
| 380 | + const res = await getStoreCooperationPayable(this.queryParams) | |
| 381 | + console.log('合作机构应付接口返回:', res) | |
| 382 | + if (res.code === 200) { | |
| 383 | + const data = Array.isArray(res.data) ? res.data : [] | |
| 384 | + console.log('合作机构应付数据:', data) | |
| 385 | + let totalPayable = 0 | |
| 386 | + const cooperationSet = new Set() | |
| 387 | + data.forEach(store => { | |
| 388 | + totalPayable += store.totalPayable || 0 | |
| 389 | + if (store.cooperationItems && Array.isArray(store.cooperationItems)) { | |
| 390 | + store.cooperationItems.forEach(item => { | |
| 391 | + cooperationSet.add(item.cooperationId) | |
| 392 | + }) | |
| 393 | + } | |
| 394 | + }) | |
| 395 | + this.cooperationPayable = totalPayable | |
| 396 | + this.cooperationCount = cooperationSet.size | |
| 397 | + this.payableData = data | |
| 398 | + console.log('合作机构应付汇总:', this.cooperationPayable, '机构数:', this.cooperationCount) | |
| 399 | + } else { | |
| 400 | + console.error('合作机构应付接口返回错误:', res.msg) | |
| 401 | + this.payableData = [] | |
| 402 | + } | |
| 403 | + } catch (error) { | |
| 404 | + console.error('加载合作机构应付数据失败:', error) | |
| 405 | + this.payableData = [] | |
| 406 | + } | |
| 407 | + }, | |
| 408 | + // 加载付款医院应收数据 | |
| 409 | + async loadReceivableData() { | |
| 410 | + try { | |
| 411 | + const res = await getStorePaymentHospitalReceivable(this.queryParams) | |
| 412 | + console.log('付款医院应收接口返回:', res) | |
| 413 | + if (res.code === 200) { | |
| 414 | + const data = Array.isArray(res.data) ? res.data : [] | |
| 415 | + console.log('付款医院应收数据:', data) | |
| 416 | + let totalReceivable = 0 | |
| 417 | + const hospitalSet = new Set() | |
| 418 | + data.forEach(store => { | |
| 419 | + totalReceivable += store.totalReceivable || 0 | |
| 420 | + if (store.hospitalItems && Array.isArray(store.hospitalItems)) { | |
| 421 | + store.hospitalItems.forEach(item => { | |
| 422 | + hospitalSet.add(item.hospitalId) | |
| 423 | + }) | |
| 424 | + } | |
| 425 | + }) | |
| 426 | + this.hospitalReceivable = totalReceivable | |
| 427 | + this.hospitalCount = hospitalSet.size | |
| 428 | + this.receivableData = data | |
| 429 | + console.log('付款医院应收汇总:', this.hospitalReceivable, '医院数:', this.hospitalCount) | |
| 430 | + } else { | |
| 431 | + console.error('付款医院应收接口返回错误:', res.msg) | |
| 432 | + this.receivableData = [] | |
| 433 | + } | |
| 434 | + } catch (error) { | |
| 435 | + console.error('加载付款医院应收数据失败:', error) | |
| 436 | + this.receivableData = [] | |
| 437 | + } | |
| 438 | + }, | |
| 439 | + // 初始化图表 | |
| 440 | + initCharts() { | |
| 441 | + this.initTotalIncomeChart() | |
| 442 | + this.initChannelPieChart() | |
| 443 | + this.initPayableChart() | |
| 444 | + this.initReceivableChart() | |
| 445 | + }, | |
| 446 | + // 初始化总收入趋势图 | |
| 447 | + initTotalIncomeChart() { | |
| 448 | + if (!this.$refs.totalIncomeChart) return | |
| 449 | + if (this.totalIncomeChart) { | |
| 450 | + this.totalIncomeChart.dispose() | |
| 451 | + } | |
| 452 | + this.totalIncomeChart = echarts.init(this.$refs.totalIncomeChart) | |
| 453 | + window.addEventListener('resize', () => { | |
| 454 | + if (this.totalIncomeChart) { | |
| 455 | + this.totalIncomeChart.resize() | |
| 456 | + } | |
| 457 | + }) | |
| 458 | + }, | |
| 459 | + // 初始化收款渠道饼图 | |
| 460 | + initChannelPieChart() { | |
| 461 | + if (!this.$refs.channelPieChart) return | |
| 462 | + if (this.channelPieChart) { | |
| 463 | + this.channelPieChart.dispose() | |
| 464 | + } | |
| 465 | + this.channelPieChart = echarts.init(this.$refs.channelPieChart) | |
| 466 | + window.addEventListener('resize', () => { | |
| 467 | + if (this.channelPieChart) { | |
| 468 | + this.channelPieChart.resize() | |
| 469 | + } | |
| 470 | + }) | |
| 471 | + }, | |
| 472 | + // 初始化应付趋势图 | |
| 473 | + initPayableChart() { | |
| 474 | + if (!this.$refs.payableChart) return | |
| 475 | + if (this.payableChart) { | |
| 476 | + this.payableChart.dispose() | |
| 477 | + } | |
| 478 | + this.payableChart = echarts.init(this.$refs.payableChart) | |
| 479 | + window.addEventListener('resize', () => { | |
| 480 | + if (this.payableChart) { | |
| 481 | + this.payableChart.resize() | |
| 482 | + } | |
| 483 | + }) | |
| 484 | + }, | |
| 485 | + // 初始化应收趋势图 | |
| 486 | + initReceivableChart() { | |
| 487 | + if (!this.$refs.receivableChart) return | |
| 488 | + if (this.receivableChart) { | |
| 489 | + this.receivableChart.dispose() | |
| 490 | + } | |
| 491 | + this.receivableChart = echarts.init(this.$refs.receivableChart) | |
| 492 | + window.addEventListener('resize', () => { | |
| 493 | + if (this.receivableChart) { | |
| 494 | + this.receivableChart.resize() | |
| 495 | + } | |
| 496 | + }) | |
| 497 | + }, | |
| 498 | + // 更新所有图表 | |
| 499 | + updateCharts() { | |
| 500 | + this.$nextTick(() => { | |
| 501 | + this.updateTotalIncomeChart() | |
| 502 | + this.updateChannelPieChart() | |
| 503 | + this.updatePayableChart() | |
| 504 | + this.updateReceivableChart() | |
| 505 | + }) | |
| 506 | + }, | |
| 507 | + // 更新总收入趋势图 | |
| 508 | + updateTotalIncomeChart() { | |
| 509 | + if (!this.totalIncomeChart) { | |
| 510 | + console.warn('总收入图表实例不存在') | |
| 511 | + return | |
| 512 | + } | |
| 513 | + if (!this.totalIncomeData || this.totalIncomeData.length === 0) { | |
| 514 | + console.warn('总收入数据为空') | |
| 515 | + this.totalIncomeChart.setOption({ | |
| 516 | + title: { | |
| 517 | + text: '暂无数据', | |
| 518 | + left: 'center', | |
| 519 | + top: 'middle', | |
| 520 | + textStyle: { | |
| 521 | + color: '#999', | |
| 522 | + fontSize: 16 | |
| 523 | + } | |
| 524 | + } | |
| 525 | + }) | |
| 526 | + return | |
| 527 | + } | |
| 528 | + | |
| 529 | + // 按日期聚合数据 | |
| 530 | + const dateMap = new Map() | |
| 531 | + this.totalIncomeData.forEach(item => { | |
| 532 | + const date = item.periodDate | |
| 533 | + const income = item.totalIncome || 0 | |
| 534 | + if (dateMap.has(date)) { | |
| 535 | + dateMap.set(date, dateMap.get(date) + income) | |
| 536 | + } else { | |
| 537 | + dateMap.set(date, income) | |
| 538 | + } | |
| 539 | + }) | |
| 540 | + | |
| 541 | + const dates = Array.from(dateMap.keys()).sort() | |
| 542 | + const values = dates.map(date => dateMap.get(date)) | |
| 543 | + | |
| 544 | + console.log('总收入图表数据 - 日期:', dates, '金额:', values) | |
| 545 | + | |
| 546 | + const option = { | |
| 547 | + tooltip: { | |
| 548 | + trigger: 'axis', | |
| 549 | + formatter: (params) => { | |
| 550 | + const param = params[0] | |
| 551 | + return `${param.axisValue}<br/>${param.seriesName}: ${this.formatCurrency(param.value)}` | |
| 552 | + } | |
| 553 | + }, | |
| 554 | + grid: { | |
| 555 | + left: '3%', | |
| 556 | + right: '4%', | |
| 557 | + bottom: '3%', | |
| 558 | + containLabel: true | |
| 559 | + }, | |
| 560 | + xAxis: { | |
| 561 | + type: 'category', | |
| 562 | + data: dates, | |
| 563 | + axisLine: { lineStyle: { color: '#E0E6F1' } }, | |
| 564 | + axisLabel: { color: '#666', rotate: 45 } | |
| 565 | + }, | |
| 566 | + yAxis: { | |
| 567 | + type: 'value', | |
| 568 | + axisLine: { lineStyle: { color: '#E0E6F1' } }, | |
| 569 | + axisLabel: { | |
| 570 | + color: '#666', | |
| 571 | + formatter: (value) => { | |
| 572 | + if (value >= 10000) { | |
| 573 | + return (value / 10000).toFixed(1) + '万' | |
| 574 | + } | |
| 575 | + return value | |
| 576 | + } | |
| 577 | + }, | |
| 578 | + splitLine: { lineStyle: { color: '#F0F0F0' } } | |
| 579 | + }, | |
| 580 | + series: [{ | |
| 581 | + name: '总收入', | |
| 582 | + type: 'line', | |
| 583 | + smooth: true, | |
| 584 | + data: values, | |
| 585 | + itemStyle: { color: '#409EFF' }, | |
| 586 | + areaStyle: { | |
| 587 | + color: { | |
| 588 | + type: 'linear', | |
| 589 | + x: 0, y: 0, x2: 0, y2: 1, | |
| 590 | + colorStops: [ | |
| 591 | + { offset: 0, color: 'rgba(64,158,255,0.3)' }, | |
| 592 | + { offset: 1, color: 'rgba(64,158,255,0.1)' } | |
| 593 | + ] | |
| 594 | + } | |
| 595 | + } | |
| 596 | + }] | |
| 597 | + } | |
| 598 | + this.totalIncomeChart.setOption(option, true) | |
| 599 | + // 确保图表大小正确 | |
| 600 | + setTimeout(() => { | |
| 601 | + if (this.totalIncomeChart) { | |
| 602 | + this.totalIncomeChart.resize() | |
| 603 | + } | |
| 604 | + }, 100) | |
| 605 | + }, | |
| 606 | + // 更新收款渠道饼图 | |
| 607 | + updateChannelPieChart() { | |
| 608 | + if (!this.channelPieChart) { | |
| 609 | + console.warn('收款渠道图表实例不存在') | |
| 610 | + return | |
| 611 | + } | |
| 612 | + if (!this.channelData || this.channelData.length === 0) { | |
| 613 | + console.warn('收款渠道数据为空') | |
| 614 | + this.channelPieChart.setOption({ | |
| 615 | + title: { | |
| 616 | + text: '暂无数据', | |
| 617 | + left: 'center', | |
| 618 | + top: 'middle', | |
| 619 | + textStyle: { | |
| 620 | + color: '#999', | |
| 621 | + fontSize: 16 | |
| 622 | + } | |
| 623 | + } | |
| 624 | + }) | |
| 625 | + return | |
| 626 | + } | |
| 627 | + | |
| 628 | + // 按渠道聚合数据 | |
| 629 | + const channelMap = new Map() | |
| 630 | + this.channelData.forEach(store => { | |
| 631 | + if (store.paymentChannels && Array.isArray(store.paymentChannels)) { | |
| 632 | + store.paymentChannels.forEach(ch => { | |
| 633 | + const method = ch.paymentMethod || '未填写' | |
| 634 | + const amount = ch.amount || 0 | |
| 635 | + if (channelMap.has(method)) { | |
| 636 | + channelMap.set(method, channelMap.get(method) + amount) | |
| 637 | + } else { | |
| 638 | + channelMap.set(method, amount) | |
| 639 | + } | |
| 640 | + }) | |
| 641 | + } | |
| 642 | + }) | |
| 643 | + | |
| 644 | + const data = Array.from(channelMap.entries()).map(([name, value]) => ({ | |
| 645 | + name, | |
| 646 | + value | |
| 647 | + })) | |
| 648 | + | |
| 649 | + console.log('收款渠道图表数据:', data) | |
| 650 | + | |
| 651 | + if (data.length === 0) { | |
| 652 | + this.channelPieChart.setOption({ | |
| 653 | + title: { | |
| 654 | + text: '暂无数据', | |
| 655 | + left: 'center', | |
| 656 | + top: 'middle', | |
| 657 | + textStyle: { | |
| 658 | + color: '#999', | |
| 659 | + fontSize: 16 | |
| 660 | + } | |
| 661 | + } | |
| 662 | + }) | |
| 663 | + return | |
| 664 | + } | |
| 665 | + | |
| 666 | + const option = { | |
| 667 | + tooltip: { | |
| 668 | + trigger: 'item', | |
| 669 | + formatter: '{a} <br/>{b}: {c} ({d}%)' | |
| 670 | + }, | |
| 671 | + legend: { | |
| 672 | + orient: 'vertical', | |
| 673 | + left: 'left', | |
| 674 | + top: 'middle' | |
| 675 | + }, | |
| 676 | + series: [{ | |
| 677 | + name: '收款渠道', | |
| 678 | + type: 'pie', | |
| 679 | + radius: ['40%', '70%'], | |
| 680 | + avoidLabelOverlap: false, | |
| 681 | + itemStyle: { | |
| 682 | + borderRadius: 10, | |
| 683 | + borderColor: '#fff', | |
| 684 | + borderWidth: 2 | |
| 685 | + }, | |
| 686 | + label: { | |
| 687 | + show: true, | |
| 688 | + formatter: '{b}: {d}%' | |
| 689 | + }, | |
| 690 | + emphasis: { | |
| 691 | + label: { | |
| 692 | + show: true, | |
| 693 | + fontSize: '16', | |
| 694 | + fontWeight: 'bold' | |
| 695 | + } | |
| 696 | + }, | |
| 697 | + data: data | |
| 698 | + }] | |
| 699 | + } | |
| 700 | + this.channelPieChart.setOption(option, true) | |
| 701 | + setTimeout(() => { | |
| 702 | + if (this.channelPieChart) { | |
| 703 | + this.channelPieChart.resize() | |
| 704 | + } | |
| 705 | + }, 100) | |
| 706 | + }, | |
| 707 | + // 更新应付趋势图 | |
| 708 | + updatePayableChart() { | |
| 709 | + if (!this.payableChart) { | |
| 710 | + console.warn('应付图表实例不存在') | |
| 711 | + return | |
| 712 | + } | |
| 713 | + if (!this.payableData || this.payableData.length === 0) { | |
| 714 | + console.warn('应付数据为空') | |
| 715 | + this.payableChart.setOption({ | |
| 716 | + title: { | |
| 717 | + text: '暂无数据', | |
| 718 | + left: 'center', | |
| 719 | + top: 'middle', | |
| 720 | + textStyle: { | |
| 721 | + color: '#999', | |
| 722 | + fontSize: 16 | |
| 723 | + } | |
| 724 | + } | |
| 725 | + }) | |
| 726 | + return | |
| 727 | + } | |
| 728 | + | |
| 729 | + const dateMap = new Map() | |
| 730 | + this.payableData.forEach(item => { | |
| 731 | + const date = item.periodDate | |
| 732 | + const amount = item.totalPayable || 0 | |
| 733 | + if (dateMap.has(date)) { | |
| 734 | + dateMap.set(date, dateMap.get(date) + amount) | |
| 735 | + } else { | |
| 736 | + dateMap.set(date, amount) | |
| 737 | + } | |
| 738 | + }) | |
| 739 | + | |
| 740 | + const dates = Array.from(dateMap.keys()).sort() | |
| 741 | + const values = dates.map(date => dateMap.get(date)) | |
| 742 | + | |
| 743 | + console.log('应付图表数据 - 日期:', dates, '金额:', values) | |
| 744 | + | |
| 745 | + const option = { | |
| 746 | + tooltip: { | |
| 747 | + trigger: 'axis', | |
| 748 | + formatter: (params) => { | |
| 749 | + const param = params[0] | |
| 750 | + return `${param.axisValue}<br/>${param.seriesName}: ${this.formatCurrency(param.value)}` | |
| 751 | + } | |
| 752 | + }, | |
| 753 | + grid: { | |
| 754 | + left: '3%', | |
| 755 | + right: '4%', | |
| 756 | + bottom: '3%', | |
| 757 | + containLabel: true | |
| 758 | + }, | |
| 759 | + xAxis: { | |
| 760 | + type: 'category', | |
| 761 | + data: dates, | |
| 762 | + axisLine: { lineStyle: { color: '#E0E6F1' } }, | |
| 763 | + axisLabel: { color: '#666', rotate: 45 } | |
| 764 | + }, | |
| 765 | + yAxis: { | |
| 766 | + type: 'value', | |
| 767 | + axisLine: { lineStyle: { color: '#E0E6F1' } }, | |
| 768 | + axisLabel: { | |
| 769 | + color: '#666', | |
| 770 | + formatter: (value) => { | |
| 771 | + if (value >= 10000) { | |
| 772 | + return (value / 10000).toFixed(1) + '万' | |
| 773 | + } | |
| 774 | + return value | |
| 775 | + } | |
| 776 | + }, | |
| 777 | + splitLine: { lineStyle: { color: '#F0F0F0' } } | |
| 778 | + }, | |
| 779 | + series: [{ | |
| 780 | + name: '应付金额', | |
| 781 | + type: 'bar', | |
| 782 | + data: values, | |
| 783 | + itemStyle: { color: '#E6A23C' } | |
| 784 | + }] | |
| 785 | + } | |
| 786 | + this.payableChart.setOption(option, true) | |
| 787 | + setTimeout(() => { | |
| 788 | + if (this.payableChart) { | |
| 789 | + this.payableChart.resize() | |
| 790 | + } | |
| 791 | + }, 100) | |
| 792 | + }, | |
| 793 | + // 更新应收趋势图 | |
| 794 | + updateReceivableChart() { | |
| 795 | + if (!this.receivableChart) { | |
| 796 | + console.warn('应收图表实例不存在') | |
| 797 | + return | |
| 798 | + } | |
| 799 | + if (!this.receivableData || this.receivableData.length === 0) { | |
| 800 | + console.warn('应收数据为空') | |
| 801 | + this.receivableChart.setOption({ | |
| 802 | + title: { | |
| 803 | + text: '暂无数据', | |
| 804 | + left: 'center', | |
| 805 | + top: 'middle', | |
| 806 | + textStyle: { | |
| 807 | + color: '#999', | |
| 808 | + fontSize: 16 | |
| 809 | + } | |
| 810 | + } | |
| 811 | + }) | |
| 812 | + return | |
| 813 | + } | |
| 814 | + | |
| 815 | + const dateMap = new Map() | |
| 816 | + this.receivableData.forEach(item => { | |
| 817 | + const date = item.periodDate | |
| 818 | + const amount = item.totalReceivable || 0 | |
| 819 | + if (dateMap.has(date)) { | |
| 820 | + dateMap.set(date, dateMap.get(date) + amount) | |
| 821 | + } else { | |
| 822 | + dateMap.set(date, amount) | |
| 823 | + } | |
| 824 | + }) | |
| 825 | + | |
| 826 | + const dates = Array.from(dateMap.keys()).sort() | |
| 827 | + const values = dates.map(date => dateMap.get(date)) | |
| 828 | + | |
| 829 | + console.log('应收图表数据 - 日期:', dates, '金额:', values) | |
| 830 | + | |
| 831 | + const option = { | |
| 832 | + tooltip: { | |
| 833 | + trigger: 'axis', | |
| 834 | + formatter: (params) => { | |
| 835 | + const param = params[0] | |
| 836 | + return `${param.axisValue}<br/>${param.seriesName}: ${this.formatCurrency(param.value)}` | |
| 837 | + } | |
| 838 | + }, | |
| 839 | + grid: { | |
| 840 | + left: '3%', | |
| 841 | + right: '4%', | |
| 842 | + bottom: '3%', | |
| 843 | + containLabel: true | |
| 844 | + }, | |
| 845 | + xAxis: { | |
| 846 | + type: 'category', | |
| 847 | + data: dates, | |
| 848 | + axisLine: { lineStyle: { color: '#E0E6F1' } }, | |
| 849 | + axisLabel: { color: '#666', rotate: 45 } | |
| 850 | + }, | |
| 851 | + yAxis: { | |
| 852 | + type: 'value', | |
| 853 | + axisLine: { lineStyle: { color: '#E0E6F1' } }, | |
| 854 | + axisLabel: { | |
| 855 | + color: '#666', | |
| 856 | + formatter: (value) => { | |
| 857 | + if (value >= 10000) { | |
| 858 | + return (value / 10000).toFixed(1) + '万' | |
| 859 | + } | |
| 860 | + return value | |
| 861 | + } | |
| 862 | + }, | |
| 863 | + splitLine: { lineStyle: { color: '#F0F0F0' } } | |
| 864 | + }, | |
| 865 | + series: [{ | |
| 866 | + name: '应收金额', | |
| 867 | + type: 'bar', | |
| 868 | + data: values, | |
| 869 | + itemStyle: { color: '#67C23A' } | |
| 870 | + }] | |
| 871 | + } | |
| 872 | + this.receivableChart.setOption(option, true) | |
| 873 | + setTimeout(() => { | |
| 874 | + if (this.receivableChart) { | |
| 875 | + this.receivableChart.resize() | |
| 876 | + } | |
| 877 | + }, 100) | |
| 878 | + }, | |
| 879 | + // 销毁图表 | |
| 880 | + disposeCharts() { | |
| 881 | + if (this.totalIncomeChart) { | |
| 882 | + this.totalIncomeChart.dispose() | |
| 883 | + this.totalIncomeChart = null | |
| 884 | + } | |
| 885 | + if (this.channelPieChart) { | |
| 886 | + this.channelPieChart.dispose() | |
| 887 | + this.channelPieChart = null | |
| 888 | + } | |
| 889 | + if (this.payableChart) { | |
| 890 | + this.payableChart.dispose() | |
| 891 | + this.payableChart = null | |
| 892 | + } | |
| 893 | + if (this.receivableChart) { | |
| 894 | + this.receivableChart.dispose() | |
| 895 | + this.receivableChart = null | |
| 896 | + } | |
| 897 | + }, | |
| 898 | + // 切换表格Tab | |
| 899 | + handleTabClick(tab) { | |
| 900 | + this.updateTable() | |
| 901 | + }, | |
| 902 | + // 更新表格数据 | |
| 903 | + updateTable() { | |
| 904 | + this.tableLoading = true | |
| 905 | + setTimeout(() => { | |
| 906 | + switch (this.activeTab) { | |
| 907 | + case 'totalIncome': | |
| 908 | + this.updateTotalIncomeTable() | |
| 909 | + break | |
| 910 | + case 'channel': | |
| 911 | + this.updateChannelTable() | |
| 912 | + break | |
| 913 | + case 'payable': | |
| 914 | + this.updatePayableTable() | |
| 915 | + break | |
| 916 | + case 'receivable': | |
| 917 | + this.updateReceivableTable() | |
| 918 | + break | |
| 919 | + } | |
| 920 | + this.tableLoading = false | |
| 921 | + }, 100) | |
| 922 | + }, | |
| 923 | + // 更新总收入表格 | |
| 924 | + updateTotalIncomeTable() { | |
| 925 | + if (!this.totalIncomeData) { | |
| 926 | + this.tableData = [] | |
| 927 | + return | |
| 928 | + } | |
| 929 | + this.tableColumns = [ | |
| 930 | + { prop: 'storeName', label: '门店名称', width: 150 }, | |
| 931 | + { prop: 'periodDate', label: '统计日期', width: 120 }, | |
| 932 | + { | |
| 933 | + prop: 'totalIncome', | |
| 934 | + label: '总收入', | |
| 935 | + width: 150, | |
| 936 | + formatter: (row) => this.formatCurrency(row.totalIncome) | |
| 937 | + }, | |
| 938 | + { prop: 'billingCount', label: '开单笔数', width: 120 }, | |
| 939 | + { | |
| 940 | + prop: 'averageAmount', | |
| 941 | + label: '平均单笔', | |
| 942 | + width: 150, | |
| 943 | + formatter: (row) => this.formatCurrency(row.averageAmount) | |
| 944 | + } | |
| 945 | + ] | |
| 946 | + this.tableData = this.totalIncomeData.map(item => ({ | |
| 947 | + ...item, | |
| 948 | + averageAmount: item.billingCount > 0 ? item.totalIncome / item.billingCount : 0 | |
| 949 | + })) | |
| 950 | + }, | |
| 951 | + // 更新收款渠道表格 | |
| 952 | + updateChannelTable() { | |
| 953 | + if (!this.channelData) { | |
| 954 | + this.tableData = [] | |
| 955 | + return | |
| 956 | + } | |
| 957 | + this.tableColumns = [ | |
| 958 | + { prop: 'storeName', label: '门店名称', width: 150 }, | |
| 959 | + { prop: 'periodDate', label: '统计日期', width: 120 }, | |
| 960 | + { prop: 'paymentMethod', label: '付款方式', width: 120 }, | |
| 961 | + { | |
| 962 | + prop: 'amount', | |
| 963 | + label: '收款金额', | |
| 964 | + width: 150, | |
| 965 | + formatter: (row) => this.formatCurrency(row.amount) | |
| 966 | + }, | |
| 967 | + { prop: 'count', label: '笔数', width: 100 }, | |
| 968 | + { | |
| 969 | + prop: 'percentage', | |
| 970 | + label: '占比', | |
| 971 | + width: 100, | |
| 972 | + formatter: (row) => (row.percentage || 0) + '%' | |
| 973 | + } | |
| 974 | + ] | |
| 975 | + const list = [] | |
| 976 | + this.channelData.forEach(store => { | |
| 977 | + if (store.paymentChannels) { | |
| 978 | + store.paymentChannels.forEach(ch => { | |
| 979 | + list.push({ | |
| 980 | + storeName: store.storeName, | |
| 981 | + periodDate: store.periodDate, | |
| 982 | + paymentMethod: ch.paymentMethod, | |
| 983 | + amount: ch.amount, | |
| 984 | + count: ch.count, | |
| 985 | + percentage: ch.percentage | |
| 986 | + }) | |
| 987 | + }) | |
| 988 | + } | |
| 989 | + }) | |
| 990 | + this.tableData = list | |
| 991 | + }, | |
| 992 | + // 更新应付表格 | |
| 993 | + updatePayableTable() { | |
| 994 | + if (!this.payableData) { | |
| 995 | + this.tableData = [] | |
| 996 | + return | |
| 997 | + } | |
| 998 | + this.tableColumns = [ | |
| 999 | + { prop: 'storeName', label: '门店名称', width: 150 }, | |
| 1000 | + { prop: 'periodDate', label: '统计日期', width: 120 }, | |
| 1001 | + { prop: 'cooperationName', label: '合作机构', width: 200 }, | |
| 1002 | + { | |
| 1003 | + prop: 'payableAmount', | |
| 1004 | + label: '应付金额', | |
| 1005 | + width: 150, | |
| 1006 | + formatter: (row) => this.formatCurrency(row.payableAmount) | |
| 1007 | + }, | |
| 1008 | + { prop: 'billingCount', label: '开单笔数', width: 120 } | |
| 1009 | + ] | |
| 1010 | + const list = [] | |
| 1011 | + this.payableData.forEach(store => { | |
| 1012 | + if (store.cooperationItems) { | |
| 1013 | + store.cooperationItems.forEach(item => { | |
| 1014 | + list.push({ | |
| 1015 | + storeName: store.storeName, | |
| 1016 | + periodDate: store.periodDate, | |
| 1017 | + cooperationName: item.cooperationName, | |
| 1018 | + payableAmount: item.payableAmount, | |
| 1019 | + billingCount: item.billingCount | |
| 1020 | + }) | |
| 1021 | + }) | |
| 1022 | + } | |
| 1023 | + }) | |
| 1024 | + this.tableData = list | |
| 1025 | + }, | |
| 1026 | + // 更新应收表格 | |
| 1027 | + updateReceivableTable() { | |
| 1028 | + if (!this.receivableData) { | |
| 1029 | + this.tableData = [] | |
| 1030 | + return | |
| 1031 | + } | |
| 1032 | + this.tableColumns = [ | |
| 1033 | + { prop: 'storeName', label: '门店名称', width: 150 }, | |
| 1034 | + { prop: 'periodDate', label: '统计日期', width: 120 }, | |
| 1035 | + { prop: 'hospitalName', label: '付款医院', width: 200 }, | |
| 1036 | + { | |
| 1037 | + prop: 'receivableAmount', | |
| 1038 | + label: '应收金额', | |
| 1039 | + width: 150, | |
| 1040 | + formatter: (row) => this.formatCurrency(row.receivableAmount) | |
| 1041 | + }, | |
| 1042 | + { prop: 'billingCount', label: '开单笔数', width: 120 } | |
| 1043 | + ] | |
| 1044 | + const list = [] | |
| 1045 | + this.receivableData.forEach(store => { | |
| 1046 | + if (store.hospitalItems) { | |
| 1047 | + store.hospitalItems.forEach(item => { | |
| 1048 | + list.push({ | |
| 1049 | + storeName: store.storeName, | |
| 1050 | + periodDate: store.periodDate, | |
| 1051 | + hospitalName: item.hospitalName, | |
| 1052 | + receivableAmount: item.receivableAmount, | |
| 1053 | + billingCount: item.billingCount | |
| 1054 | + }) | |
| 1055 | + }) | |
| 1056 | + } | |
| 1057 | + }) | |
| 1058 | + this.tableData = list | |
| 1059 | + }, | |
| 1060 | + // 格式化货币 | |
| 1061 | + formatCurrency(value) { | |
| 1062 | + if (value === null || value === undefined) return '¥0.00' | |
| 1063 | + const num = Number(value) | |
| 1064 | + if (isNaN(num)) return '¥0.00' | |
| 1065 | + return '¥' + num.toLocaleString('zh-CN', { | |
| 1066 | + minimumFractionDigits: 2, | |
| 1067 | + maximumFractionDigits: 2 | |
| 1068 | + }) | |
| 1069 | + } | |
| 1070 | + } | |
| 1071 | +} | |
| 1072 | +</script> | |
| 1073 | + | |
| 1074 | +<style lang="scss" scoped> | |
| 1075 | +.financial-report-container { | |
| 1076 | + padding: 20px; | |
| 1077 | + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); | |
| 1078 | + min-height: calc(100vh - 84px); | |
| 1079 | + | |
| 1080 | + // 筛选卡片 | |
| 1081 | + .search-card { | |
| 1082 | + margin-bottom: 20px; | |
| 1083 | + border-radius: 12px; | |
| 1084 | + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); | |
| 1085 | + background: rgba(255, 255, 255, 0.95); | |
| 1086 | + backdrop-filter: blur(10px); | |
| 1087 | + | |
| 1088 | + .search-form { | |
| 1089 | + ::v-deep .el-form-item { | |
| 1090 | + margin-bottom: 0; | |
| 1091 | + } | |
| 1092 | + } | |
| 1093 | + } | |
| 1094 | + | |
| 1095 | + // 统计卡片区域 | |
| 1096 | + .stat-cards-section { | |
| 1097 | + margin-bottom: 20px; | |
| 1098 | + | |
| 1099 | + .stat-card { | |
| 1100 | + background: rgba(255, 255, 255, 0.95); | |
| 1101 | + backdrop-filter: blur(10px); | |
| 1102 | + border-radius: 16px; | |
| 1103 | + padding: 24px; | |
| 1104 | + display: flex; | |
| 1105 | + align-items: center; | |
| 1106 | + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); | |
| 1107 | + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 1108 | + cursor: pointer; | |
| 1109 | + border: 1px solid rgba(255, 255, 255, 0.8); | |
| 1110 | + | |
| 1111 | + &:hover { | |
| 1112 | + transform: translateY(-4px); | |
| 1113 | + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15); | |
| 1114 | + } | |
| 1115 | + | |
| 1116 | + .stat-icon { | |
| 1117 | + width: 64px; | |
| 1118 | + height: 64px; | |
| 1119 | + border-radius: 12px; | |
| 1120 | + display: flex; | |
| 1121 | + align-items: center; | |
| 1122 | + justify-content: center; | |
| 1123 | + margin-right: 20px; | |
| 1124 | + font-size: 32px; | |
| 1125 | + flex-shrink: 0; | |
| 1126 | + } | |
| 1127 | + | |
| 1128 | + .stat-content { | |
| 1129 | + flex: 1; | |
| 1130 | + min-width: 0; | |
| 1131 | + | |
| 1132 | + .stat-label { | |
| 1133 | + font-size: 14px; | |
| 1134 | + color: #666; | |
| 1135 | + margin-bottom: 8px; | |
| 1136 | + font-weight: 500; | |
| 1137 | + } | |
| 1138 | + | |
| 1139 | + .stat-value { | |
| 1140 | + font-size: 24px; | |
| 1141 | + font-weight: 700; | |
| 1142 | + color: #0f172a; | |
| 1143 | + margin-bottom: 4px; | |
| 1144 | + white-space: nowrap; | |
| 1145 | + overflow: hidden; | |
| 1146 | + text-overflow: ellipsis; | |
| 1147 | + } | |
| 1148 | + | |
| 1149 | + .stat-meta { | |
| 1150 | + font-size: 12px; | |
| 1151 | + color: #999; | |
| 1152 | + } | |
| 1153 | + } | |
| 1154 | + | |
| 1155 | + &.income-card .stat-icon { | |
| 1156 | + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| 1157 | + color: #fff; | |
| 1158 | + } | |
| 1159 | + | |
| 1160 | + &.channel-card .stat-icon { | |
| 1161 | + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| 1162 | + color: #fff; | |
| 1163 | + } | |
| 1164 | + | |
| 1165 | + &.payable-card .stat-icon { | |
| 1166 | + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); | |
| 1167 | + color: #fff; | |
| 1168 | + } | |
| 1169 | + | |
| 1170 | + &.receivable-card .stat-icon { | |
| 1171 | + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); | |
| 1172 | + color: #fff; | |
| 1173 | + } | |
| 1174 | + } | |
| 1175 | + } | |
| 1176 | + | |
| 1177 | + // 图表区域 | |
| 1178 | + .charts-section { | |
| 1179 | + margin-bottom: 20px; | |
| 1180 | + | |
| 1181 | + .chart-card { | |
| 1182 | + border-radius: 12px; | |
| 1183 | + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); | |
| 1184 | + background: rgba(255, 255, 255, 0.95); | |
| 1185 | + backdrop-filter: blur(10px); | |
| 1186 | + transition: all 0.3s ease; | |
| 1187 | + | |
| 1188 | + &:hover { | |
| 1189 | + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12); | |
| 1190 | + } | |
| 1191 | + | |
| 1192 | + .chart-header { | |
| 1193 | + display: flex; | |
| 1194 | + align-items: center; | |
| 1195 | + justify-content: space-between; | |
| 1196 | + | |
| 1197 | + .chart-title { | |
| 1198 | + font-size: 16px; | |
| 1199 | + font-weight: 600; | |
| 1200 | + color: #0f172a; | |
| 1201 | + display: flex; | |
| 1202 | + align-items: center; | |
| 1203 | + gap: 8px; | |
| 1204 | + | |
| 1205 | + i { | |
| 1206 | + font-size: 18px; | |
| 1207 | + color: #409EFF; | |
| 1208 | + } | |
| 1209 | + } | |
| 1210 | + } | |
| 1211 | + | |
| 1212 | + .chart-container { | |
| 1213 | + width: 100%; | |
| 1214 | + height: 350px; | |
| 1215 | + min-height: 350px; | |
| 1216 | + } | |
| 1217 | + } | |
| 1218 | + } | |
| 1219 | + | |
| 1220 | + // 表格卡片 | |
| 1221 | + .table-card { | |
| 1222 | + border-radius: 12px; | |
| 1223 | + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); | |
| 1224 | + background: rgba(255, 255, 255, 0.95); | |
| 1225 | + backdrop-filter: blur(10px); | |
| 1226 | + | |
| 1227 | + .table-header { | |
| 1228 | + display: flex; | |
| 1229 | + align-items: center; | |
| 1230 | + justify-content: space-between; | |
| 1231 | + | |
| 1232 | + .table-title { | |
| 1233 | + font-size: 16px; | |
| 1234 | + font-weight: 600; | |
| 1235 | + color: #0f172a; | |
| 1236 | + display: flex; | |
| 1237 | + align-items: center; | |
| 1238 | + gap: 8px; | |
| 1239 | + | |
| 1240 | + i { | |
| 1241 | + font-size: 18px; | |
| 1242 | + color: #409EFF; | |
| 1243 | + } | |
| 1244 | + } | |
| 1245 | + | |
| 1246 | + .table-tabs { | |
| 1247 | + ::v-deep .el-tabs__header { | |
| 1248 | + margin: 0; | |
| 1249 | + } | |
| 1250 | + | |
| 1251 | + ::v-deep .el-tabs__item { | |
| 1252 | + font-weight: 500; | |
| 1253 | + } | |
| 1254 | + } | |
| 1255 | + } | |
| 1256 | + | |
| 1257 | + .data-table { | |
| 1258 | + margin-top: 20px; | |
| 1259 | + } | |
| 1260 | + } | |
| 1261 | +} | |
| 1262 | + | |
| 1263 | +// 响应式适配 | |
| 1264 | +@media (max-width: 768px) { | |
| 1265 | + .financial-report-container { | |
| 1266 | + padding: 12px; | |
| 1267 | + | |
| 1268 | + .stat-cards-section { | |
| 1269 | + .stat-card { | |
| 1270 | + padding: 16px; | |
| 1271 | + | |
| 1272 | + .stat-icon { | |
| 1273 | + width: 48px; | |
| 1274 | + height: 48px; | |
| 1275 | + font-size: 24px; | |
| 1276 | + margin-right: 12px; | |
| 1277 | + } | |
| 1278 | + | |
| 1279 | + .stat-content { | |
| 1280 | + .stat-value { | |
| 1281 | + font-size: 20px; | |
| 1282 | + } | |
| 1283 | + } | |
| 1284 | + } | |
| 1285 | + } | |
| 1286 | + | |
| 1287 | + .chart-card { | |
| 1288 | + .chart-container { | |
| 1289 | + height: 280px; | |
| 1290 | + min-height: 280px; | |
| 1291 | + } | |
| 1292 | + } | |
| 1293 | + } | |
| 1294 | +} | |
| 1295 | +</style> | ... | ... |
antis-ncc-admin/src/views/lqKhxx/index.vue
| 1 | 1 | <template> |
| 2 | 2 | <div class="NCC-common-layout"> |
| 3 | 3 | <div class="NCC-common-layout-center"> |
| 4 | - <el-row class="NCC-common-search-box" :gutter="16"> | |
| 5 | - <el-form @submit.native.prevent> | |
| 6 | - <el-col :span="6"> | |
| 7 | - <el-form-item label="客户名称"> | |
| 8 | - <el-input v-model="query.khmc" placeholder="客户名称" clearable /> | |
| 9 | - </el-form-item> | |
| 10 | - </el-col> | |
| 11 | - <el-col :span="6"> | |
| 12 | - <el-form-item label="手机号"> | |
| 13 | - <el-input v-model="query.sjh" placeholder="手机号" clearable /> | |
| 14 | - </el-form-item> | |
| 15 | - </el-col> | |
| 16 | - <template v-if="showAll"> | |
| 17 | - <el-col :span="6"> | |
| 18 | - <el-form-item label="性别"> | |
| 19 | - <el-select v-model="query.xb" placeholder="性别" clearable> | |
| 20 | - <el-option v-for="(item, index) in xbOptions" :key="index" :label="item.fullName" | |
| 21 | - :value="item.id" /> | |
| 22 | - </el-select> | |
| 23 | - </el-form-item> | |
| 24 | - </el-col> | |
| 25 | - <el-col :span="6"> | |
| 26 | - <el-form-item label="归属门店"> | |
| 27 | - <el-select v-model="query.gsmd" placeholder="请选择门店" clearable filterable> | |
| 28 | - <el-option v-for="(item, index) in gsmdOptions" :key="index" :label="item.fullName" | |
| 29 | - :value="item.id" /> | |
| 30 | - </el-select> | |
| 4 | + <!-- 现代化筛选卡片 --> | |
| 5 | + <el-card class="search-card" :class="{ 'search-card-animated': searchCardMounted }" shadow="never"> | |
| 6 | + <el-form @submit.native.prevent label-width="80px" size="small"> | |
| 7 | + <!-- 第一行:基础筛选 --> | |
| 8 | + <el-row :gutter="16" class="search-row search-row-1"> | |
| 9 | + <el-col :span="5"> | |
| 10 | + <el-form-item label="客户名称"> | |
| 11 | + <el-input v-model="query.khmc" placeholder="请输入客户名称" clearable | |
| 12 | + prefix-icon="el-icon-user" /> | |
| 31 | 13 | </el-form-item> |
| 32 | 14 | </el-col> |
| 33 | - | |
| 34 | - <el-col :span="6"> | |
| 35 | - <el-form-item label="客户类型"> | |
| 36 | - <el-select v-model="query.khlx" placeholder="客户类型" clearable> | |
| 37 | - <el-option v-for="(item, index) in khlxOptions" :key="index" :label="item.fullName" | |
| 38 | - :value="item.id" /> | |
| 39 | - </el-select> | |
| 40 | - </el-form-item> | |
| 41 | - </el-col> | |
| 42 | - <el-col :span="6"> | |
| 43 | - <el-form-item label="健康师"> | |
| 44 | - <el-input v-model="query.mainHealthUser" placeholder="健康师" clearable /> | |
| 45 | - </el-form-item> | |
| 46 | - </el-col> | |
| 47 | - <el-col :span="6"> | |
| 48 | - <el-form-item label="负责顾问"> | |
| 49 | - <el-input v-model="query.subHealthUser" placeholder="负责顾问" clearable /> | |
| 15 | + <el-col :span="5"> | |
| 16 | + <el-form-item label="手机号"> | |
| 17 | + <el-input v-model="query.sjh" placeholder="请输入手机号" clearable | |
| 18 | + prefix-icon="el-icon-mobile-phone" /> | |
| 50 | 19 | </el-form-item> |
| 51 | 20 | </el-col> |
| 52 | - <el-col :span="6"> | |
| 53 | - <el-form-item label="进店渠道"> | |
| 54 | - <el-select v-model="query.jdqd" placeholder="请选择进店渠道" clearable filterable> | |
| 55 | - <el-option v-for="(item, index) in jdqdOptions" :key="index" :label="item.fullName" | |
| 21 | + <el-col :span="5"> | |
| 22 | + <el-form-item label="归属门店"> | |
| 23 | + <el-select v-model="query.gsmd" placeholder="请选择门店" clearable filterable | |
| 24 | + style="width: 100%"> | |
| 25 | + <el-option v-for="(item, index) in gsmdOptions" :key="index" :label="item.fullName" | |
| 56 | 26 | :value="item.id" /> |
| 57 | 27 | </el-select> |
| 58 | 28 | </el-form-item> |
| 59 | 29 | </el-col> |
| 60 | - <el-col :span="6"> | |
| 61 | - <el-form-item label="推荐人"> | |
| 62 | - <el-select v-model="query.tjr" filterable remote reserve-keyword placeholder="请输入关键词搜索" | |
| 63 | - :remote-method="remoteMethod" :loading="kdhyLoading" clearable> | |
| 64 | - <el-option v-for="item in kdhyOptions" :key="item.value" :label="item.label" | |
| 65 | - :value="item.value"> | |
| 66 | - </el-option> | |
| 30 | + <el-col :span="4"> | |
| 31 | + <el-form-item label="消费等级"> | |
| 32 | + <el-select v-model="query.consumeLevel" placeholder="请选择等级" clearable | |
| 33 | + style="width: 100%"> | |
| 34 | + <el-option v-for="(item, index) in consumeLevelOptions" :key="index" | |
| 35 | + :label="item.fullName" :value="item.id" /> | |
| 67 | 36 | </el-select> |
| 68 | 37 | </el-form-item> |
| 69 | 38 | </el-col> |
| 70 | - <el-col :span="6"> | |
| 71 | - <el-form-item label="出生日期"> | |
| 72 | - <el-date-picker v-model="query.yanglsr" type="daterange" value-format="timestamp" | |
| 73 | - format="yyyy-MM-dd" start-placeholder="开始日期" end-placeholder="结束日期"> | |
| 74 | - </el-date-picker> | |
| 75 | - </el-form-item> | |
| 76 | - </el-col> | |
| 77 | - <el-col :span="6"> | |
| 78 | - <el-form-item label="注册时间"> | |
| 79 | - <el-date-picker v-model="query.zcsj" type="daterange" value-format="timestamp" | |
| 80 | - format="yyyy-MM-dd" start-placeholder="开始日期" end-placeholder="结束日期"> | |
| 81 | - </el-date-picker> | |
| 82 | - </el-form-item> | |
| 39 | + <el-col :span="5"> | |
| 40 | + <div class="search-actions-inline"> | |
| 41 | + <el-button type="primary" icon="el-icon-search" @click="search()">查询</el-button> | |
| 42 | + <el-button icon="el-icon-refresh-right" @click="reset()">重置</el-button> | |
| 43 | + <el-button type="text" :icon="showAll ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" | |
| 44 | + @click="showAll = !showAll"> | |
| 45 | + {{ showAll ? '收起' : '展开' }} | |
| 46 | + </el-button> | |
| 47 | + </div> | |
| 83 | 48 | </el-col> |
| 84 | - </template> | |
| 85 | - <el-col :span="6"> | |
| 86 | - <el-form-item> | |
| 87 | - <el-button type="primary" icon="el-icon-search" @click="search()">查询</el-button> | |
| 88 | - <el-button icon="el-icon-refresh-right" @click="reset()">重置</el-button> | |
| 89 | - <el-button type="text" icon="el-icon-arrow-down" @click="showAll = true" | |
| 90 | - v-if="!showAll">展开</el-button> | |
| 91 | - <el-button type="text" icon="el-icon-arrow-up" @click="showAll = false" | |
| 92 | - v-else>收起</el-button> | |
| 93 | - </el-form-item> | |
| 94 | - </el-col> | |
| 49 | + </el-row> | |
| 50 | + | |
| 51 | + <!-- 高级筛选区域(可展开) --> | |
| 52 | + <div class="filter-expand-panel" :class="{ 'is-expanded': showAll }"> | |
| 53 | + <!-- 第二行:高级筛选 --> | |
| 54 | + <el-row :gutter="16" class="search-row search-row-2"> | |
| 55 | + <el-col :span="5"> | |
| 56 | + <el-form-item label="性别"> | |
| 57 | + <el-select v-model="query.xb" placeholder="请选择性别" clearable style="width: 100%"> | |
| 58 | + <el-option v-for="(item, index) in xbOptions" :key="index" | |
| 59 | + :label="item.fullName" :value="item.id" /> | |
| 60 | + </el-select> | |
| 61 | + </el-form-item> | |
| 62 | + </el-col> | |
| 63 | + <el-col :span="5"> | |
| 64 | + <el-form-item label="客户类型"> | |
| 65 | + <el-select v-model="query.khlx" placeholder="请选择类型" clearable style="width: 100%"> | |
| 66 | + <el-option v-for="(item, index) in khlxOptions" :key="index" | |
| 67 | + :label="item.fullName" :value="item.id" /> | |
| 68 | + </el-select> | |
| 69 | + </el-form-item> | |
| 70 | + </el-col> | |
| 71 | + <el-col :span="5"> | |
| 72 | + <el-form-item label="健康师"> | |
| 73 | + <el-input v-model="query.mainHealthUser" placeholder="请输入健康师" clearable /> | |
| 74 | + </el-form-item> | |
| 75 | + </el-col> | |
| 76 | + <el-col :span="5"> | |
| 77 | + <el-form-item label="负责顾问"> | |
| 78 | + <el-input v-model="query.subHealthUser" placeholder="请输入负责顾问" clearable /> | |
| 79 | + </el-form-item> | |
| 80 | + </el-col> | |
| 81 | + </el-row> | |
| 82 | + <el-row :gutter="16" class="search-row search-row-3"> | |
| 83 | + <el-col :span="5"> | |
| 84 | + <el-form-item label="进店渠道"> | |
| 85 | + <el-select v-model="query.jdqd" placeholder="请选择渠道" clearable filterable | |
| 86 | + style="width: 100%"> | |
| 87 | + <el-option v-for="(item, index) in jdqdOptions" :key="index" | |
| 88 | + :label="item.fullName" :value="item.id" /> | |
| 89 | + </el-select> | |
| 90 | + </el-form-item> | |
| 91 | + </el-col> | |
| 92 | + <el-col :span="5"> | |
| 93 | + <el-form-item label="沉睡天数"> | |
| 94 | + <el-select v-model="query.sleepDaysRange" placeholder="请选择天数范围" clearable | |
| 95 | + style="width: 100%"> | |
| 96 | + <el-option v-for="(item, index) in sleepDaysRangeOptions" :key="index" | |
| 97 | + :label="item.label" :value="item.value" /> | |
| 98 | + </el-select> | |
| 99 | + </el-form-item> | |
| 100 | + </el-col> | |
| 101 | + <el-col :span="5"> | |
| 102 | + <el-form-item label="注册时间"> | |
| 103 | + <el-date-picker v-model="query.zcsj" type="daterange" value-format="timestamp" | |
| 104 | + format="yyyy-MM-dd" start-placeholder="开始" end-placeholder="结束" | |
| 105 | + style="width: 100%"> | |
| 106 | + </el-date-picker> | |
| 107 | + </el-form-item> | |
| 108 | + </el-col> | |
| 109 | + <el-col :span="5"> | |
| 110 | + <el-form-item label="出生日期"> | |
| 111 | + <el-date-picker v-model="query.yanglsr" type="daterange" value-format="timestamp" | |
| 112 | + format="yyyy-MM-dd" start-placeholder="开始" end-placeholder="结束" | |
| 113 | + style="width: 100%"> | |
| 114 | + </el-date-picker> | |
| 115 | + </el-form-item> | |
| 116 | + </el-col> | |
| 117 | + </el-row> | |
| 118 | + </div> | |
| 95 | 119 | </el-form> |
| 96 | - </el-row> | |
| 120 | + </el-card> | |
| 97 | 121 | <div class="NCC-common-layout-main NCC-flex-main"> |
| 98 | - <div class="NCC-common-head"> | |
| 99 | - <div> | |
| 100 | - <el-button type="primary" icon="el-icon-plus" @click="addOrUpdateHandle()">新增</el-button> | |
| 101 | - <!-- <el-button type="text" icon="el-icon-download" @click="exportData()">导出</el-button> --> | |
| 102 | - <el-button type="text" icon="el-icon-download" :loading="exportLoading" | |
| 103 | - @click="exportMemberItems()">导出</el-button> | |
| 104 | - <el-button v-has="'btn_remove'" type="text" icon="el-icon-delete" | |
| 105 | - @click="handleBatchRemoveDel()">批量删除</el-button> | |
| 106 | - </div> | |
| 107 | - <div class="NCC-common-head-right"> | |
| 108 | - <div style="display: inline-block; margin-right: 15px;"> | |
| 109 | - <el-button size="mini" :type="viewMode === 'list' ? 'primary' : ''" icon="el-icon-s-data" | |
| 110 | - @click="switchView('list')">列表</el-button> | |
| 111 | - <el-button size="mini" :type="viewMode === 'card' ? 'primary' : ''" icon="el-icon-menu" | |
| 112 | - @click="switchView('card')">卡片</el-button> | |
| 122 | + <!-- 现代化操作栏 --> | |
| 123 | + <el-card class="toolbar-card" shadow="never"> | |
| 124 | + <div class="toolbar-container"> | |
| 125 | + <div class="toolbar-left"> | |
| 126 | + <el-button-group> | |
| 127 | + <el-button type="primary" icon="el-icon-plus" | |
| 128 | + @click="addOrUpdateHandle()">新增会员</el-button> | |
| 129 | + <el-button icon="el-icon-download" :loading="exportLoading" | |
| 130 | + @click="exportMemberItems()">导出数据</el-button> | |
| 131 | + <el-button v-has="'btn_remove'" type="danger" icon="el-icon-delete" plain | |
| 132 | + @click="handleBatchRemoveDel()">批量删除</el-button> | |
| 133 | + </el-button-group> | |
| 134 | + </div> | |
| 135 | + <div class="toolbar-right"> | |
| 136 | + <el-radio-group v-model="viewMode" size="small" @change="switchView"> | |
| 137 | + <el-radio-button label="list"> | |
| 138 | + <i class="el-icon-s-grid"></i> 列表 | |
| 139 | + </el-radio-button> | |
| 140 | + <el-radio-button label="card"> | |
| 141 | + <i class="el-icon-menu"></i> 卡片 | |
| 142 | + </el-radio-button> | |
| 143 | + </el-radio-group> | |
| 144 | + <el-divider direction="vertical"></el-divider> | |
| 145 | + <el-button size="small" icon="el-icon-refresh" circle @click="reset()" | |
| 146 | + title="刷新"></el-button> | |
| 147 | + <screenfull isContainer /> | |
| 113 | 148 | </div> |
| 114 | - <el-tooltip effect="dark" content="刷新" placement="top"> | |
| 115 | - <el-link icon="icon-ym icon-ym-Refresh NCC-common-head-icon" :underline="false" | |
| 116 | - @click="reset()" /> | |
| 117 | - </el-tooltip> | |
| 118 | - <screenfull isContainer /> | |
| 119 | 149 | </div> |
| 120 | - </div> | |
| 150 | + </el-card> | |
| 121 | 151 | <div v-if="viewMode === 'card'" v-loading="listLoading" class="member-card-wrapper"> |
| 122 | 152 | <div class="member-card-scroll-container"> |
| 123 | - <el-row :gutter="16" type="flex" style="flex-wrap: wrap;" class="member-card-row"> | |
| 124 | - <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" v-for="item in list" :key="item.id"> | |
| 153 | + <el-row :gutter="16" type="flex" style="flex-wrap: wrap;" class="member-card-row"> | |
| 154 | + <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" v-for="item in list" :key="item.id"> | |
| 125 | 155 | <el-card shadow="hover" class="member-card" :class="`level-card-${item.consumeLevel}`"> |
| 126 | - <div class="card-header-wrapper"> | |
| 127 | - <div class="header-top"> | |
| 128 | - <div class="info-left"> | |
| 129 | - <span class="member-name">{{ item.khmc || '无名氏' }}</span> | |
| 130 | - <i class="el-icon-user gender-icon" :class="getGenderClass(item.xb)"></i> | |
| 156 | + <div class="card-header-wrapper"> | |
| 157 | + <div class="header-top"> | |
| 158 | + <div class="info-left"> | |
| 159 | + <span class="member-name">{{ item.khmc || '无名氏' }}</span> | |
| 160 | + <i class="el-icon-user gender-icon" | |
| 161 | + :class="getGenderClass(item.xb)"></i> | |
| 162 | + </div> | |
| 163 | + <el-tag :type="getConsumeLevelType(item.consumeLevel)" size="small" | |
| 164 | + class="level-tag" effect="dark"> | |
| 165 | + {{ getConsumeLevelName(item.consumeLevel) }} | |
| 166 | + </el-tag> | |
| 131 | 167 | </div> |
| 132 | - <el-tag :type="getConsumeLevelType(item.consumeLevel)" size="small" | |
| 133 | - class="level-tag" effect="dark"> | |
| 134 | - {{ getConsumeLevelName(item.consumeLevel) }} | |
| 135 | - </el-tag> | |
| 136 | - </div> | |
| 137 | - <div class="header-tags"> | |
| 138 | - <el-tag v-if="item.isBeautyMember === 1" type="success" size="mini" | |
| 139 | - effect="plain" class="mini-tag">生美</el-tag> | |
| 140 | - <el-tag v-if="item.isMedicalMember === 1" type="warning" size="mini" | |
| 141 | - effect="plain" class="mini-tag">医美</el-tag> | |
| 142 | - <el-tag v-if="item.isTechMember === 1" type="info" size="mini" effect="plain" | |
| 143 | - class="mini-tag">科技</el-tag> | |
| 144 | - <el-tag v-if="item.isEducationMember === 1" type="primary" size="mini" | |
| 145 | - effect="plain" class="mini-tag">教育</el-tag> | |
| 146 | - </div> | |
| 147 | - </div> | |
| 148 | - | |
| 149 | - <div class="card-content"> | |
| 150 | - <div class="data-grid"> | |
| 151 | - <div class="data-item"> | |
| 152 | - <span class="label"> | |
| 153 | - <i class="el-icon-phone icon-inline"></i>手机号 | |
| 154 | - </span> | |
| 155 | - <span class="value">{{ item.sjh || '-' }}</span> | |
| 168 | + <div class="header-tags"> | |
| 169 | + <el-tag v-if="item.isBeautyMember === 1" type="success" size="mini" | |
| 170 | + effect="plain" class="mini-tag">生美</el-tag> | |
| 171 | + <el-tag v-if="item.isMedicalMember === 1" type="warning" size="mini" | |
| 172 | + effect="plain" class="mini-tag">医美</el-tag> | |
| 173 | + <el-tag v-if="item.isTechMember === 1" type="info" size="mini" | |
| 174 | + effect="plain" class="mini-tag">科技</el-tag> | |
| 175 | + <el-tag v-if="item.isEducationMember === 1" type="primary" size="mini" | |
| 176 | + effect="plain" class="mini-tag">教育</el-tag> | |
| 156 | 177 | </div> |
| 157 | - <div class="data-item"> | |
| 158 | - <span class="label"> | |
| 159 | - <i class="el-icon-shop icon-inline"></i>归属门店 | |
| 160 | - </span> | |
| 161 | - <span class="value text-truncate" :title="item.gsmdName">{{ item.gsmdName || | |
| 162 | - '-' | |
| 178 | + </div> | |
| 179 | + | |
| 180 | + <div class="card-content"> | |
| 181 | + <div class="data-grid"> | |
| 182 | + <div class="data-item"> | |
| 183 | + <span class="label"> | |
| 184 | + <i class="el-icon-phone icon-inline"></i>手机号 | |
| 185 | + </span> | |
| 186 | + <span class="value">{{ item.sjh || '-' }}</span> | |
| 187 | + </div> | |
| 188 | + <div class="data-item"> | |
| 189 | + <span class="label"> | |
| 190 | + <i class="el-icon-shop icon-inline"></i>归属门店 | |
| 191 | + </span> | |
| 192 | + <span class="value text-truncate" :title="item.gsmdName">{{ | |
| 193 | + item.gsmdName || | |
| 194 | + '-' | |
| 163 | 195 | }}</span> |
| 164 | - </div> | |
| 165 | - <div class="data-item"> | |
| 166 | - <span class="label"> | |
| 167 | - <i class="el-icon-user icon-inline"></i>客户类型 | |
| 168 | - </span> | |
| 169 | - <span class="value">{{ item.khlxName || '-' }}</span> | |
| 170 | - </div> | |
| 171 | - <div class="data-item"> | |
| 172 | - <span class="label"> | |
| 173 | - <i class="el-icon-time icon-inline" :class="{ 'icon-warning': item.sleepDays > 0 }"></i>沉睡天数 | |
| 174 | - </span> | |
| 175 | - <span class="value" :class="{ 'text-danger': item.sleepDays > 0 }">{{ | |
| 176 | - item.sleepDays || 0 | |
| 196 | + </div> | |
| 197 | + <div class="data-item"> | |
| 198 | + <span class="label"> | |
| 199 | + <i class="el-icon-user icon-inline"></i>客户类型 | |
| 200 | + </span> | |
| 201 | + <span class="value">{{ item.khlxName || '-' }}</span> | |
| 202 | + </div> | |
| 203 | + <div class="data-item"> | |
| 204 | + <span class="label"> | |
| 205 | + <i class="el-icon-time icon-inline" | |
| 206 | + :class="{ 'icon-warning': item.sleepDays > 0 }"></i>沉睡天数 | |
| 207 | + </span> | |
| 208 | + <span class="value" :class="{ 'text-danger': item.sleepDays > 0 }">{{ | |
| 209 | + item.sleepDays || 0 | |
| 177 | 210 | }}天</span> |
| 211 | + </div> | |
| 212 | + <div class="data-item full-width"> | |
| 213 | + <span class="label"> | |
| 214 | + <i class="el-icon-calendar icon-inline"></i>最后到店 | |
| 215 | + </span> | |
| 216 | + <span class="value">{{ formatDate(item.lastVisitTime) }}</span> | |
| 217 | + </div> | |
| 178 | 218 | </div> |
| 179 | - <div class="data-item full-width"> | |
| 180 | - <span class="label"> | |
| 181 | - <i class="el-icon-calendar icon-inline"></i>最后到店 | |
| 182 | - </span> | |
| 183 | - <span class="value">{{ formatDate(item.lastVisitTime) }}</span> | |
| 184 | - </div> | |
| 185 | - </div> | |
| 186 | 219 | |
| 187 | - <div class="amount-section"> | |
| 188 | - <div class="amount-box"> | |
| 189 | - <span class="sub-label"> | |
| 190 | - <i class="el-icon-wallet icon-inline"></i>开卡金额 | |
| 191 | - </span> | |
| 192 | - <span class="sub-value primary-color">{{ | |
| 193 | - formatMoney(item.totalBillingAmount) }}</span> | |
| 194 | - </div> | |
| 195 | - <div class="divider"></div> | |
| 196 | - <div class="amount-box"> | |
| 197 | - <span class="sub-label"> | |
| 198 | - <i class="el-icon-coin icon-inline"></i>剩余权益 | |
| 199 | - </span> | |
| 200 | - <span class="sub-value warning-color">{{ | |
| 201 | - formatMoney(item.remainingRightsAmount) | |
| 220 | + <div class="amount-section"> | |
| 221 | + <div class="amount-box"> | |
| 222 | + <span class="sub-label"> | |
| 223 | + <i class="el-icon-wallet icon-inline"></i>开卡金额 | |
| 224 | + </span> | |
| 225 | + <span class="sub-value primary-color">{{ | |
| 226 | + formatMoney(item.totalBillingAmount) }}</span> | |
| 227 | + </div> | |
| 228 | + <div class="divider"></div> | |
| 229 | + <div class="amount-box"> | |
| 230 | + <span class="sub-label"> | |
| 231 | + <i class="el-icon-coin icon-inline"></i>剩余权益 | |
| 232 | + </span> | |
| 233 | + <span class="sub-value warning-color">{{ | |
| 234 | + formatMoney(item.remainingRightsAmount) | |
| 202 | 235 | }}</span> |
| 236 | + </div> | |
| 203 | 237 | </div> |
| 204 | 238 | </div> |
| 205 | - </div> | |
| 206 | - | |
| 207 | - <div class="card-footer"> | |
| 208 | - <el-tooltip content="详情" placement="top"> | |
| 209 | - <el-button type="text" icon="el-icon-view" class="action-btn view-btn" | |
| 210 | - @click="openMemberPortrait(item.id)"> | |
| 211 | - 详情 | |
| 212 | - </el-button> | |
| 213 | - </el-tooltip> | |
| 214 | - <el-tooltip content="权益" placement="top"> | |
| 215 | - <el-button type="text" icon="el-icon-folder-opened" | |
| 216 | - class="action-btn rights-btn" @click="showMemberRights(item.id)"> | |
| 217 | - 权益 | |
| 218 | - </el-button> | |
| 219 | - </el-tooltip> | |
| 220 | - <div class="more-actions"> | |
| 221 | - <el-dropdown trigger="click" @command="handleCommand($event, item)"> | |
| 222 | - <span class="el-dropdown-link"> | |
| 223 | - 更多<i class="el-icon-arrow-down el-icon--right"></i> | |
| 224 | - </span> | |
| 225 | - <el-dropdown-menu slot="dropdown"> | |
| 226 | - <el-dropdown-item command="edit" icon="el-icon-edit" | |
| 227 | - v-if="has('btn_edit')">编辑</el-dropdown-item> | |
| 228 | - <el-dropdown-item command="delete" icon="el-icon-delete" | |
| 229 | - v-if="has('btn_remove')" | |
| 230 | - style="color: #F56C6C;">删除</el-dropdown-item> | |
| 231 | - </el-dropdown-menu> | |
| 232 | - </el-dropdown> | |
| 239 | + | |
| 240 | + <div class="card-footer"> | |
| 241 | + <el-tooltip content="详情" placement="top"> | |
| 242 | + <el-button type="text" icon="el-icon-view" class="action-btn view-btn" | |
| 243 | + @click="openMemberPortrait(item.id)"> | |
| 244 | + 详情 | |
| 245 | + </el-button> | |
| 246 | + </el-tooltip> | |
| 247 | + <el-tooltip content="权益" placement="top"> | |
| 248 | + <el-button type="text" icon="el-icon-folder-opened" | |
| 249 | + class="action-btn rights-btn" @click="showMemberRights(item.id)"> | |
| 250 | + 权益 | |
| 251 | + </el-button> | |
| 252 | + </el-tooltip> | |
| 253 | + <div class="more-actions"> | |
| 254 | + <el-dropdown trigger="click" @command="handleCommand($event, item)"> | |
| 255 | + <span class="el-dropdown-link"> | |
| 256 | + 更多<i class="el-icon-arrow-down el-icon--right"></i> | |
| 257 | + </span> | |
| 258 | + <el-dropdown-menu slot="dropdown"> | |
| 259 | + <el-dropdown-item command="edit" icon="el-icon-edit" | |
| 260 | + v-if="has('btn_edit')">编辑</el-dropdown-item> | |
| 261 | + <el-dropdown-item command="delete" icon="el-icon-delete" | |
| 262 | + v-if="has('btn_remove')" | |
| 263 | + style="color: #F56C6C;">删除</el-dropdown-item> | |
| 264 | + </el-dropdown-menu> | |
| 265 | + </el-dropdown> | |
| 266 | + </div> | |
| 233 | 267 | </div> |
| 234 | - </div> | |
| 235 | - </el-card> | |
| 268 | + </el-card> | |
| 236 | 269 | </el-col> |
| 237 | 270 | </el-row> |
| 238 | 271 | <div v-if="list.length === 0" class="empty-data"> |
| ... | ... | @@ -240,217 +273,175 @@ |
| 240 | 273 | </div> |
| 241 | 274 | </div> |
| 242 | 275 | </div> |
| 243 | - <NCC-table v-if="viewMode === 'list'" v-loading="listLoading" :data="list" has-c | |
| 276 | + <NCC-table v-if="viewMode === 'list'" v-loading="listLoading" :data="list" | |
| 244 | 277 | @selection-change="handleSelectionChange" |
| 245 | - :header-cell-style="{ background: '#f5f7fa', color: '#606266' }"> | |
| 246 | - <!-- 客户名称 --> | |
| 247 | - <el-table-column label="客户名称" align="center" width="120px" fixed="left"> | |
| 278 | + :header-cell-style="{ background: '#fafafa', color: '#262626', fontWeight: '600' }"> | |
| 279 | + <!-- 客户id --> | |
| 280 | + <el-table-column label="客户ID" align="left" width="150px" fixed="left"> | |
| 248 | 281 | <template slot-scope="scope"> |
| 249 | - <div class="customer-name-info"> | |
| 250 | - <i class="el-icon-user-solid customer-name-icon"></i> | |
| 251 | - <span class="text-nowrap">{{ scope.row.khmc || '无' }}</span> | |
| 252 | - </div> | |
| 282 | + <span class="cell-text-plain muted">{{ scope.row.id || '无' }}</span> | |
| 253 | 283 | </template> |
| 254 | 284 | </el-table-column> |
| 255 | - | |
| 256 | - <!-- 手机号 --> | |
| 257 | - <el-table-column label="手机号" align="center" width="120px" fixed="left"> | |
| 285 | + <!-- 客户名称 --> | |
| 286 | + <el-table-column label="客户名称" align="left" width="100px" fixed="left"> | |
| 258 | 287 | <template slot-scope="scope"> |
| 259 | - <div class="phone-info"> | |
| 260 | - <i class="el-icon-phone phone-icon"></i> | |
| 261 | - <span class="text-nowrap">{{ scope.row.sjh || '无' }}</span> | |
| 288 | + <div class="table-cell-with-icon"> | |
| 289 | + <i class="el-icon-user-solid cell-icon primary"></i> | |
| 290 | + <span class="cell-text">{{ scope.row.khmc || '无' }}</span> | |
| 262 | 291 | </div> |
| 263 | 292 | </template> |
| 264 | 293 | </el-table-column> |
| 265 | - <!-- 客户id --> | |
| 266 | - <el-table-column label="客户id" align="center" width="180px"> | |
| 294 | + | |
| 295 | + <!-- 手机号 --> | |
| 296 | + <el-table-column label="手机号" align="left" width="130px" fixed="left"> | |
| 267 | 297 | <template slot-scope="scope"> |
| 268 | - <div class="customer-code-info"> | |
| 269 | - <i class="el-icon-postcard customer-code-icon"></i> | |
| 270 | - <span class="text-nowrap">{{ scope.row.id || '无' }}</span> | |
| 298 | + <div class="table-cell-with-icon"> | |
| 299 | + <i class="el-icon-mobile-phone cell-icon success"></i> | |
| 300 | + <span class="cell-text">{{ scope.row.sjh || '无' }}</span> | |
| 271 | 301 | </div> |
| 272 | 302 | </template> |
| 273 | 303 | </el-table-column> |
| 304 | + | |
| 274 | 305 | <!-- 档案号 --> |
| 275 | - <el-table-column label="档案号" align="center" width="180px"> | |
| 306 | + <el-table-column label="档案号" align="left" width="150px"> | |
| 276 | 307 | <template slot-scope="scope"> |
| 277 | - <div class="customer-code-info"> | |
| 278 | - <i class="el-icon-postcard customer-code-icon"></i> | |
| 279 | - <span class="text-nowrap">{{ scope.row.dah || '无' }}</span> | |
| 280 | - </div> | |
| 308 | + <span class="cell-text-plain">{{ scope.row.dah || '无' }}</span> | |
| 281 | 309 | </template> |
| 282 | 310 | </el-table-column> |
| 283 | 311 | |
| 284 | 312 | |
| 285 | 313 | <!-- 性别 --> |
| 286 | - <el-table-column label="性别" align="center"> | |
| 314 | + <el-table-column label="性别" align="center" width="70px"> | |
| 287 | 315 | <template slot-scope="scope"> |
| 288 | - <div class="gender-info"> | |
| 289 | - <i class="el-icon-user gender-icon" :class="getGenderClass(scope.row.xb)"></i> | |
| 290 | - <span class="text-nowrap">{{ scope.row.xb | dynamicText(xbOptions) || '无' }}</span> | |
| 291 | - </div> | |
| 316 | + <el-tag :type="scope.row.xb === '男' ? 'primary' : scope.row.xb === '女' ? 'danger' : 'info'" | |
| 317 | + size="small" effect="plain"> | |
| 318 | + {{ scope.row.xb | dynamicText(xbOptions) || '未知' }} | |
| 319 | + </el-tag> | |
| 292 | 320 | </template> |
| 293 | 321 | </el-table-column> |
| 294 | 322 | |
| 295 | 323 | <!-- 归属门店 --> |
| 296 | - <el-table-column label="归属门店" align="center" width="140px"> | |
| 324 | + <el-table-column label="归属门店" align="left" width="120px"> | |
| 297 | 325 | <template slot-scope="scope"> |
| 298 | - <div class="store-info"> | |
| 299 | - <i class="el-icon-office-building store-icon"></i> | |
| 300 | - <span class="text-nowrap">{{ scope.row.gsmdName || '无' }}</span> | |
| 326 | + <div class="table-cell-with-icon"> | |
| 327 | + <i class="el-icon-office-building cell-icon info"></i> | |
| 328 | + <span class="cell-text">{{ scope.row.gsmdName || '无' }}</span> | |
| 301 | 329 | </div> |
| 302 | 330 | </template> |
| 303 | 331 | </el-table-column> |
| 304 | 332 | |
| 305 | 333 | <!-- 客户类型 --> |
| 306 | - <el-table-column label="客户类型" align="center"> | |
| 334 | + <el-table-column label="客户类型" align="center" width="100px"> | |
| 307 | 335 | <template slot-scope="scope"> |
| 308 | - <div class="customer-type-info"> | |
| 309 | - <i class="el-icon-user customer-type-icon"></i> | |
| 310 | - <span class="text-nowrap">{{ scope.row.khlxName }}</span> | |
| 311 | - </div> | |
| 336 | + <span class="cell-text-plain">{{ scope.row.khlxName || '无' }}</span> | |
| 312 | 337 | </template> |
| 313 | 338 | </el-table-column> |
| 314 | 339 | <!-- 健康师 --> |
| 315 | - <el-table-column label="健康师" align="center"> | |
| 340 | + <el-table-column label="健康师" align="left" width="70px"> | |
| 316 | 341 | <template slot-scope="scope"> |
| 317 | - <div class="beautician-info"> | |
| 318 | - <i class="el-icon-user beautician-icon"></i> | |
| 319 | - <span class="text-nowrap">{{ scope.row.mainHealthUserName || '无' }}</span> | |
| 320 | - </div> | |
| 342 | + <span class="cell-text-plain">{{ scope.row.mainHealthUserName || '无' }}</span> | |
| 321 | 343 | </template> |
| 322 | 344 | </el-table-column> |
| 323 | 345 | <!-- 负责顾问 --> |
| 324 | - <el-table-column label="负责顾问" align="center"> | |
| 346 | + <el-table-column label="负责顾问" align="left" width="80px"> | |
| 325 | 347 | <template slot-scope="scope"> |
| 326 | - <div class="advisor-info"> | |
| 327 | - <i class="el-icon-service advisor-icon"></i> | |
| 328 | - <span class="text-nowrap">{{ scope.row.subHealthUserName || '无' }}</span> | |
| 329 | - </div> | |
| 348 | + <span class="cell-text-plain">{{ scope.row.subHealthUserName || '无' }}</span> | |
| 330 | 349 | </template> |
| 331 | 350 | </el-table-column> |
| 332 | 351 | <!-- 进店渠道 --> |
| 333 | - <el-table-column label="进店渠道" align="center"> | |
| 352 | + <el-table-column label="进店渠道" align="left" width="100px"> | |
| 334 | 353 | <template slot-scope="scope"> |
| 335 | - <div class="channel-info"> | |
| 336 | - <i class="el-icon-connection channel-icon"></i> | |
| 337 | - <span class="text-nowrap">{{ scope.row.jdqd || '无' }}</span> | |
| 338 | - </div> | |
| 354 | + <span class="cell-text-plain">{{ scope.row.jdqd || '无' }}</span> | |
| 339 | 355 | </template> |
| 340 | 356 | </el-table-column> |
| 341 | 357 | <!-- 推荐人 --> |
| 342 | - <el-table-column label="推荐人" align="center"> | |
| 358 | + <el-table-column label="推荐人" align="left" width="80px"> | |
| 343 | 359 | <template slot-scope="scope"> |
| 344 | - <div class="referrer-info"> | |
| 345 | - <i class="el-icon-user referrer-icon"></i> | |
| 346 | - <span class="text-nowrap">{{ scope.row.tjrName || '无' }}</span> | |
| 347 | - </div> | |
| 360 | + <span class="cell-text-plain">{{ scope.row.tjrName || '无' }}</span> | |
| 348 | 361 | </template> |
| 349 | 362 | </el-table-column> |
| 350 | 363 | <!-- 拓客人员 --> |
| 351 | - <el-table-column label="拓客人员" align="center"> | |
| 364 | + <el-table-column label="拓客人员" align="left" width="80px"> | |
| 352 | 365 | <template slot-scope="scope"> |
| 353 | - <div class="referrer-info"> | |
| 354 | - <i class="el-icon-user referrer-icon"></i> | |
| 355 | - <span class="text-nowrap">{{ scope.row.expandUserName || '无' }}</span> | |
| 356 | - </div> | |
| 366 | + <span class="cell-text-plain">{{ scope.row.expandUserName || '无' }}</span> | |
| 357 | 367 | </template> |
| 358 | 368 | </el-table-column> |
| 359 | 369 | |
| 360 | 370 | <!-- 会员类型 --> |
| 361 | - <el-table-column label="会员类型" align="center" min-width="120"> | |
| 371 | + <el-table-column label="会员类型" align="center" min-width="130"> | |
| 362 | 372 | <template slot-scope="scope"> |
| 363 | - <div class="member-type-info"> | |
| 373 | + <div class="member-tags"> | |
| 364 | 374 | <el-tag v-if="scope.row.isBeautyMember === 1" type="success" size="mini" |
| 365 | - style="margin-right: 4px;">生美</el-tag> | |
| 375 | + effect="plain">生美</el-tag> | |
| 366 | 376 | <el-tag v-if="scope.row.isMedicalMember === 1" type="warning" size="mini" |
| 367 | - style="margin-right: 4px;">医美</el-tag> | |
| 377 | + effect="plain">医美</el-tag> | |
| 368 | 378 | <el-tag v-if="scope.row.isTechMember === 1" type="info" size="mini" |
| 369 | - style="margin-right: 4px;">科技部</el-tag> | |
| 370 | - <el-tag v-if="scope.row.isEducationMember === 1" type="primary" size="mini">教育部</el-tag> | |
| 379 | + effect="plain">科技</el-tag> | |
| 380 | + <el-tag v-if="scope.row.isEducationMember === 1" type="primary" size="mini" | |
| 381 | + effect="plain">教育</el-tag> | |
| 371 | 382 | <span |
| 372 | 383 | v-if="scope.row.isBeautyMember !== 1 && scope.row.isMedicalMember !== 1 && scope.row.isTechMember !== 1 && scope.row.isEducationMember !== 1" |
| 373 | - class="text-nowrap">无</span> | |
| 384 | + class="cell-text-plain muted">无</span> | |
| 374 | 385 | </div> |
| 375 | 386 | </template> |
| 376 | 387 | </el-table-column> |
| 377 | 388 | |
| 378 | 389 | <!-- 消费等级 --> |
| 379 | - <el-table-column label="消费等级" align="center"> | |
| 390 | + <el-table-column label="消费等级" align="center" width="90px"> | |
| 380 | 391 | <template slot-scope="scope"> |
| 381 | - <div class="consume-level-info"> | |
| 382 | - <i class="el-icon-trophy consume-level-icon"></i> | |
| 383 | - <el-tag :type="getConsumeLevelType(scope.row.consumeLevel)" size="mini"> | |
| 384 | - {{ getConsumeLevelName(scope.row.consumeLevel) }} | |
| 385 | - </el-tag> | |
| 386 | - </div> | |
| 392 | + <el-tag :type="getConsumeLevelType(scope.row.consumeLevel)" size="small" effect="dark"> | |
| 393 | + {{ getConsumeLevelName(scope.row.consumeLevel) }} | |
| 394 | + </el-tag> | |
| 387 | 395 | </template> |
| 388 | 396 | </el-table-column> |
| 389 | 397 | |
| 390 | 398 | <!-- 开卡总金额 --> |
| 391 | - <el-table-column label="开卡总金额" align="center" width="150"> | |
| 399 | + <el-table-column label="开卡总金额" align="right" width="120"> | |
| 392 | 400 | <template slot-scope="scope"> |
| 393 | - <div class="amount-info"> | |
| 394 | - <i class="el-icon-wallet amount-icon"></i> | |
| 395 | - <span class="text-nowrap amount-value">{{ formatMoney(scope.row.totalBillingAmount) | |
| 396 | - }}</span> | |
| 397 | - </div> | |
| 401 | + <span class="amount-text primary">{{ formatMoney(scope.row.totalBillingAmount) }}</span> | |
| 398 | 402 | </template> |
| 399 | 403 | </el-table-column> |
| 400 | 404 | |
| 401 | 405 | <!-- 剩余权益总金额 --> |
| 402 | - <el-table-column label="剩余权益总金额" align="center" width="150"> | |
| 406 | + <el-table-column label="剩余权益" align="right" width="120"> | |
| 403 | 407 | <template slot-scope="scope"> |
| 404 | - <div class="amount-info"> | |
| 405 | - <i class="el-icon-coin amount-icon"></i> | |
| 406 | - <span class="text-nowrap amount-value">{{ formatMoney(scope.row.remainingRightsAmount) | |
| 407 | - }}</span> | |
| 408 | - </div> | |
| 408 | + <span class="amount-text success">{{ formatMoney(scope.row.remainingRightsAmount) }}</span> | |
| 409 | 409 | </template> |
| 410 | 410 | </el-table-column> |
| 411 | 411 | |
| 412 | 412 | <!-- 首次到店时间 --> |
| 413 | - <el-table-column label="首次到店时间" align="center" width="140"> | |
| 413 | + <el-table-column label="首次到店" align="left" width="110"> | |
| 414 | 414 | <template slot-scope="scope"> |
| 415 | - <div class="time-info"> | |
| 416 | - <i class="el-icon-time time-icon"></i> | |
| 417 | - <span class="text-nowrap">{{ formatDateTime(scope.row.firstVisitTime) || '无' }}</span> | |
| 418 | - </div> | |
| 415 | + <span class="cell-text-plain muted">{{ formatDate(scope.row.firstVisitTime) || '无' }}</span> | |
| 419 | 416 | </template> |
| 420 | 417 | </el-table-column> |
| 421 | 418 | |
| 422 | 419 | <!-- 最后到店时间 --> |
| 423 | - <el-table-column label="最后到店时间" align="center" width="140"> | |
| 420 | + <el-table-column label="最后到店" align="left" width="110"> | |
| 424 | 421 | <template slot-scope="scope"> |
| 425 | - <div class="time-info"> | |
| 426 | - <i class="el-icon-time time-icon"></i> | |
| 427 | - <span class="text-nowrap">{{ formatDateTime(scope.row.lastVisitTime) || '无' }}</span> | |
| 428 | - </div> | |
| 422 | + <span class="cell-text-plain muted">{{ formatDate(scope.row.lastVisitTime) || '无' }}</span> | |
| 429 | 423 | </template> |
| 430 | 424 | </el-table-column> |
| 431 | 425 | |
| 432 | 426 | <!-- 到店天数 --> |
| 433 | - <el-table-column label="到店天数" align="center"> | |
| 427 | + <el-table-column label="到店天数" align="center" width="90px"> | |
| 434 | 428 | <template slot-scope="scope"> |
| 435 | - <div class="days-info"> | |
| 436 | - <i class="el-icon-calendar days-icon"></i> | |
| 437 | - <span class="text-nowrap">{{ scope.row.visitDays || 0 }}天</span> | |
| 438 | - </div> | |
| 429 | + <el-tag size="small" type="info" effect="plain">{{ scope.row.visitDays || 0 }}天</el-tag> | |
| 439 | 430 | </template> |
| 440 | 431 | </el-table-column> |
| 441 | 432 | |
| 442 | 433 | <!-- 沉睡天数 --> |
| 443 | - <el-table-column label="沉睡天数" align="center"> | |
| 434 | + <el-table-column label="沉睡天数" align="center" width="90px"> | |
| 444 | 435 | <template slot-scope="scope"> |
| 445 | - <div class="sleep-days-info" :class="{ 'sleep-warning': scope.row.sleepDays > 0 }"> | |
| 446 | - <i class="el-icon-moon sleep-icon"></i> | |
| 447 | - <span class="text-nowrap">{{ scope.row.sleepDays || 0 }}天</span> | |
| 448 | - </div> | |
| 436 | + <el-tag size="small" :type="scope.row.sleepDays > 30 ? 'warning' : 'success'" | |
| 437 | + effect="plain"> | |
| 438 | + {{ scope.row.sleepDays || 0 }}天 | |
| 439 | + </el-tag> | |
| 449 | 440 | </template> |
| 450 | 441 | </el-table-column> |
| 451 | 442 | |
| 452 | 443 | <!-- 操作 --> |
| 453 | - <el-table-column label="操作" width="280" align="left" fixed="right"> | |
| 444 | + <el-table-column label="操作" width="320" align="left" fixed="right"> | |
| 454 | 445 | <template slot-scope="scope"> |
| 455 | 446 | <div class="action-buttons"> |
| 456 | 447 | <el-button type="text" class="detail-btn" icon="el-icon-view" |
| ... | ... | @@ -472,12 +463,8 @@ |
| 472 | 463 | </template> |
| 473 | 464 | </el-table-column> |
| 474 | 465 | </NCC-table> |
| 475 | - <pagination | |
| 476 | - :total="total" | |
| 477 | - :page.sync="listQuery.currentPage" | |
| 478 | - :limit.sync="listQuery.pageSize" | |
| 479 | - @pagination="initData" | |
| 480 | - :class="{ 'pagination-card-mode': viewMode === 'card' }" /> | |
| 466 | + <pagination :total="total" :page.sync="listQuery.currentPage" :limit.sync="listQuery.pageSize" | |
| 467 | + @pagination="initData" :class="{ 'pagination-card-mode': viewMode === 'card' }" /> | |
| 481 | 468 | </div> |
| 482 | 469 | </div> |
| 483 | 470 | <NCC-Form v-if="formVisible" ref="NCCForm" @refresh="refresh" /> |
| ... | ... | @@ -503,6 +490,7 @@ export default { |
| 503 | 490 | return { |
| 504 | 491 | viewMode: 'list', // list or card |
| 505 | 492 | showAll: false, |
| 493 | + searchCardMounted: false, // 控制筛选卡片动画 | |
| 506 | 494 | kdhyLoading: false, |
| 507 | 495 | memberRightsDialogVisible: false, |
| 508 | 496 | detailDialogVisible: false, |
| ... | ... | @@ -536,6 +524,8 @@ export default { |
| 536 | 524 | yanglsr: undefined, |
| 537 | 525 | yinlsr: undefined, |
| 538 | 526 | ml: undefined, |
| 527 | + consumeLevel: undefined, // 消费等级 | |
| 528 | + sleepDaysRange: undefined, // 沉睡天数范围 | |
| 539 | 529 | }, |
| 540 | 530 | list: [], |
| 541 | 531 | listLoading: true, |
| ... | ... | @@ -600,6 +590,21 @@ export default { |
| 600 | 590 | khmqgsOptions: [{ "fullName": "会员", "id": "会员" }, { "fullName": "线索池", "id": "线索池" }, { "fullName": "会员-归档", "id": "会员-归档" }], |
| 601 | 591 | khlxOptions: [], |
| 602 | 592 | khjdOptions: [{ "fullName": "体验", "id": "体验" }, { "fullName": "有效", "id": "有效" }, { "fullName": "沉睡", "id": "沉睡" }, { "fullName": "流失", "id": "流失" }], |
| 593 | + consumeLevelOptions: [ | |
| 594 | + { "fullName": "D", "id": 0 }, | |
| 595 | + { "fullName": "C", "id": 1 }, | |
| 596 | + { "fullName": "B", "id": 2 }, | |
| 597 | + { "fullName": "A", "id": 3 }, | |
| 598 | + { "fullName": "A+", "id": 4 }, | |
| 599 | + { "fullName": "A++", "id": 5 } | |
| 600 | + ], | |
| 601 | + sleepDaysRangeOptions: [ | |
| 602 | + { label: "0-30天", value: "0-30天", range: "0,30" }, | |
| 603 | + { label: "30-60天", value: "30-60天", range: "30,60" }, | |
| 604 | + { label: "60-90天", value: "60-90天", range: "60,90" }, | |
| 605 | + { label: "90-180天", value: "90-180天", range: "90,180" }, | |
| 606 | + { label: "180天以上", value: "180天以上", range: "180,9999" } | |
| 607 | + ], | |
| 603 | 608 | khxfOptions: [{ "fullName": "D客", "id": "D客" }, { "fullName": "有效", "id": "有效" }], |
| 604 | 609 | xfpcOptions: [{ "fullName": "高频", "id": "高频" }, { "fullName": "低频", "id": "低频" }], |
| 605 | 610 | jdqdOptions: [{ "fullName": "19.9卡", "id": "19.9卡" }, { "fullName": "自然到店", "id": "自然到店" }, { "fullName": "嘉宾", "id": "嘉宾" }, { "fullName": "售后", "id": "售后" }, { "fullName": "直播间", "id": "直播间" }, { "fullName": "转店顾客", "id": "转店顾客" }, { "fullName": "联联", "id": "联联" }, { "fullName": "美团", "id": "美团" }, { "fullName": "三方拓客", "id": "三方拓客" }, { "fullName": "其他", "id": "其他" }], |
| ... | ... | @@ -616,6 +621,14 @@ export default { |
| 616 | 621 | this.getkhlxOptions(); |
| 617 | 622 | this.getkdhyOptions(); |
| 618 | 623 | }, |
| 624 | + mounted() { | |
| 625 | + // 触发筛选卡片的滑动展开动画 | |
| 626 | + this.$nextTick(() => { | |
| 627 | + setTimeout(() => { | |
| 628 | + this.searchCardMounted = true; | |
| 629 | + }, 50); | |
| 630 | + }); | |
| 631 | + }, | |
| 619 | 632 | methods: { |
| 620 | 633 | // 权限检查方法 |
| 621 | 634 | has(enCode) { |
| ... | ... | @@ -720,6 +733,15 @@ export default { |
| 720 | 733 | ...this.listQuery, |
| 721 | 734 | ...this.query |
| 722 | 735 | }; |
| 736 | + | |
| 737 | + // 转换沉睡天数选项值为范围格式 | |
| 738 | + if (_query.sleepDaysRange) { | |
| 739 | + const option = this.sleepDaysRangeOptions.find(opt => opt.value === _query.sleepDaysRange); | |
| 740 | + if (option) { | |
| 741 | + _query.sleepDaysRange = option.range; | |
| 742 | + } | |
| 743 | + } | |
| 744 | + | |
| 723 | 745 | let query = {} |
| 724 | 746 | for (let key in _query) { |
| 725 | 747 | if (Array.isArray(_query[key])) { |
| ... | ... | @@ -803,6 +825,15 @@ export default { |
| 803 | 825 | }, |
| 804 | 826 | download(data) { |
| 805 | 827 | let query = { ...data, ...this.listQuery, ...this.query } |
| 828 | + | |
| 829 | + // 转换沉睡天数选项值为范围格式 | |
| 830 | + if (query.sleepDaysRange) { | |
| 831 | + const option = this.sleepDaysRangeOptions.find(opt => opt.value === query.sleepDaysRange); | |
| 832 | + if (option) { | |
| 833 | + query.sleepDaysRange = option.range; | |
| 834 | + } | |
| 835 | + } | |
| 836 | + | |
| 806 | 837 | request({ |
| 807 | 838 | url: `/api/Extend/LqKhxx/Actions/Export`, |
| 808 | 839 | method: 'GET', |
| ... | ... | @@ -817,6 +848,15 @@ export default { |
| 817 | 848 | // 导出会员品项 |
| 818 | 849 | exportMemberItems() { |
| 819 | 850 | let query = { ...this.listQuery, ...this.query } |
| 851 | + | |
| 852 | + // 转换沉睡天数选项值为范围格式 | |
| 853 | + if (query.sleepDaysRange) { | |
| 854 | + const option = this.sleepDaysRangeOptions.find(opt => opt.value === query.sleepDaysRange); | |
| 855 | + if (option) { | |
| 856 | + query.sleepDaysRange = option.range; | |
| 857 | + } | |
| 858 | + } | |
| 859 | + | |
| 820 | 860 | // 处理数组参数 |
| 821 | 861 | let exportQuery = {} |
| 822 | 862 | for (let key in query) { |
| ... | ... | @@ -1578,22 +1618,22 @@ export default { |
| 1578 | 1618 | min-height: 0; // 重要:允许 flex 子元素收缩 |
| 1579 | 1619 | // 使用渐变背景(为毛玻璃效果提供视觉基础) |
| 1580 | 1620 | background: linear-gradient(135deg, #F8FAFC 0%, #F1F5F9 50%, #E2E8F0 100%); |
| 1581 | - | |
| 1621 | + | |
| 1582 | 1622 | // 自定义滚动条样式 |
| 1583 | 1623 | &::-webkit-scrollbar { |
| 1584 | 1624 | width: 8px; |
| 1585 | 1625 | } |
| 1586 | - | |
| 1626 | + | |
| 1587 | 1627 | &::-webkit-scrollbar-track { |
| 1588 | 1628 | background: rgba(0, 0, 0, 0.05); |
| 1589 | 1629 | border-radius: 4px; |
| 1590 | 1630 | } |
| 1591 | - | |
| 1631 | + | |
| 1592 | 1632 | &::-webkit-scrollbar-thumb { |
| 1593 | 1633 | background: rgba(0, 0, 0, 0.2); |
| 1594 | 1634 | border-radius: 4px; |
| 1595 | 1635 | transition: background 0.2s ease; |
| 1596 | - | |
| 1636 | + | |
| 1597 | 1637 | &:hover { |
| 1598 | 1638 | background: rgba(0, 0, 0, 0.3); |
| 1599 | 1639 | } |
| ... | ... | @@ -1605,7 +1645,7 @@ export default { |
| 1605 | 1645 | display: flex; |
| 1606 | 1646 | } |
| 1607 | 1647 | } |
| 1608 | - | |
| 1648 | + | |
| 1609 | 1649 | .empty-data { |
| 1610 | 1650 | display: flex; |
| 1611 | 1651 | justify-content: center; |
| ... | ... | @@ -1622,16 +1662,14 @@ export default { |
| 1622 | 1662 | // 精致的毛玻璃效果边框(双层边框) |
| 1623 | 1663 | border: 1px solid rgba(255, 255, 255, 0.4); |
| 1624 | 1664 | // 增强的毛玻璃背景 |
| 1625 | - background: linear-gradient( | |
| 1626 | - 135deg, | |
| 1627 | - rgba(255, 255, 255, 0.9) 0%, | |
| 1628 | - rgba(255, 255, 255, 0.8) 50%, | |
| 1629 | - rgba(255, 255, 255, 0.85) 100% | |
| 1630 | - ); | |
| 1665 | + background: linear-gradient(135deg, | |
| 1666 | + rgba(255, 255, 255, 0.9) 0%, | |
| 1667 | + rgba(255, 255, 255, 0.8) 50%, | |
| 1668 | + rgba(255, 255, 255, 0.85) 100%); | |
| 1631 | 1669 | backdrop-filter: blur(24px) saturate(190%); |
| 1632 | 1670 | -webkit-backdrop-filter: blur(24px) saturate(190%); |
| 1633 | 1671 | // 精致的多层次阴影系统(5层阴影,创造更强的深度) |
| 1634 | - box-shadow: | |
| 1672 | + box-shadow: | |
| 1635 | 1673 | 0 2px 4px rgba(0, 0, 0, 0.04), |
| 1636 | 1674 | 0 4px 8px rgba(0, 0, 0, 0.03), |
| 1637 | 1675 | 0 8px 16px rgba(0, 0, 0, 0.02), |
| ... | ... | @@ -1641,6 +1679,7 @@ export default { |
| 1641 | 1679 | overflow: hidden; |
| 1642 | 1680 | position: relative; |
| 1643 | 1681 | cursor: pointer; |
| 1682 | + | |
| 1644 | 1683 | // 添加微妙的渐变遮罩层 |
| 1645 | 1684 | &::after { |
| 1646 | 1685 | content: ''; |
| ... | ... | @@ -1649,12 +1688,10 @@ export default { |
| 1649 | 1688 | left: 0; |
| 1650 | 1689 | right: 0; |
| 1651 | 1690 | bottom: 0; |
| 1652 | - background: linear-gradient( | |
| 1653 | - 135deg, | |
| 1654 | - rgba(255, 255, 255, 0.1) 0%, | |
| 1655 | - transparent 50%, | |
| 1656 | - rgba(0, 0, 0, 0.02) 100% | |
| 1657 | - ); | |
| 1691 | + background: linear-gradient(135deg, | |
| 1692 | + rgba(255, 255, 255, 0.1) 0%, | |
| 1693 | + transparent 50%, | |
| 1694 | + rgba(0, 0, 0, 0.02) 100%); | |
| 1658 | 1695 | pointer-events: none; |
| 1659 | 1696 | opacity: 0; |
| 1660 | 1697 | transition: opacity 0.3s ease; |
| ... | ... | @@ -1668,12 +1705,10 @@ export default { |
| 1668 | 1705 | left: 0; |
| 1669 | 1706 | right: 0; |
| 1670 | 1707 | height: 4px; |
| 1671 | - background: linear-gradient( | |
| 1672 | - 90deg, | |
| 1673 | - #2563EB 0%, | |
| 1674 | - #3B82F6 50%, | |
| 1675 | - #60A5FA 100% | |
| 1676 | - ); | |
| 1708 | + background: linear-gradient(90deg, | |
| 1709 | + #2563EB 0%, | |
| 1710 | + #3B82F6 50%, | |
| 1711 | + #60A5FA 100%); | |
| 1677 | 1712 | opacity: 1; |
| 1678 | 1713 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| 1679 | 1714 | box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3); // 添加发光效果 |
| ... | ... | @@ -1722,16 +1757,14 @@ export default { |
| 1722 | 1757 | &:hover { |
| 1723 | 1758 | transform: translateY(-8px) scale(1.02); |
| 1724 | 1759 | // 增强毛玻璃效果 |
| 1725 | - background: linear-gradient( | |
| 1726 | - 135deg, | |
| 1727 | - rgba(255, 255, 255, 0.95) 0%, | |
| 1728 | - rgba(255, 255, 255, 0.9) 50%, | |
| 1729 | - rgba(255, 255, 255, 0.92) 100% | |
| 1730 | - ); | |
| 1760 | + background: linear-gradient(135deg, | |
| 1761 | + rgba(255, 255, 255, 0.95) 0%, | |
| 1762 | + rgba(255, 255, 255, 0.9) 50%, | |
| 1763 | + rgba(255, 255, 255, 0.92) 100%); | |
| 1731 | 1764 | backdrop-filter: blur(30px) saturate(200%); |
| 1732 | 1765 | -webkit-backdrop-filter: blur(30px) saturate(200%); |
| 1733 | 1766 | // 增强的悬浮阴影(创造浮动感) |
| 1734 | - box-shadow: | |
| 1767 | + box-shadow: | |
| 1735 | 1768 | 0 8px 16px rgba(0, 0, 0, 0.08), |
| 1736 | 1769 | 0 16px 32px rgba(0, 0, 0, 0.06), |
| 1737 | 1770 | 0 32px 64px rgba(0, 0, 0, 0.04), |
| ... | ... | @@ -1745,7 +1778,7 @@ export default { |
| 1745 | 1778 | height: 6px; |
| 1746 | 1779 | box-shadow: 0 4px 16px rgba(37, 99, 235, 0.4); |
| 1747 | 1780 | } |
| 1748 | - | |
| 1781 | + | |
| 1749 | 1782 | &::after { |
| 1750 | 1783 | opacity: 1; // 显示渐变遮罩 |
| 1751 | 1784 | } |
| ... | ... | @@ -1754,7 +1787,7 @@ export default { |
| 1754 | 1787 | // 响应减少动画偏好 |
| 1755 | 1788 | @media (prefers-reduced-motion: reduce) { |
| 1756 | 1789 | transition: none; |
| 1757 | - | |
| 1790 | + | |
| 1758 | 1791 | &:hover { |
| 1759 | 1792 | transform: none; |
| 1760 | 1793 | } |
| ... | ... | @@ -1770,13 +1803,12 @@ export default { |
| 1770 | 1803 | .card-header-wrapper { |
| 1771 | 1804 | padding: 10px 12px 8px; // 缩小卡片头部内边距 |
| 1772 | 1805 | border-bottom: 1px solid rgba(255, 255, 255, 0.3); |
| 1773 | - background: linear-gradient( | |
| 1774 | - 180deg, | |
| 1775 | - rgba(255, 255, 255, 0.4) 0%, | |
| 1776 | - rgba(255, 255, 255, 0.2) 50%, | |
| 1777 | - rgba(255, 255, 255, 0) 100% | |
| 1778 | - ); | |
| 1806 | + background: linear-gradient(180deg, | |
| 1807 | + rgba(255, 255, 255, 0.4) 0%, | |
| 1808 | + rgba(255, 255, 255, 0.2) 50%, | |
| 1809 | + rgba(255, 255, 255, 0) 100%); | |
| 1779 | 1810 | position: relative; |
| 1811 | + | |
| 1780 | 1812 | // 添加微妙的光影效果 |
| 1781 | 1813 | &::after { |
| 1782 | 1814 | content: ''; |
| ... | ... | @@ -1785,12 +1817,10 @@ export default { |
| 1785 | 1817 | left: 16px; |
| 1786 | 1818 | right: 16px; |
| 1787 | 1819 | height: 1px; |
| 1788 | - background: linear-gradient( | |
| 1789 | - 90deg, | |
| 1790 | - transparent 0%, | |
| 1791 | - rgba(255, 255, 255, 0.5) 50%, | |
| 1792 | - transparent 100% | |
| 1793 | - ); | |
| 1820 | + background: linear-gradient(90deg, | |
| 1821 | + transparent 0%, | |
| 1822 | + rgba(255, 255, 255, 0.5) 50%, | |
| 1823 | + transparent 100%); | |
| 1794 | 1824 | } |
| 1795 | 1825 | |
| 1796 | 1826 | .header-top { |
| ... | ... | @@ -1840,44 +1870,40 @@ export default { |
| 1840 | 1870 | .header-tags { |
| 1841 | 1871 | display: flex; |
| 1842 | 1872 | flex-wrap: wrap; |
| 1843 | - gap: 4px; // 缩小标签间距 | |
| 1873 | + gap: 6px; | |
| 1844 | 1874 | min-height: 24px; |
| 1845 | 1875 | |
| 1846 | 1876 | .mini-tag { |
| 1847 | - border-radius: 6px; | |
| 1848 | - padding: 3px 8px; | |
| 1849 | - height: 22px; | |
| 1850 | - line-height: 18px; | |
| 1851 | - font-size: 13px; // 增大标签字体 | |
| 1877 | + border-radius: 10px; | |
| 1878 | + padding: 4px 8px; | |
| 1879 | + height: 20px; | |
| 1880 | + line-height: 12px; | |
| 1881 | + font-size: 11px; | |
| 1852 | 1882 | font-weight: 600; |
| 1853 | - transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); | |
| 1854 | - backdrop-filter: blur(4px); | |
| 1855 | - -webkit-backdrop-filter: blur(4px); | |
| 1856 | - border: 0.5px solid rgba(255, 255, 255, 0.3); | |
| 1857 | - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); | |
| 1883 | + transition: box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| 1884 | + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); | |
| 1885 | + will-change: box-shadow; | |
| 1886 | + transform: translateZ(0); // 启用硬件加速 | |
| 1858 | 1887 | |
| 1859 | 1888 | &:hover { |
| 1860 | - transform: translateY(-1px) scale(1.05); | |
| 1861 | - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12); | |
| 1889 | + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); | |
| 1862 | 1890 | } |
| 1863 | 1891 | } |
| 1864 | 1892 | } |
| 1865 | 1893 | |
| 1866 | 1894 | .level-tag { |
| 1867 | 1895 | font-weight: 700; |
| 1868 | - border-radius: 8px; | |
| 1896 | + border-radius: 12px; | |
| 1869 | 1897 | padding: 5px 12px; |
| 1870 | - font-size: 13px; // 增大等级标签字体 | |
| 1898 | + font-size: 13px; | |
| 1871 | 1899 | letter-spacing: 0.03em; |
| 1872 | - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| 1873 | - backdrop-filter: blur(4px); | |
| 1874 | - -webkit-backdrop-filter: blur(4px); | |
| 1875 | - border: 0.5px solid rgba(255, 255, 255, 0.3); | |
| 1876 | - transition: all 0.25s ease; | |
| 1900 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); | |
| 1901 | + transition: box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| 1902 | + will-change: box-shadow; | |
| 1903 | + transform: translateZ(0); // 启用硬件加速 | |
| 1877 | 1904 | |
| 1878 | 1905 | &:hover { |
| 1879 | - transform: translateY(-1px); | |
| 1880 | - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); | |
| 1906 | + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); | |
| 1881 | 1907 | } |
| 1882 | 1908 | } |
| 1883 | 1909 | } |
| ... | ... | @@ -1962,7 +1988,7 @@ export default { |
| 1962 | 1988 | color: #EF4444; |
| 1963 | 1989 | font-weight: 700; |
| 1964 | 1990 | position: relative; |
| 1965 | - | |
| 1991 | + | |
| 1966 | 1992 | &::after { |
| 1967 | 1993 | content: ''; |
| 1968 | 1994 | position: absolute; |
| ... | ... | @@ -1980,12 +2006,10 @@ export default { |
| 1980 | 2006 | .amount-section { |
| 1981 | 2007 | margin-top: auto; |
| 1982 | 2008 | // 更精致的渐变背景 |
| 1983 | - background: linear-gradient( | |
| 1984 | - 135deg, | |
| 1985 | - rgba(37, 99, 235, 0.12) 0%, | |
| 1986 | - rgba(59, 130, 246, 0.08) 50%, | |
| 1987 | - rgba(96, 165, 250, 0.06) 100% | |
| 1988 | - ); | |
| 2009 | + background: linear-gradient(135deg, | |
| 2010 | + rgba(37, 99, 235, 0.12) 0%, | |
| 2011 | + rgba(59, 130, 246, 0.08) 50%, | |
| 2012 | + rgba(96, 165, 250, 0.06) 100%); | |
| 1989 | 2013 | backdrop-filter: blur(12px) saturate(150%); |
| 1990 | 2014 | -webkit-backdrop-filter: blur(12px) saturate(150%); |
| 1991 | 2015 | border: 1px solid rgba(255, 255, 255, 0.5); |
| ... | ... | @@ -1997,7 +2021,7 @@ export default { |
| 1997 | 2021 | margin-top: 6px; // 缩小上边距 |
| 1998 | 2022 | position: relative; |
| 1999 | 2023 | overflow: hidden; |
| 2000 | - box-shadow: | |
| 2024 | + box-shadow: | |
| 2001 | 2025 | inset 0 1px 2px rgba(255, 255, 255, 0.6), |
| 2002 | 2026 | inset 0 -1px 1px rgba(37, 99, 235, 0.1), |
| 2003 | 2027 | 0 2px 8px rgba(37, 99, 235, 0.1); |
| ... | ... | @@ -2009,12 +2033,10 @@ export default { |
| 2009 | 2033 | left: 0; |
| 2010 | 2034 | right: 0; |
| 2011 | 2035 | height: 2px; |
| 2012 | - background: linear-gradient( | |
| 2013 | - 90deg, | |
| 2014 | - transparent 0%, | |
| 2015 | - rgba(37, 99, 235, 0.4) 50%, | |
| 2016 | - transparent 100% | |
| 2017 | - ); | |
| 2036 | + background: linear-gradient(90deg, | |
| 2037 | + transparent 0%, | |
| 2038 | + rgba(37, 99, 235, 0.4) 50%, | |
| 2039 | + transparent 100%); | |
| 2018 | 2040 | box-shadow: 0 2px 4px rgba(37, 99, 235, 0.2); |
| 2019 | 2041 | } |
| 2020 | 2042 | |
| ... | ... | @@ -2042,7 +2064,7 @@ export default { |
| 2042 | 2064 | opacity: 0.8; |
| 2043 | 2065 | transition: all 0.2s ease; |
| 2044 | 2066 | } |
| 2045 | - | |
| 2067 | + | |
| 2046 | 2068 | &:hover .icon-inline { |
| 2047 | 2069 | opacity: 1; |
| 2048 | 2070 | transform: scale(1.1); |
| ... | ... | @@ -2091,15 +2113,13 @@ export default { |
| 2091 | 2113 | display: flex; |
| 2092 | 2114 | justify-content: space-between; |
| 2093 | 2115 | align-items: center; |
| 2094 | - background: linear-gradient( | |
| 2095 | - 180deg, | |
| 2096 | - rgba(255, 255, 255, 0) 0%, | |
| 2097 | - rgba(255, 255, 255, 0.2) 50%, | |
| 2098 | - rgba(248, 250, 252, 0.4) 100% | |
| 2099 | - ); | |
| 2116 | + background: linear-gradient(180deg, | |
| 2117 | + rgba(255, 255, 255, 0) 0%, | |
| 2118 | + rgba(255, 255, 255, 0.2) 50%, | |
| 2119 | + rgba(248, 250, 252, 0.4) 100%); | |
| 2100 | 2120 | gap: 8px; // 合理的间距 |
| 2101 | 2121 | position: relative; |
| 2102 | - | |
| 2122 | + | |
| 2103 | 2123 | // 顶部高光线条 |
| 2104 | 2124 | &::before { |
| 2105 | 2125 | content: ''; |
| ... | ... | @@ -2108,12 +2128,10 @@ export default { |
| 2108 | 2128 | left: 16px; |
| 2109 | 2129 | right: 16px; |
| 2110 | 2130 | height: 1px; |
| 2111 | - background: linear-gradient( | |
| 2112 | - 90deg, | |
| 2113 | - transparent 0%, | |
| 2114 | - rgba(255, 255, 255, 0.6) 50%, | |
| 2115 | - transparent 100% | |
| 2116 | - ); | |
| 2131 | + background: linear-gradient(90deg, | |
| 2132 | + transparent 0%, | |
| 2133 | + rgba(255, 255, 255, 0.6) 50%, | |
| 2134 | + transparent 100%); | |
| 2117 | 2135 | } |
| 2118 | 2136 | |
| 2119 | 2137 | .action-btn { |
| ... | ... | @@ -2196,16 +2214,935 @@ export default { |
| 2196 | 2214 | |
| 2197 | 2215 | // 分页组件样式(卡片模式) |
| 2198 | 2216 | ::v-deep .pagination-card-mode { |
| 2199 | - flex-shrink: 0; // 防止分页被压缩 | |
| 2217 | + flex-shrink: 0; | |
| 2200 | 2218 | position: relative; |
| 2201 | - z-index: 20; // 确保在卡片之上 | |
| 2219 | + z-index: 20; | |
| 2202 | 2220 | margin-top: 16px; |
| 2203 | 2221 | padding: 12px 0; |
| 2204 | 2222 | background: transparent; |
| 2205 | - // 毛玻璃效果的分页背景(可选) | |
| 2206 | - // background: rgba(255, 255, 255, 0.85); | |
| 2207 | - // backdrop-filter: blur(10px); | |
| 2208 | - // border-radius: 8px; | |
| 2209 | - // border-top: 1px solid rgba(226, 232, 240, 0.5); | |
| 2223 | +} | |
| 2224 | + | |
| 2225 | +/* ============================================ | |
| 2226 | + 现代化筛选卡片样式 | |
| 2227 | + ============================================ */ | |
| 2228 | +.search-card { | |
| 2229 | + margin-bottom: 16px; | |
| 2230 | + border-radius: 12px; | |
| 2231 | + border: 1px solid #e8e8e8; | |
| 2232 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); | |
| 2233 | + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 2234 | + /* 初始状态:隐藏在上方 */ | |
| 2235 | + opacity: 0; | |
| 2236 | + transform: translateY(-20px); | |
| 2237 | + max-height: 0; | |
| 2238 | + overflow: hidden; | |
| 2239 | + | |
| 2240 | + &.search-card-animated { | |
| 2241 | + /* 动画状态:显示并滑入 */ | |
| 2242 | + opacity: 1; | |
| 2243 | + transform: translateY(0); | |
| 2244 | + max-height: 2000px; | |
| 2245 | + transition: opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1), | |
| 2246 | + transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1), | |
| 2247 | + max-height 0.6s cubic-bezier(0.4, 0, 0.2, 1); | |
| 2248 | + } | |
| 2249 | + | |
| 2250 | + &:hover { | |
| 2251 | + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); | |
| 2252 | + } | |
| 2253 | + | |
| 2254 | + ::v-deep .el-card__body { | |
| 2255 | + padding: 16px 24px; | |
| 2256 | + } | |
| 2257 | + | |
| 2258 | + .search-row { | |
| 2259 | + margin-bottom: 0; | |
| 2260 | + transition: margin 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 2261 | + | |
| 2262 | + .el-col { | |
| 2263 | + display: flex; | |
| 2264 | + align-items: stretch; | |
| 2265 | + } | |
| 2266 | + | |
| 2267 | + .el-form-item { | |
| 2268 | + margin-bottom: 0; | |
| 2269 | + width: 100%; | |
| 2270 | + display: flex; | |
| 2271 | + align-items: center; | |
| 2272 | + min-height: 32px; | |
| 2273 | + | |
| 2274 | + ::v-deep .el-form-item__label { | |
| 2275 | + height: 32px; | |
| 2276 | + line-height: 32px; | |
| 2277 | + padding-bottom: 0; | |
| 2278 | + padding-top: 0; | |
| 2279 | + margin-bottom: 0; | |
| 2280 | + display: flex; | |
| 2281 | + align-items: center; | |
| 2282 | + flex-shrink: 0; | |
| 2283 | + } | |
| 2284 | + | |
| 2285 | + ::v-deep .el-form-item__content { | |
| 2286 | + line-height: 32px; | |
| 2287 | + display: flex; | |
| 2288 | + align-items: center; | |
| 2289 | + flex: 1; | |
| 2290 | + min-height: 32px; | |
| 2291 | + } | |
| 2292 | + } | |
| 2293 | + } | |
| 2294 | + | |
| 2295 | + /* 第一行默认显示,有页面加载动画 */ | |
| 2296 | + .search-row-1 { | |
| 2297 | + margin-bottom: 0; | |
| 2298 | + opacity: 0; | |
| 2299 | + transform: translate3d(0, -10px, 0); | |
| 2300 | + transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0.1s, | |
| 2301 | + transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0.1s; | |
| 2302 | + will-change: opacity, transform; | |
| 2303 | + backface-visibility: hidden; | |
| 2304 | + perspective: 1000px; | |
| 2305 | + } | |
| 2306 | + | |
| 2307 | + /* 卡片动画完成后,第一行滑入 */ | |
| 2308 | + &.search-card-animated { | |
| 2309 | + .search-row-1 { | |
| 2310 | + opacity: 1; | |
| 2311 | + transform: translate3d(0, 0, 0); | |
| 2312 | + } | |
| 2313 | + } | |
| 2314 | + | |
| 2315 | + /* 展开面板滑动动画 */ | |
| 2316 | + .filter-expand-panel { | |
| 2317 | + max-height: 0; | |
| 2318 | + overflow: hidden; | |
| 2319 | + opacity: 0; | |
| 2320 | + transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1), | |
| 2321 | + opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1) 0.05s, | |
| 2322 | + padding-top 0.25s cubic-bezier(0.4, 0, 0.2, 1), | |
| 2323 | + margin-top 0.25s cubic-bezier(0.4, 0, 0.2, 1); | |
| 2324 | + padding-top: 0; | |
| 2325 | + margin-top: 0; | |
| 2326 | + will-change: max-height, opacity; | |
| 2327 | + backface-visibility: hidden; | |
| 2328 | + | |
| 2329 | + &.is-expanded { | |
| 2330 | + max-height: 1000px; | |
| 2331 | + opacity: 1; | |
| 2332 | + padding-top: 16px; | |
| 2333 | + margin-top: 0; | |
| 2334 | + transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1), | |
| 2335 | + opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1) 0.05s, | |
| 2336 | + padding-top 0.25s cubic-bezier(0.4, 0, 0.2, 1), | |
| 2337 | + margin-top 0.25s cubic-bezier(0.4, 0, 0.2, 1); | |
| 2338 | + } | |
| 2339 | + | |
| 2340 | + .search-row { | |
| 2341 | + opacity: 1; | |
| 2342 | + transform: translateY(0); | |
| 2343 | + margin-bottom: 16px; | |
| 2344 | + | |
| 2345 | + &:last-child { | |
| 2346 | + margin-bottom: 0; | |
| 2347 | + } | |
| 2348 | + | |
| 2349 | + .el-col { | |
| 2350 | + display: flex; | |
| 2351 | + align-items: stretch; | |
| 2352 | + } | |
| 2353 | + | |
| 2354 | + .el-form-item { | |
| 2355 | + margin-bottom: 0; | |
| 2356 | + width: 100%; | |
| 2357 | + display: flex; | |
| 2358 | + align-items: center; | |
| 2359 | + min-height: 32px; | |
| 2360 | + | |
| 2361 | + ::v-deep .el-form-item__label { | |
| 2362 | + height: 32px; | |
| 2363 | + line-height: 32px; | |
| 2364 | + padding-bottom: 0; | |
| 2365 | + padding-top: 0; | |
| 2366 | + margin-bottom: 0; | |
| 2367 | + display: flex; | |
| 2368 | + align-items: center; | |
| 2369 | + flex-shrink: 0; | |
| 2370 | + } | |
| 2371 | + | |
| 2372 | + ::v-deep .el-form-item__content { | |
| 2373 | + line-height: 32px; | |
| 2374 | + display: flex; | |
| 2375 | + align-items: center; | |
| 2376 | + flex: 1; | |
| 2377 | + min-height: 32px; | |
| 2378 | + } | |
| 2379 | + } | |
| 2380 | + } | |
| 2381 | + } | |
| 2382 | + | |
| 2383 | + .el-form { | |
| 2384 | + ::v-deep .el-row { | |
| 2385 | + display: flex; | |
| 2386 | + flex-wrap: wrap; | |
| 2387 | + } | |
| 2388 | + | |
| 2389 | + ::v-deep .el-col { | |
| 2390 | + display: flex; | |
| 2391 | + align-items: stretch; | |
| 2392 | + } | |
| 2393 | + | |
| 2394 | + .el-form-item { | |
| 2395 | + width: 100%; | |
| 2396 | + display: flex; | |
| 2397 | + align-items: center; | |
| 2398 | + margin-bottom: 0; | |
| 2399 | + | |
| 2400 | + ::v-deep .el-form-item__label { | |
| 2401 | + color: #595959; | |
| 2402 | + font-weight: 500; | |
| 2403 | + height: 32px; | |
| 2404 | + line-height: 32px; | |
| 2405 | + padding-bottom: 0; | |
| 2406 | + padding-top: 0; | |
| 2407 | + margin-bottom: 0; | |
| 2408 | + display: flex; | |
| 2409 | + align-items: center; | |
| 2410 | + flex-shrink: 0; | |
| 2411 | + } | |
| 2412 | + | |
| 2413 | + ::v-deep .el-form-item__content { | |
| 2414 | + line-height: 32px; | |
| 2415 | + display: flex; | |
| 2416 | + align-items: center; | |
| 2417 | + flex: 1; | |
| 2418 | + min-height: 32px; | |
| 2419 | + } | |
| 2420 | + | |
| 2421 | + ::v-deep .el-input, | |
| 2422 | + ::v-deep .el-select, | |
| 2423 | + ::v-deep .el-date-editor { | |
| 2424 | + width: 100%; | |
| 2425 | + display: flex; | |
| 2426 | + align-items: center; | |
| 2427 | + vertical-align: middle; | |
| 2428 | + } | |
| 2429 | + | |
| 2430 | + ::v-deep .el-input__inner, | |
| 2431 | + ::v-deep .el-select .el-input__inner, | |
| 2432 | + ::v-deep .el-date-editor.el-input__inner { | |
| 2433 | + height: 32px; | |
| 2434 | + line-height: 32px; | |
| 2435 | + border-radius: 6px; | |
| 2436 | + border-color: #d9d9d9; | |
| 2437 | + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 2438 | + vertical-align: middle; | |
| 2439 | + box-sizing: border-box; | |
| 2440 | + | |
| 2441 | + &:hover { | |
| 2442 | + border-color: #40a9ff; | |
| 2443 | + } | |
| 2444 | + | |
| 2445 | + &:focus { | |
| 2446 | + border-color: #1890ff; | |
| 2447 | + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); | |
| 2448 | + } | |
| 2449 | + } | |
| 2450 | + | |
| 2451 | + ::v-deep .el-input__prefix, | |
| 2452 | + ::v-deep .el-input__suffix { | |
| 2453 | + .el-input__icon { | |
| 2454 | + line-height: 32px; | |
| 2455 | + } | |
| 2456 | + } | |
| 2457 | + | |
| 2458 | + ::v-deep .el-select__caret { | |
| 2459 | + line-height: 32px; | |
| 2460 | + } | |
| 2461 | + | |
| 2462 | + ::v-deep .el-date-editor { | |
| 2463 | + width: 100%; | |
| 2464 | + display: flex; | |
| 2465 | + align-items: center; | |
| 2466 | + | |
| 2467 | + &.el-input { | |
| 2468 | + width: 100%; | |
| 2469 | + display: flex; | |
| 2470 | + align-items: center; | |
| 2471 | + | |
| 2472 | + .el-input__inner { | |
| 2473 | + height: 32px; | |
| 2474 | + line-height: 32px; | |
| 2475 | + width: 100%; | |
| 2476 | + } | |
| 2477 | + } | |
| 2478 | + | |
| 2479 | + &.el-input__inner { | |
| 2480 | + height: 32px; | |
| 2481 | + line-height: 32px; | |
| 2482 | + width: 100%; | |
| 2483 | + } | |
| 2484 | + } | |
| 2485 | + | |
| 2486 | + // 日期范围选择器特殊处理 | |
| 2487 | + ::v-deep .el-range-editor { | |
| 2488 | + &.el-input__inner { | |
| 2489 | + height: 32px; | |
| 2490 | + line-height: 32px; | |
| 2491 | + display: flex; | |
| 2492 | + align-items: center; | |
| 2493 | + padding: 0 10px; | |
| 2494 | + | |
| 2495 | + .el-range-input { | |
| 2496 | + height: 30px; | |
| 2497 | + line-height: 30px; | |
| 2498 | + flex: 1; | |
| 2499 | + } | |
| 2500 | + | |
| 2501 | + .el-range-separator { | |
| 2502 | + line-height: 30px; | |
| 2503 | + padding: 0 8px; | |
| 2504 | + flex-shrink: 0; | |
| 2505 | + } | |
| 2506 | + } | |
| 2507 | + } | |
| 2508 | + | |
| 2509 | + // 输入框组(如沉睡天数的范围输入) | |
| 2510 | + ::v-deep .el-input-group { | |
| 2511 | + display: flex; | |
| 2512 | + align-items: center; | |
| 2513 | + width: 100%; | |
| 2514 | + | |
| 2515 | + .el-input__inner { | |
| 2516 | + height: 32px; | |
| 2517 | + line-height: 32px; | |
| 2518 | + flex: 1; | |
| 2519 | + } | |
| 2520 | + | |
| 2521 | + .el-input-group__prepend { | |
| 2522 | + height: 32px; | |
| 2523 | + line-height: 30px; | |
| 2524 | + display: flex; | |
| 2525 | + align-items: center; | |
| 2526 | + padding: 0 12px; | |
| 2527 | + flex-shrink: 0; | |
| 2528 | + } | |
| 2529 | + } | |
| 2530 | + } | |
| 2531 | + } | |
| 2532 | + | |
| 2533 | + .search-actions { | |
| 2534 | + display: flex; | |
| 2535 | + align-items: flex-end; | |
| 2536 | + height: 100%; | |
| 2537 | + gap: 8px; | |
| 2538 | + justify-content: flex-start; | |
| 2539 | + padding-top: 2px; | |
| 2540 | + | |
| 2541 | + .el-button { | |
| 2542 | + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 2543 | + } | |
| 2544 | + } | |
| 2545 | + | |
| 2546 | + .search-actions-inline { | |
| 2547 | + display: flex; | |
| 2548 | + align-items: center; | |
| 2549 | + height: 32px; | |
| 2550 | + gap: 8px; | |
| 2551 | + justify-content: flex-end; | |
| 2552 | + padding-top: 0; | |
| 2553 | + width: 100%; | |
| 2554 | + | |
| 2555 | + .el-button { | |
| 2556 | + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 2557 | + height: 32px; | |
| 2558 | + line-height: 32px; | |
| 2559 | + padding: 0 15px; | |
| 2560 | + display: inline-flex; | |
| 2561 | + align-items: center; | |
| 2562 | + justify-content: center; | |
| 2563 | + vertical-align: middle; | |
| 2564 | + | |
| 2565 | + &[type="text"] { | |
| 2566 | + padding: 0 15px; | |
| 2567 | + height: 32px; | |
| 2568 | + line-height: 32px; | |
| 2569 | + } | |
| 2570 | + | |
| 2571 | + &.el-button--small { | |
| 2572 | + height: 28px; | |
| 2573 | + line-height: 28px; | |
| 2574 | + padding: 0 12px; | |
| 2575 | + } | |
| 2576 | + } | |
| 2577 | + } | |
| 2578 | +} | |
| 2579 | + | |
| 2580 | +/* 滑动展开筛选面板 - 流畅的向下展开动画 */ | |
| 2581 | + | |
| 2582 | +.toolbar-card { | |
| 2583 | + margin-bottom: 16px; | |
| 2584 | + border-radius: 12px; | |
| 2585 | + border: 1px solid #e8e8e8; | |
| 2586 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); | |
| 2587 | + | |
| 2588 | + ::v-deep .el-card__body { | |
| 2589 | + padding: 16px 24px; | |
| 2590 | + } | |
| 2591 | + | |
| 2592 | + .toolbar-container { | |
| 2593 | + display: flex; | |
| 2594 | + align-items: center; | |
| 2595 | + justify-content: space-between; | |
| 2596 | + | |
| 2597 | + .toolbar-left { | |
| 2598 | + .el-button-group { | |
| 2599 | + .el-button { | |
| 2600 | + border-radius: 6px; | |
| 2601 | + | |
| 2602 | + &:first-child { | |
| 2603 | + border-top-right-radius: 0; | |
| 2604 | + border-bottom-right-radius: 0; | |
| 2605 | + } | |
| 2606 | + | |
| 2607 | + &:last-child { | |
| 2608 | + border-top-left-radius: 0; | |
| 2609 | + border-bottom-left-radius: 0; | |
| 2610 | + } | |
| 2611 | + | |
| 2612 | + &:not(:first-child):not(:last-child) { | |
| 2613 | + border-radius: 0; | |
| 2614 | + } | |
| 2615 | + } | |
| 2616 | + } | |
| 2617 | + } | |
| 2618 | + | |
| 2619 | + .toolbar-right { | |
| 2620 | + display: flex; | |
| 2621 | + align-items: center; | |
| 2622 | + gap: 12px; | |
| 2623 | + | |
| 2624 | + .el-radio-group { | |
| 2625 | + border-radius: 6px; | |
| 2626 | + overflow: hidden; | |
| 2627 | + } | |
| 2628 | + | |
| 2629 | + .el-divider--vertical { | |
| 2630 | + height: 1.5em; | |
| 2631 | + margin: 0 8px; | |
| 2632 | + } | |
| 2633 | + } | |
| 2634 | + } | |
| 2635 | +} | |
| 2636 | + | |
| 2637 | +/* ============================================ | |
| 2638 | + 现代化表格样式 | |
| 2639 | + ============================================ */ | |
| 2640 | +::v-deep .el-table { | |
| 2641 | + font-size: 14px; | |
| 2642 | + border-radius: 12px; | |
| 2643 | + overflow: hidden; | |
| 2644 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); | |
| 2645 | + | |
| 2646 | + th { | |
| 2647 | + background: linear-gradient(180deg, #fafafa 0%, #f5f5f5 100%) !important; | |
| 2648 | + color: #262626 !important; | |
| 2649 | + font-weight: 600; | |
| 2650 | + font-size: 14px; | |
| 2651 | + border-bottom: 2px solid #e8e8e8; | |
| 2652 | + padding: 14px 0; | |
| 2653 | + } | |
| 2654 | + | |
| 2655 | + td { | |
| 2656 | + border-bottom: 1px solid #f0f0f0; | |
| 2657 | + padding: 14px 0; | |
| 2658 | + white-space: nowrap; | |
| 2659 | + overflow: hidden; | |
| 2660 | + text-overflow: ellipsis; | |
| 2661 | + transform: translateZ(0); // 启用硬件加速,防止闪烁 | |
| 2662 | + backface-visibility: hidden; | |
| 2663 | + } | |
| 2664 | + | |
| 2665 | + tr { | |
| 2666 | + transition: background-color 0.15s ease; | |
| 2667 | + will-change: background-color; | |
| 2668 | + | |
| 2669 | + &:hover>td { | |
| 2670 | + background: #f7f9fc !important; | |
| 2671 | + } | |
| 2672 | + } | |
| 2673 | + | |
| 2674 | + .el-table__body-wrapper { | |
| 2675 | + scrollbar-width: thin; | |
| 2676 | + scrollbar-color: #d9d9d9 transparent; | |
| 2677 | + | |
| 2678 | + &::-webkit-scrollbar { | |
| 2679 | + height: 10px; | |
| 2680 | + width: 10px; | |
| 2681 | + } | |
| 2682 | + | |
| 2683 | + &::-webkit-scrollbar-track { | |
| 2684 | + background: #f5f5f5; | |
| 2685 | + border-radius: 5px; | |
| 2686 | + } | |
| 2687 | + | |
| 2688 | + &::-webkit-scrollbar-thumb { | |
| 2689 | + background: linear-gradient(180deg, #d9d9d9 0%, #bfbfbf 100%); | |
| 2690 | + border-radius: 5px; | |
| 2691 | + border: 2px solid #f5f5f5; | |
| 2692 | + | |
| 2693 | + &:hover { | |
| 2694 | + background: linear-gradient(180deg, #bfbfbf 0%, #999 100%); | |
| 2695 | + } | |
| 2696 | + } | |
| 2697 | + } | |
| 2698 | + | |
| 2699 | + .el-table__fixed, | |
| 2700 | + .el-table__fixed-right { | |
| 2701 | + box-shadow: 0 0 10px rgba(0, 0, 0, 0.06); | |
| 2702 | + } | |
| 2703 | +} | |
| 2704 | + | |
| 2705 | +// 表格单元格样式 | |
| 2706 | +.table-cell-with-icon { | |
| 2707 | + display: flex; | |
| 2708 | + align-items: center; | |
| 2709 | + gap: 8px; | |
| 2710 | + white-space: nowrap; | |
| 2711 | + overflow: hidden; | |
| 2712 | + | |
| 2713 | + .cell-icon { | |
| 2714 | + font-size: 16px; | |
| 2715 | + flex-shrink: 0; | |
| 2716 | + | |
| 2717 | + &.primary { | |
| 2718 | + color: #1890ff; | |
| 2719 | + } | |
| 2720 | + | |
| 2721 | + &.success { | |
| 2722 | + color: #52c41a; | |
| 2723 | + } | |
| 2724 | + | |
| 2725 | + &.warning { | |
| 2726 | + color: #faad14; | |
| 2727 | + } | |
| 2728 | + | |
| 2729 | + &.danger { | |
| 2730 | + color: #ff4d4f; | |
| 2731 | + } | |
| 2732 | + | |
| 2733 | + &.info { | |
| 2734 | + color: #8c8c8c; | |
| 2735 | + } | |
| 2736 | + } | |
| 2737 | + | |
| 2738 | + .cell-text { | |
| 2739 | + color: #262626; | |
| 2740 | + font-weight: 500; | |
| 2741 | + overflow: hidden; | |
| 2742 | + text-overflow: ellipsis; | |
| 2743 | + white-space: nowrap; | |
| 2744 | + } | |
| 2745 | +} | |
| 2746 | + | |
| 2747 | +.cell-text-plain { | |
| 2748 | + color: #595959; | |
| 2749 | + white-space: nowrap; | |
| 2750 | + overflow: hidden; | |
| 2751 | + text-overflow: ellipsis; | |
| 2752 | + display: inline-block; | |
| 2753 | + max-width: 100%; | |
| 2754 | + | |
| 2755 | + &.muted { | |
| 2756 | + color: #8c8c8c; | |
| 2757 | + } | |
| 2758 | +} | |
| 2759 | + | |
| 2760 | +// 金额样式 | |
| 2761 | +.amount-text { | |
| 2762 | + font-weight: 600; | |
| 2763 | + font-family: 'Helvetica Neue', -apple-system, sans-serif; | |
| 2764 | + | |
| 2765 | + &.primary { | |
| 2766 | + color: #1890ff; | |
| 2767 | + } | |
| 2768 | + | |
| 2769 | + &.success { | |
| 2770 | + color: #52c41a; | |
| 2771 | + } | |
| 2772 | + | |
| 2773 | + &.warning { | |
| 2774 | + color: #faad14; | |
| 2775 | + } | |
| 2776 | + | |
| 2777 | + &.danger { | |
| 2778 | + color: #ff4d4f; | |
| 2779 | + } | |
| 2780 | +} | |
| 2781 | + | |
| 2782 | +// 会员标签组 | |
| 2783 | +.member-tags { | |
| 2784 | + display: flex; | |
| 2785 | + gap: 4px; | |
| 2786 | + flex-wrap: wrap; | |
| 2787 | + justify-content: center; | |
| 2788 | + | |
| 2789 | + .el-tag { | |
| 2790 | + margin: 0; | |
| 2791 | + } | |
| 2792 | +} | |
| 2793 | + | |
| 2794 | +// 操作按钮组 | |
| 2795 | +::v-deep .action-buttons { | |
| 2796 | + display: flex; | |
| 2797 | + gap: 8px; | |
| 2798 | + flex-wrap: wrap; | |
| 2799 | + | |
| 2800 | + .el-button { | |
| 2801 | + padding: 0; | |
| 2802 | + font-size: 14px; | |
| 2803 | + transition: all 0.2s; | |
| 2804 | + | |
| 2805 | + &.detail-btn { | |
| 2806 | + color: #1890ff; | |
| 2807 | + | |
| 2808 | + &:hover { | |
| 2809 | + color: #40a9ff; | |
| 2810 | + transform: translateY(-1px); | |
| 2811 | + } | |
| 2812 | + } | |
| 2813 | + | |
| 2814 | + &.edit-btn { | |
| 2815 | + color: #52c41a; | |
| 2816 | + | |
| 2817 | + &:hover { | |
| 2818 | + color: #73d13d; | |
| 2819 | + transform: translateY(-1px); | |
| 2820 | + } | |
| 2821 | + } | |
| 2822 | + | |
| 2823 | + &.delete-btn { | |
| 2824 | + color: #ff4d4f; | |
| 2825 | + | |
| 2826 | + &:hover { | |
| 2827 | + color: #ff7875; | |
| 2828 | + transform: translateY(-1px); | |
| 2829 | + } | |
| 2830 | + } | |
| 2831 | + } | |
| 2832 | +} | |
| 2833 | + | |
| 2834 | +/* ============================================ | |
| 2835 | + 页面整体现代化样式 | |
| 2836 | + ============================================ */ | |
| 2837 | +.NCC-common-layout { | |
| 2838 | + background: #f5f7fa; | |
| 2839 | + min-height: 100vh; | |
| 2840 | +} | |
| 2841 | + | |
| 2842 | +.NCC-common-layout-center { | |
| 2843 | + padding: 16px; | |
| 2844 | +} | |
| 2845 | + | |
| 2846 | +.NCC-common-layout-main { | |
| 2847 | + background: transparent; | |
| 2848 | +} | |
| 2849 | + | |
| 2850 | +/* ============================================ | |
| 2851 | + 现代化标签样式系统 | |
| 2852 | + ============================================ */ | |
| 2853 | +::v-deep .el-tag { | |
| 2854 | + border-radius: 12px; | |
| 2855 | + padding: 4px 12px; | |
| 2856 | + font-weight: 600; | |
| 2857 | + font-size: 12px; | |
| 2858 | + border: none; | |
| 2859 | + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06); | |
| 2860 | + transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), | |
| 2861 | + box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| 2862 | + position: relative; | |
| 2863 | + overflow: hidden; | |
| 2864 | + will-change: transform, box-shadow; | |
| 2865 | + transform: translateZ(0); // 启用硬件加速 | |
| 2866 | + | |
| 2867 | + &.el-tag--small { | |
| 2868 | + height: 24px; | |
| 2869 | + line-height: 16px; | |
| 2870 | + padding: 4px 10px; | |
| 2871 | + font-size: 12px; | |
| 2872 | + border-radius: 12px; | |
| 2873 | + } | |
| 2874 | + | |
| 2875 | + &.el-tag--mini { | |
| 2876 | + height: 20px; | |
| 2877 | + line-height: 12px; | |
| 2878 | + padding: 4px 8px; | |
| 2879 | + font-size: 11px; | |
| 2880 | + border-radius: 10px; | |
| 2881 | + font-weight: 500; | |
| 2882 | + } | |
| 2883 | + | |
| 2884 | + /* Plain风格 - 现代化半透明背景 */ | |
| 2885 | + &.el-tag--plain { | |
| 2886 | + background: rgba(0, 0, 0, 0.05); | |
| 2887 | + color: #595959; | |
| 2888 | + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); | |
| 2889 | + | |
| 2890 | + &.el-tag--success { | |
| 2891 | + background: linear-gradient(135deg, rgba(82, 196, 26, 0.15) 0%, rgba(82, 196, 26, 0.1) 100%); | |
| 2892 | + color: #389e0d; | |
| 2893 | + border: 1px solid rgba(82, 196, 26, 0.2); | |
| 2894 | + box-shadow: 0 2px 6px rgba(82, 196, 26, 0.15); | |
| 2895 | + | |
| 2896 | + &:hover { | |
| 2897 | + background: linear-gradient(135deg, rgba(82, 196, 26, 0.2) 0%, rgba(82, 196, 26, 0.15) 100%); | |
| 2898 | + box-shadow: 0 4px 8px rgba(82, 196, 26, 0.2); | |
| 2899 | + } | |
| 2900 | + } | |
| 2901 | + | |
| 2902 | + &.el-tag--warning { | |
| 2903 | + background: linear-gradient(135deg, rgba(250, 173, 20, 0.15) 0%, rgba(250, 173, 20, 0.1) 100%); | |
| 2904 | + color: #d48806; | |
| 2905 | + border: 1px solid rgba(250, 173, 20, 0.2); | |
| 2906 | + box-shadow: 0 2px 6px rgba(250, 173, 20, 0.15); | |
| 2907 | + | |
| 2908 | + &:hover { | |
| 2909 | + background: linear-gradient(135deg, rgba(250, 173, 20, 0.2) 0%, rgba(250, 173, 20, 0.15) 100%); | |
| 2910 | + box-shadow: 0 4px 8px rgba(250, 173, 20, 0.2); | |
| 2911 | + } | |
| 2912 | + } | |
| 2913 | + | |
| 2914 | + &.el-tag--danger { | |
| 2915 | + background: linear-gradient(135deg, rgba(255, 77, 79, 0.15) 0%, rgba(255, 77, 79, 0.1) 100%); | |
| 2916 | + color: #cf1322; | |
| 2917 | + border: 1px solid rgba(255, 77, 79, 0.2); | |
| 2918 | + box-shadow: 0 2px 6px rgba(255, 77, 79, 0.15); | |
| 2919 | + | |
| 2920 | + &:hover { | |
| 2921 | + background: linear-gradient(135deg, rgba(255, 77, 79, 0.2) 0%, rgba(255, 77, 79, 0.15) 100%); | |
| 2922 | + box-shadow: 0 4px 8px rgba(255, 77, 79, 0.2); | |
| 2923 | + } | |
| 2924 | + } | |
| 2925 | + | |
| 2926 | + &.el-tag--info { | |
| 2927 | + background: linear-gradient(135deg, rgba(140, 140, 140, 0.12) 0%, rgba(140, 140, 140, 0.08) 100%); | |
| 2928 | + color: #595959; | |
| 2929 | + border: 1px solid rgba(140, 140, 140, 0.15); | |
| 2930 | + box-shadow: 0 2px 6px rgba(140, 140, 140, 0.1); | |
| 2931 | + | |
| 2932 | + &:hover { | |
| 2933 | + background: linear-gradient(135deg, rgba(140, 140, 140, 0.18) 0%, rgba(140, 140, 140, 0.12) 100%); | |
| 2934 | + box-shadow: 0 4px 8px rgba(140, 140, 140, 0.15); | |
| 2935 | + } | |
| 2936 | + } | |
| 2937 | + | |
| 2938 | + &.el-tag--primary { | |
| 2939 | + background: linear-gradient(135deg, rgba(24, 144, 255, 0.15) 0%, rgba(24, 144, 255, 0.1) 100%); | |
| 2940 | + color: #0958d9; | |
| 2941 | + border: 1px solid rgba(24, 144, 255, 0.2); | |
| 2942 | + box-shadow: 0 2px 6px rgba(24, 144, 255, 0.15); | |
| 2943 | + | |
| 2944 | + &:hover { | |
| 2945 | + background: linear-gradient(135deg, rgba(24, 144, 255, 0.2) 0%, rgba(24, 144, 255, 0.15) 100%); | |
| 2946 | + box-shadow: 0 4px 8px rgba(24, 144, 255, 0.2); | |
| 2947 | + } | |
| 2948 | + } | |
| 2949 | + } | |
| 2950 | + | |
| 2951 | + /* Dark风格 - 现代化渐变背景 */ | |
| 2952 | + &.el-tag--dark { | |
| 2953 | + font-weight: 600; | |
| 2954 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); | |
| 2955 | + | |
| 2956 | + &.el-tag--primary { | |
| 2957 | + background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); | |
| 2958 | + color: #ffffff; | |
| 2959 | + box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3); | |
| 2960 | + | |
| 2961 | + &:hover { | |
| 2962 | + background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%); | |
| 2963 | + box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4); | |
| 2964 | + transform: translateY(-1px); | |
| 2965 | + } | |
| 2966 | + } | |
| 2967 | + | |
| 2968 | + &.el-tag--success { | |
| 2969 | + background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%); | |
| 2970 | + color: #ffffff; | |
| 2971 | + box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3); | |
| 2972 | + | |
| 2973 | + &:hover { | |
| 2974 | + background: linear-gradient(135deg, #73d13d 0%, #52c41a 100%); | |
| 2975 | + box-shadow: 0 4px 12px rgba(82, 196, 26, 0.4); | |
| 2976 | + transform: translateY(-1px); | |
| 2977 | + } | |
| 2978 | + } | |
| 2979 | + | |
| 2980 | + &.el-tag--warning { | |
| 2981 | + background: linear-gradient(135deg, #faad14 0%, #d48806 100%); | |
| 2982 | + color: #ffffff; | |
| 2983 | + box-shadow: 0 2px 8px rgba(250, 173, 20, 0.3); | |
| 2984 | + | |
| 2985 | + &:hover { | |
| 2986 | + background: linear-gradient(135deg, #ffc53d 0%, #faad14 100%); | |
| 2987 | + box-shadow: 0 4px 12px rgba(250, 173, 20, 0.4); | |
| 2988 | + transform: translateY(-1px); | |
| 2989 | + } | |
| 2990 | + } | |
| 2991 | + | |
| 2992 | + &.el-tag--danger { | |
| 2993 | + background: linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%); | |
| 2994 | + color: #ffffff; | |
| 2995 | + box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3); | |
| 2996 | + | |
| 2997 | + &:hover { | |
| 2998 | + background: linear-gradient(135deg, #ff7875 0%, #ff4d4f 100%); | |
| 2999 | + box-shadow: 0 4px 12px rgba(255, 77, 79, 0.4); | |
| 3000 | + transform: translateY(-1px); | |
| 3001 | + } | |
| 3002 | + } | |
| 3003 | + | |
| 3004 | + &.el-tag--info { | |
| 3005 | + background: linear-gradient(135deg, #8c8c8c 0%, #595959 100%); | |
| 3006 | + color: #ffffff; | |
| 3007 | + box-shadow: 0 2px 8px rgba(140, 140, 140, 0.3); | |
| 3008 | + | |
| 3009 | + &:hover { | |
| 3010 | + background: linear-gradient(135deg, #bfbfbf 0%, #8c8c8c 100%); | |
| 3011 | + box-shadow: 0 4px 12px rgba(140, 140, 140, 0.4); | |
| 3012 | + transform: translateY(-1px); | |
| 3013 | + } | |
| 3014 | + } | |
| 3015 | + } | |
| 3016 | +} | |
| 3017 | + | |
| 3018 | +/* 标签组样式 */ | |
| 3019 | +.member-tags { | |
| 3020 | + display: flex; | |
| 3021 | + gap: 6px; | |
| 3022 | + flex-wrap: wrap; | |
| 3023 | + justify-content: center; | |
| 3024 | + align-items: center; | |
| 3025 | + | |
| 3026 | + .el-tag { | |
| 3027 | + margin: 0; | |
| 3028 | + } | |
| 3029 | +} | |
| 3030 | + | |
| 3031 | +/* 按钮现代化 */ | |
| 3032 | +::v-deep .el-button { | |
| 3033 | + border-radius: 6px; | |
| 3034 | + transition: all 0.3s; | |
| 3035 | + font-weight: 500; | |
| 3036 | + | |
| 3037 | + &.el-button--primary { | |
| 3038 | + background: #1890ff; | |
| 3039 | + border-color: #1890ff; | |
| 3040 | + | |
| 3041 | + &:hover { | |
| 3042 | + background: #40a9ff; | |
| 3043 | + border-color: #40a9ff; | |
| 3044 | + transform: translateY(-2px); | |
| 3045 | + box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3); | |
| 3046 | + } | |
| 3047 | + | |
| 3048 | + &:active { | |
| 3049 | + transform: translateY(0); | |
| 3050 | + } | |
| 3051 | + } | |
| 3052 | + | |
| 3053 | + &.el-button--danger { | |
| 3054 | + &:hover { | |
| 3055 | + transform: translateY(-2px); | |
| 3056 | + box-shadow: 0 4px 12px rgba(255, 77, 79, 0.3); | |
| 3057 | + } | |
| 3058 | + | |
| 3059 | + &:active { | |
| 3060 | + transform: translateY(0); | |
| 3061 | + } | |
| 3062 | + } | |
| 3063 | + | |
| 3064 | + &.el-button--default { | |
| 3065 | + &:hover { | |
| 3066 | + color: #1890ff; | |
| 3067 | + border-color: #1890ff; | |
| 3068 | + } | |
| 3069 | + } | |
| 3070 | + | |
| 3071 | + &.is-circle { | |
| 3072 | + transition: all 0.3s; | |
| 3073 | + | |
| 3074 | + &:hover { | |
| 3075 | + transform: rotate(180deg); | |
| 3076 | + } | |
| 3077 | + } | |
| 3078 | +} | |
| 3079 | + | |
| 3080 | +/* 输入框现代化 */ | |
| 3081 | +::v-deep .el-input__inner { | |
| 3082 | + transition: all 0.3s; | |
| 3083 | + | |
| 3084 | + &:hover { | |
| 3085 | + border-color: #40a9ff; | |
| 3086 | + } | |
| 3087 | + | |
| 3088 | + &:focus { | |
| 3089 | + border-color: #1890ff; | |
| 3090 | + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); | |
| 3091 | + } | |
| 3092 | +} | |
| 3093 | + | |
| 3094 | +/* 下拉框现代化 */ | |
| 3095 | +::v-deep .el-select { | |
| 3096 | + .el-input__inner { | |
| 3097 | + &:hover { | |
| 3098 | + border-color: #40a9ff; | |
| 3099 | + } | |
| 3100 | + } | |
| 3101 | + | |
| 3102 | + &.is-focus .el-input__inner { | |
| 3103 | + border-color: #1890ff; | |
| 3104 | + } | |
| 3105 | +} | |
| 3106 | + | |
| 3107 | +/* 日期选择器现代化 */ | |
| 3108 | +::v-deep .el-date-editor { | |
| 3109 | + .el-input__inner { | |
| 3110 | + &:hover { | |
| 3111 | + border-color: #40a9ff; | |
| 3112 | + } | |
| 3113 | + } | |
| 3114 | + | |
| 3115 | + &.is-active .el-input__inner { | |
| 3116 | + border-color: #1890ff; | |
| 3117 | + } | |
| 3118 | +} | |
| 3119 | + | |
| 3120 | +/* 分页现代化 */ | |
| 3121 | +::v-deep .el-pagination { | |
| 3122 | + | |
| 3123 | + .el-pagination__total, | |
| 3124 | + .el-pagination__jump { | |
| 3125 | + color: #595959; | |
| 3126 | + font-weight: 500; | |
| 3127 | + } | |
| 3128 | + | |
| 3129 | + .btn-prev, | |
| 3130 | + .btn-next, | |
| 3131 | + .el-pager li { | |
| 3132 | + border-radius: 6px; | |
| 3133 | + font-weight: 500; | |
| 3134 | + transition: all 0.3s; | |
| 3135 | + | |
| 3136 | + &:hover { | |
| 3137 | + color: #1890ff; | |
| 3138 | + transform: translateY(-2px); | |
| 3139 | + } | |
| 3140 | + | |
| 3141 | + &.active { | |
| 3142 | + background: #1890ff; | |
| 3143 | + transform: translateY(-2px); | |
| 3144 | + box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3); | |
| 3145 | + } | |
| 3146 | + } | |
| 2210 | 3147 | } |
| 2211 | 3148 | </style> |
| 2212 | 3149 | \ No newline at end of file | ... | ... |
antis-ncc-admin/src/views/lqKhxxBirthday/index.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <div class="birthday-calendar-container"> | |
| 3 | + <!-- 头部筛选区域 --> | |
| 4 | + <div class="filter-section"> | |
| 5 | + <el-card class="filter-card" shadow="hover"> | |
| 6 | + <div class="filter-content"> | |
| 7 | + <div class="filter-item"> | |
| 8 | + <label class="filter-label">选择门店:</label> | |
| 9 | + <el-select v-model="selectedStore" placeholder="请选择门店" clearable filterable | |
| 10 | + @change="handleStoreChange" style="width: 300px"> | |
| 11 | + <el-option label="全部门店" value=""></el-option> | |
| 12 | + <el-option v-for="store in storeList" :key="store.id" :label="store.fullName" | |
| 13 | + :value="store.id"></el-option> | |
| 14 | + </el-select> | |
| 15 | + </div> | |
| 16 | + <div class="filter-item"> | |
| 17 | + <el-button type="primary" icon="el-icon-refresh" @click="loadBirthdayData">刷新</el-button> | |
| 18 | + </div> | |
| 19 | + | |
| 20 | + <!-- 等级说明 --> | |
| 21 | + <div class="level-legend"> | |
| 22 | + <label class="filter-label" style="margin-right: 15px;">等级说明:</label> | |
| 23 | + <div class="legend-item"> | |
| 24 | + <span class="legend-dot level-0"></span> | |
| 25 | + <span>D</span> | |
| 26 | + </div> | |
| 27 | + <div class="legend-item"> | |
| 28 | + <span class="legend-dot level-1"></span> | |
| 29 | + <span>C</span> | |
| 30 | + </div> | |
| 31 | + <div class="legend-item"> | |
| 32 | + <span class="legend-dot level-2"></span> | |
| 33 | + <span>B</span> | |
| 34 | + </div> | |
| 35 | + <div class="legend-item"> | |
| 36 | + <span class="legend-dot level-3"></span> | |
| 37 | + <span>A</span> | |
| 38 | + </div> | |
| 39 | + <div class="legend-item"> | |
| 40 | + <span class="legend-dot level-4"></span> | |
| 41 | + <span>A+</span> | |
| 42 | + </div> | |
| 43 | + <div class="legend-item"> | |
| 44 | + <span class="legend-dot level-5"></span> | |
| 45 | + <span>A++</span> | |
| 46 | + </div> | |
| 47 | + </div> | |
| 48 | + </div> | |
| 49 | + </el-card> | |
| 50 | + </div> | |
| 51 | + | |
| 52 | + <!-- 日历区域 --> | |
| 53 | + <div class="calendar-section"> | |
| 54 | + <el-card shadow="hover"> | |
| 55 | + <el-calendar ref="calendar" v-model="currentDate"> | |
| 56 | + <template slot="dateCell" slot-scope="{date, data}"> | |
| 57 | + <div class="calendar-day"> | |
| 58 | + <div class="day-number">{{ data.day.split('-')[2] }}</div> | |
| 59 | + <div class="birthday-list"> | |
| 60 | + <el-tooltip v-for="member in getBirthdaysByDate(data.day)" :key="member.id" | |
| 61 | + :content="`${member.khmc} (${member.consumeLevelName})`" placement="top"> | |
| 62 | + <span class="birthday-member" :class="`level-${member.consumeLevel}`" | |
| 63 | + @click.stop="showMemberDetail(member)"> | |
| 64 | + <i class="el-icon-user"></i>{{ member.khmc }} | |
| 65 | + </span> | |
| 66 | + </el-tooltip> | |
| 67 | + </div> | |
| 68 | + </div> | |
| 69 | + </template> | |
| 70 | + </el-calendar> | |
| 71 | + </el-card> | |
| 72 | + </div> | |
| 73 | + | |
| 74 | + <!-- 会员详情弹窗 --> | |
| 75 | + <el-dialog :visible.sync="detailDialogVisible" width="750px" :close-on-click-modal="false" | |
| 76 | + :append-to-body="true" :custom-class="'birthday-detail-dialog'"> | |
| 77 | + <div slot="title" class="dialog-header"> | |
| 78 | + <div class="member-avatar"> | |
| 79 | + <i class="el-icon-user"></i> | |
| 80 | + </div> | |
| 81 | + <div class="member-title-info"> | |
| 82 | + <h3 class="member-name">{{ selectedMember ? selectedMember.khmc : '' }}</h3> | |
| 83 | + <p class="member-subtitle">会员生日详情</p> | |
| 84 | + </div> | |
| 85 | + <div v-if="selectedMember" class="member-level-badge" :class="`level-${selectedMember.consumeLevel}`"> | |
| 86 | + <i class="el-icon-star-on"></i> | |
| 87 | + <span>{{ selectedMember.consumeLevelName }}</span> | |
| 88 | + </div> | |
| 89 | + </div> | |
| 90 | + | |
| 91 | + <div v-if="selectedMember" class="member-detail-content"> | |
| 92 | + <div class="info-list"> | |
| 93 | + <div class="info-row"> | |
| 94 | + <label>会员姓名</label> | |
| 95 | + <span class="value">{{ selectedMember.khmc }}</span> | |
| 96 | + </div> | |
| 97 | + <div class="info-row"> | |
| 98 | + <label>档案号</label> | |
| 99 | + <span class="value">{{ selectedMember.dah || '无' }}</span> | |
| 100 | + </div> | |
| 101 | + <div class="info-row"> | |
| 102 | + <label>手机号</label> | |
| 103 | + <span class="value">{{ selectedMember.sjh || '无' }}</span> | |
| 104 | + </div> | |
| 105 | + <div class="info-row"> | |
| 106 | + <label>性别</label> | |
| 107 | + <span class="value">{{ selectedMember.xb || '无' }}</span> | |
| 108 | + </div> | |
| 109 | + <div class="info-row"> | |
| 110 | + <label>归属门店</label> | |
| 111 | + <span class="value">{{ selectedMember.gsmdName || '无' }}</span> | |
| 112 | + </div> | |
| 113 | + <div class="info-row"> | |
| 114 | + <label>生日日期</label> | |
| 115 | + <span class="value">{{ formatBirthday(selectedMember.yanglsr) }}</span> | |
| 116 | + </div> | |
| 117 | + <div class="info-row"> | |
| 118 | + <label>年龄</label> | |
| 119 | + <span class="value">{{ selectedMember.age }}岁</span> | |
| 120 | + </div> | |
| 121 | + <div class="info-row"> | |
| 122 | + <label>剩余权益</label> | |
| 123 | + <span class="value amount">¥{{ selectedMember.remainingRightsAmount.toFixed(2) }}</span> | |
| 124 | + </div> | |
| 125 | + <div v-if="selectedMember.bz" class="info-row"> | |
| 126 | + <label>备注</label> | |
| 127 | + <span class="value">{{ selectedMember.bz }}</span> | |
| 128 | + </div> | |
| 129 | + </div> | |
| 130 | + </div> | |
| 131 | + | |
| 132 | + <span slot="footer" class="dialog-footer-custom"> | |
| 133 | + <el-button class="close-btn" @click="detailDialogVisible = false"> | |
| 134 | + <i class="el-icon-close"></i> | |
| 135 | + 关闭 | |
| 136 | + </el-button> | |
| 137 | + </span> | |
| 138 | + </el-dialog> | |
| 139 | + </div> | |
| 140 | +</template> | |
| 141 | + | |
| 142 | +<script> | |
| 143 | +import request from '@/utils/request' | |
| 144 | + | |
| 145 | +export default { | |
| 146 | + name: 'MemberBirthdayCalendar', | |
| 147 | + data() { | |
| 148 | + return { | |
| 149 | + currentDate: new Date(), | |
| 150 | + savedCalendarDate: null, // 保存日历的日期状态 | |
| 151 | + selectedStore: '', | |
| 152 | + storeList: [], | |
| 153 | + birthdayData: [], | |
| 154 | + birthdayMap: {}, // 日期到会员的映射 | |
| 155 | + detailDialogVisible: false, | |
| 156 | + selectedMember: null, | |
| 157 | + loading: false | |
| 158 | + } | |
| 159 | + }, | |
| 160 | + watch: { | |
| 161 | + detailDialogVisible(newVal, oldVal) { | |
| 162 | + if (newVal === true) { | |
| 163 | + // 弹窗打开时,保存当前日历日期 | |
| 164 | + this.savedCalendarDate = new Date(this.currentDate) | |
| 165 | + } else if (newVal === false && oldVal === true) { | |
| 166 | + // 弹窗关闭时,恢复日历日期 | |
| 167 | + if (this.savedCalendarDate) { | |
| 168 | + this.$nextTick(() => { | |
| 169 | + this.currentDate = new Date(this.savedCalendarDate) | |
| 170 | + }) | |
| 171 | + } | |
| 172 | + } | |
| 173 | + } | |
| 174 | + }, | |
| 175 | + created() { | |
| 176 | + this.loadStoreList() | |
| 177 | + this.loadBirthdayData() | |
| 178 | + }, | |
| 179 | + methods: { | |
| 180 | + /** | |
| 181 | + * 加载门店列表 | |
| 182 | + */ | |
| 183 | + async loadStoreList() { | |
| 184 | + try { | |
| 185 | + // 获取所有门店(使用Selector接口) | |
| 186 | + const res = await request({ | |
| 187 | + url: '/api/Extend/LqMdxx/Selector', | |
| 188 | + method: 'get' | |
| 189 | + }) | |
| 190 | + if (res.code === 200 && res.data) { | |
| 191 | + this.storeList = res.data.list || [] | |
| 192 | + } | |
| 193 | + } catch (error) { | |
| 194 | + console.error('加载门店列表失败', error) | |
| 195 | + } | |
| 196 | + }, | |
| 197 | + | |
| 198 | + /** | |
| 199 | + * 加载生日数据 | |
| 200 | + */ | |
| 201 | + async loadBirthdayData() { | |
| 202 | + this.loading = true | |
| 203 | + try { | |
| 204 | + const res = await request({ | |
| 205 | + url: '/api/Extend/LqKhxx/GetUpcomingBirthdays', | |
| 206 | + method: 'get', | |
| 207 | + data: { | |
| 208 | + storeId: this.selectedStore | |
| 209 | + } | |
| 210 | + }) | |
| 211 | + | |
| 212 | + if (res.code === 200 && res.data && res.data.code === 200) { | |
| 213 | + this.birthdayData = res.data.data || [] | |
| 214 | + this.buildBirthdayMap() | |
| 215 | + // this.$message.success(`成功加载 ${this.birthdayData.length} 位会员的生日信息`) | |
| 216 | + } else { | |
| 217 | + this.$message.error(res.msg || '加载失败') | |
| 218 | + } | |
| 219 | + } catch (error) { | |
| 220 | + console.error('加载生日数据失败', error) | |
| 221 | + this.$message.error('加载生日数据失败') | |
| 222 | + } finally { | |
| 223 | + this.loading = false | |
| 224 | + } | |
| 225 | + }, | |
| 226 | + | |
| 227 | + /** | |
| 228 | + * 构建日期到会员的映射 | |
| 229 | + */ | |
| 230 | + buildBirthdayMap() { | |
| 231 | + this.birthdayMap = {} | |
| 232 | + this.birthdayData.forEach(member => { | |
| 233 | + const date = new Date(member.birthdayFullDate) | |
| 234 | + const dateStr = this.formatDate(date) | |
| 235 | + if (!this.birthdayMap[dateStr]) { | |
| 236 | + this.birthdayMap[dateStr] = [] | |
| 237 | + } | |
| 238 | + this.birthdayMap[dateStr].push(member) | |
| 239 | + }) | |
| 240 | + }, | |
| 241 | + | |
| 242 | + /** | |
| 243 | + * 格式化日期 | |
| 244 | + */ | |
| 245 | + formatDate(date) { | |
| 246 | + const d = new Date(date) | |
| 247 | + const year = d.getFullYear() | |
| 248 | + const month = String(d.getMonth() + 1).padStart(2, '0') | |
| 249 | + const day = String(d.getDate()).padStart(2, '0') | |
| 250 | + return `${year}-${month}-${day}` | |
| 251 | + }, | |
| 252 | + | |
| 253 | + /** | |
| 254 | + * 根据日期获取过生日的会员 | |
| 255 | + */ | |
| 256 | + getBirthdaysByDate(dateStr) { | |
| 257 | + return this.birthdayMap[dateStr] || [] | |
| 258 | + }, | |
| 259 | + | |
| 260 | + /** | |
| 261 | + * 门店变化 | |
| 262 | + */ | |
| 263 | + handleStoreChange() { | |
| 264 | + this.loadBirthdayData() | |
| 265 | + }, | |
| 266 | + | |
| 267 | + /** | |
| 268 | + * 显示会员详情 | |
| 269 | + */ | |
| 270 | + showMemberDetail(member) { | |
| 271 | + this.selectedMember = member | |
| 272 | + this.detailDialogVisible = true | |
| 273 | + }, | |
| 274 | + | |
| 275 | + /** | |
| 276 | + * 格式化生日 | |
| 277 | + */ | |
| 278 | + formatBirthday(timestamp) { | |
| 279 | + if (!timestamp) return '无' | |
| 280 | + const date = new Date(timestamp) | |
| 281 | + return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日` | |
| 282 | + }, | |
| 283 | + | |
| 284 | + /** | |
| 285 | + * 获取等级标签类型 | |
| 286 | + */ | |
| 287 | + getLevelTagType(level) { | |
| 288 | + const types = { | |
| 289 | + 0: 'info', | |
| 290 | + 1: 'success', | |
| 291 | + 2: '', | |
| 292 | + 3: 'warning', | |
| 293 | + 4: 'danger', | |
| 294 | + 5: 'danger' | |
| 295 | + } | |
| 296 | + return types[level] || 'info' | |
| 297 | + } | |
| 298 | + } | |
| 299 | +} | |
| 300 | +</script> | |
| 301 | + | |
| 302 | +<style lang="scss" scoped> | |
| 303 | +.birthday-calendar-container { | |
| 304 | + padding: 24px; | |
| 305 | + background: linear-gradient(135deg, #f8fafc 0%, #e8f3ff 100%); | |
| 306 | + min-height: calc(100vh - 84px); | |
| 307 | + position: relative; | |
| 308 | + | |
| 309 | + // 背景装饰 | |
| 310 | + &::before { | |
| 311 | + content: ''; | |
| 312 | + position: absolute; | |
| 313 | + top: 0; | |
| 314 | + right: 0; | |
| 315 | + width: 500px; | |
| 316 | + height: 500px; | |
| 317 | + background: radial-gradient(circle, rgba(59, 130, 246, 0.08) 0%, transparent 70%); | |
| 318 | + pointer-events: none; | |
| 319 | + z-index: 0; | |
| 320 | + } | |
| 321 | + | |
| 322 | + >* { | |
| 323 | + position: relative; | |
| 324 | + z-index: 1; | |
| 325 | + } | |
| 326 | + | |
| 327 | + .filter-section { | |
| 328 | + margin-bottom: 24px; | |
| 329 | + | |
| 330 | + .filter-card { | |
| 331 | + border-radius: 16px; | |
| 332 | + border: 1px solid rgba(226, 232, 240, 0.8); | |
| 333 | + background: rgba(255, 255, 255, 0.9); | |
| 334 | + backdrop-filter: blur(12px); | |
| 335 | + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06); | |
| 336 | + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 337 | + | |
| 338 | + &:hover { | |
| 339 | + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); | |
| 340 | + transform: translateY(-2px); | |
| 341 | + } | |
| 342 | + | |
| 343 | + ::v-deep .el-card__body { | |
| 344 | + padding: 24px; | |
| 345 | + } | |
| 346 | + | |
| 347 | + .filter-content { | |
| 348 | + display: flex; | |
| 349 | + align-items: center; | |
| 350 | + gap: 20px; | |
| 351 | + | |
| 352 | + .filter-item { | |
| 353 | + display: flex; | |
| 354 | + align-items: center; | |
| 355 | + | |
| 356 | + .filter-label { | |
| 357 | + font-size: 14px; | |
| 358 | + font-weight: 600; | |
| 359 | + color: #1e293b; | |
| 360 | + margin-right: 12px; | |
| 361 | + white-space: nowrap; | |
| 362 | + letter-spacing: -0.01em; | |
| 363 | + } | |
| 364 | + | |
| 365 | + .el-select { | |
| 366 | + ::v-deep .el-input__inner { | |
| 367 | + border-radius: 10px; | |
| 368 | + border: 1.5px solid #e2e8f0; | |
| 369 | + transition: all 0.3s; | |
| 370 | + | |
| 371 | + &:hover { | |
| 372 | + border-color: #3b82f6; | |
| 373 | + } | |
| 374 | + | |
| 375 | + &:focus { | |
| 376 | + border-color: #3b82f6; | |
| 377 | + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); | |
| 378 | + } | |
| 379 | + } | |
| 380 | + } | |
| 381 | + | |
| 382 | + .el-button { | |
| 383 | + border-radius: 10px; | |
| 384 | + font-weight: 500; | |
| 385 | + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 386 | + | |
| 387 | + &:hover { | |
| 388 | + transform: translateY(-2px); | |
| 389 | + box-shadow: 0 8px 16px rgba(59, 130, 246, 0.3); | |
| 390 | + } | |
| 391 | + } | |
| 392 | + } | |
| 393 | + | |
| 394 | + .level-legend { | |
| 395 | + display: flex; | |
| 396 | + align-items: center; | |
| 397 | + gap: 16px; | |
| 398 | + margin-left: auto; | |
| 399 | + padding: 8px 16px; | |
| 400 | + background: linear-gradient(135deg, rgba(59, 130, 246, 0.05) 0%, rgba(96, 165, 250, 0.05) 100%); | |
| 401 | + border-radius: 12px; | |
| 402 | + border: 1px solid rgba(59, 130, 246, 0.1); | |
| 403 | + | |
| 404 | + .legend-item { | |
| 405 | + display: flex; | |
| 406 | + align-items: center; | |
| 407 | + gap: 6px; | |
| 408 | + font-size: 13px; | |
| 409 | + font-weight: 500; | |
| 410 | + color: #475569; | |
| 411 | + transition: all 0.2s; | |
| 412 | + cursor: default; | |
| 413 | + | |
| 414 | + &:hover { | |
| 415 | + color: #1e293b; | |
| 416 | + } | |
| 417 | + | |
| 418 | + .legend-dot { | |
| 419 | + width: 16px; | |
| 420 | + height: 16px; | |
| 421 | + border-radius: 4px; | |
| 422 | + flex-shrink: 0; | |
| 423 | + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| 424 | + transition: all 0.2s; | |
| 425 | + | |
| 426 | + &.level-0 { | |
| 427 | + background: linear-gradient(135deg, #94a3b8 0%, #64748b 100%); | |
| 428 | + } | |
| 429 | + | |
| 430 | + &.level-1 { | |
| 431 | + background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%); | |
| 432 | + } | |
| 433 | + | |
| 434 | + &.level-2 { | |
| 435 | + background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%); | |
| 436 | + } | |
| 437 | + | |
| 438 | + &.level-3 { | |
| 439 | + background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); | |
| 440 | + } | |
| 441 | + | |
| 442 | + &.level-4 { | |
| 443 | + background: linear-gradient(135deg, #f87171 0%, #ef4444 100%); | |
| 444 | + } | |
| 445 | + | |
| 446 | + &.level-5 { | |
| 447 | + background: linear-gradient(135deg, #ec4899 0%, #db2777 100%); | |
| 448 | + } | |
| 449 | + } | |
| 450 | + | |
| 451 | + &:hover .legend-dot { | |
| 452 | + transform: scale(1.1); | |
| 453 | + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); | |
| 454 | + } | |
| 455 | + } | |
| 456 | + } | |
| 457 | + } | |
| 458 | + } | |
| 459 | + } | |
| 460 | + | |
| 461 | + .calendar-section { | |
| 462 | + .el-card { | |
| 463 | + border-radius: 16px; | |
| 464 | + border: 1px solid rgba(226, 232, 240, 0.8); | |
| 465 | + background: rgba(255, 255, 255, 0.95); | |
| 466 | + backdrop-filter: blur(16px); | |
| 467 | + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); | |
| 468 | + overflow: hidden; | |
| 469 | + } | |
| 470 | + | |
| 471 | + ::v-deep .el-calendar { | |
| 472 | + .el-calendar__header { | |
| 473 | + padding: 24px 28px; | |
| 474 | + border-bottom: 2px solid #f1f5f9; | |
| 475 | + background: linear-gradient(135deg, rgba(59, 130, 246, 0.02) 0%, rgba(96, 165, 250, 0.02) 100%); | |
| 476 | + | |
| 477 | + .el-calendar__title { | |
| 478 | + font-size: 20px; | |
| 479 | + font-weight: 700; | |
| 480 | + color: #1e293b; | |
| 481 | + letter-spacing: -0.02em; | |
| 482 | + background: linear-gradient(135deg, #1e293b 0%, #3b82f6 100%); | |
| 483 | + -webkit-background-clip: text; | |
| 484 | + -webkit-text-fill-color: transparent; | |
| 485 | + background-clip: text; | |
| 486 | + } | |
| 487 | + | |
| 488 | + .el-calendar__button-group { | |
| 489 | + button { | |
| 490 | + border-radius: 10px; | |
| 491 | + border: 1.5px solid #e2e8f0; | |
| 492 | + font-weight: 500; | |
| 493 | + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 494 | + padding: 8px 16px; | |
| 495 | + | |
| 496 | + &:hover { | |
| 497 | + border-color: #3b82f6; | |
| 498 | + background: linear-gradient(135deg, rgba(59, 130, 246, 0.05) 0%, rgba(96, 165, 250, 0.05) 100%); | |
| 499 | + transform: translateY(-1px); | |
| 500 | + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2); | |
| 501 | + } | |
| 502 | + | |
| 503 | + &:active { | |
| 504 | + transform: translateY(0); | |
| 505 | + } | |
| 506 | + } | |
| 507 | + } | |
| 508 | + } | |
| 509 | + | |
| 510 | + .el-calendar__body { | |
| 511 | + padding: 20px; | |
| 512 | + } | |
| 513 | + | |
| 514 | + .el-calendar-table { | |
| 515 | + .el-calendar-day { | |
| 516 | + height: 140px; | |
| 517 | + padding: 8px; | |
| 518 | + border-radius: 12px; | |
| 519 | + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 520 | + position: relative; | |
| 521 | + | |
| 522 | + &:hover { | |
| 523 | + background: linear-gradient(135deg, rgba(59, 130, 246, 0.04) 0%, rgba(96, 165, 250, 0.04) 100%); | |
| 524 | + transform: translateY(-2px); | |
| 525 | + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); | |
| 526 | + } | |
| 527 | + } | |
| 528 | + | |
| 529 | + thead th { | |
| 530 | + padding: 16px 0; | |
| 531 | + font-weight: 700; | |
| 532 | + font-size: 13px; | |
| 533 | + color: #64748b; | |
| 534 | + text-transform: uppercase; | |
| 535 | + letter-spacing: 0.05em; | |
| 536 | + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); | |
| 537 | + border-bottom: 2px solid #e2e8f0; | |
| 538 | + } | |
| 539 | + | |
| 540 | + td { | |
| 541 | + border: 1px solid #f1f5f9; | |
| 542 | + transition: all 0.2s; | |
| 543 | + } | |
| 544 | + | |
| 545 | + .is-today { | |
| 546 | + .calendar-day .day-number { | |
| 547 | + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); | |
| 548 | + color: #fff; | |
| 549 | + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); | |
| 550 | + font-weight: 700; | |
| 551 | + } | |
| 552 | + } | |
| 553 | + | |
| 554 | + .is-selected { | |
| 555 | + td { | |
| 556 | + border-color: #3b82f6; | |
| 557 | + } | |
| 558 | + } | |
| 559 | + } | |
| 560 | + } | |
| 561 | + | |
| 562 | + .calendar-day { | |
| 563 | + height: 100%; | |
| 564 | + display: flex; | |
| 565 | + flex-direction: column; | |
| 566 | + padding: 4px; | |
| 567 | + | |
| 568 | + .day-number { | |
| 569 | + display: inline-flex; | |
| 570 | + align-items: center; | |
| 571 | + justify-content: center; | |
| 572 | + width: 32px; | |
| 573 | + height: 32px; | |
| 574 | + border-radius: 8px; | |
| 575 | + font-size: 14px; | |
| 576 | + font-weight: 600; | |
| 577 | + color: #475569; | |
| 578 | + margin-bottom: 6px; | |
| 579 | + flex-shrink: 0; | |
| 580 | + transition: all 0.2s; | |
| 581 | + background: rgba(248, 250, 252, 0.5); | |
| 582 | + border: 1px solid rgba(226, 232, 240, 0.5); | |
| 583 | + } | |
| 584 | + | |
| 585 | + .birthday-list { | |
| 586 | + flex: 1; | |
| 587 | + overflow-y: auto; | |
| 588 | + display: flex; | |
| 589 | + flex-wrap: wrap; | |
| 590 | + gap: 5px; | |
| 591 | + align-content: flex-start; | |
| 592 | + | |
| 593 | + /* 自定义滚动条 */ | |
| 594 | + &::-webkit-scrollbar { | |
| 595 | + width: 4px; | |
| 596 | + } | |
| 597 | + | |
| 598 | + &::-webkit-scrollbar-track { | |
| 599 | + background: transparent; | |
| 600 | + } | |
| 601 | + | |
| 602 | + &::-webkit-scrollbar-thumb { | |
| 603 | + background: rgba(203, 213, 225, 0.5); | |
| 604 | + border-radius: 2px; | |
| 605 | + | |
| 606 | + &:hover { | |
| 607 | + background: rgba(148, 163, 184, 0.7); | |
| 608 | + } | |
| 609 | + } | |
| 610 | + | |
| 611 | + .birthday-member { | |
| 612 | + display: inline-flex; | |
| 613 | + align-items: center; | |
| 614 | + gap: 5px; | |
| 615 | + cursor: pointer; | |
| 616 | + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 617 | + border-radius: 6px; | |
| 618 | + padding: 4px 10px; | |
| 619 | + font-size: 13px; | |
| 620 | + color: #fff; | |
| 621 | + white-space: nowrap; | |
| 622 | + font-weight: 600; | |
| 623 | + position: relative; | |
| 624 | + overflow: hidden; | |
| 625 | + | |
| 626 | + /* 添加微妙的光泽效果 */ | |
| 627 | + &::before { | |
| 628 | + content: ''; | |
| 629 | + position: absolute; | |
| 630 | + top: 0; | |
| 631 | + left: -100%; | |
| 632 | + width: 100%; | |
| 633 | + height: 100%; | |
| 634 | + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); | |
| 635 | + transition: left 0.5s; | |
| 636 | + } | |
| 637 | + | |
| 638 | + &:hover::before { | |
| 639 | + left: 100%; | |
| 640 | + } | |
| 641 | + | |
| 642 | + &.level-0 { | |
| 643 | + background: linear-gradient(135deg, #94a3b8 0%, #64748b 100%); | |
| 644 | + box-shadow: 0 2px 8px rgba(148, 163, 184, 0.3); | |
| 645 | + } | |
| 646 | + | |
| 647 | + &.level-1 { | |
| 648 | + background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%); | |
| 649 | + box-shadow: 0 2px 8px rgba(74, 222, 128, 0.3); | |
| 650 | + } | |
| 651 | + | |
| 652 | + &.level-2 { | |
| 653 | + background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%); | |
| 654 | + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); | |
| 655 | + } | |
| 656 | + | |
| 657 | + &.level-3 { | |
| 658 | + background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); | |
| 659 | + box-shadow: 0 2px 8px rgba(251, 191, 36, 0.3); | |
| 660 | + } | |
| 661 | + | |
| 662 | + &.level-4 { | |
| 663 | + background: linear-gradient(135deg, #f87171 0%, #ef4444 100%); | |
| 664 | + box-shadow: 0 2px 8px rgba(248, 113, 113, 0.3); | |
| 665 | + } | |
| 666 | + | |
| 667 | + &.level-5 { | |
| 668 | + background: linear-gradient(135deg, #ec4899 0%, #db2777 100%); | |
| 669 | + box-shadow: 0 2px 8px rgba(236, 72, 153, 0.3); | |
| 670 | + } | |
| 671 | + | |
| 672 | + &:hover { | |
| 673 | + transform: translateY(-2px) scale(1.02); | |
| 674 | + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); | |
| 675 | + z-index: 10; | |
| 676 | + } | |
| 677 | + | |
| 678 | + &:active { | |
| 679 | + transform: translateY(0) scale(0.98); | |
| 680 | + } | |
| 681 | + | |
| 682 | + i { | |
| 683 | + font-size: 13px; | |
| 684 | + opacity: 0.9; | |
| 685 | + } | |
| 686 | + } | |
| 687 | + } | |
| 688 | + } | |
| 689 | + } | |
| 690 | + | |
| 691 | +} | |
| 692 | +</style> | |
| 693 | + | |
| 694 | +<!-- 非 scoped 样式用于弹窗(append-to-body) --> | |
| 695 | +<style lang="scss"> | |
| 696 | +/* 弹窗全局样式 */ | |
| 697 | +.birthday-detail-dialog { | |
| 698 | + border-radius: 8px !important; | |
| 699 | + overflow: hidden !important; | |
| 700 | + | |
| 701 | + .el-dialog__header { | |
| 702 | + padding: 0 !important; | |
| 703 | + } | |
| 704 | + | |
| 705 | + .el-dialog__headerbtn { | |
| 706 | + top: 16px !important; | |
| 707 | + right: 16px !important; | |
| 708 | + | |
| 709 | + .el-dialog__close { | |
| 710 | + color: #64748b !important; | |
| 711 | + | |
| 712 | + &:hover { | |
| 713 | + color: #ef4444 !important; | |
| 714 | + } | |
| 715 | + } | |
| 716 | + } | |
| 717 | + | |
| 718 | + .el-dialog__body { | |
| 719 | + padding: 0 !important; | |
| 720 | + } | |
| 721 | + | |
| 722 | + .el-dialog__footer { | |
| 723 | + padding: 16px 24px !important; | |
| 724 | + background: #f8fafc !important; | |
| 725 | + border-top: 1px solid #e2e8f0 !important; | |
| 726 | + } | |
| 727 | +} | |
| 728 | + | |
| 729 | +/* 弹窗头部 */ | |
| 730 | +.dialog-header { | |
| 731 | + display: flex; | |
| 732 | + align-items: center; | |
| 733 | + gap: 16px; | |
| 734 | + padding: 24px 32px; | |
| 735 | + background: #f8fafc; | |
| 736 | + border-bottom: 1px solid #e2e8f0; | |
| 737 | + | |
| 738 | + .member-avatar { | |
| 739 | + width: 48px; | |
| 740 | + height: 48px; | |
| 741 | + border-radius: 8px; | |
| 742 | + background: #3b82f6; | |
| 743 | + display: flex; | |
| 744 | + align-items: center; | |
| 745 | + justify-content: center; | |
| 746 | + flex-shrink: 0; | |
| 747 | + | |
| 748 | + i { | |
| 749 | + font-size: 24px; | |
| 750 | + color: #ffffff; | |
| 751 | + } | |
| 752 | + } | |
| 753 | + | |
| 754 | + .member-title-info { | |
| 755 | + flex: 1; | |
| 756 | + | |
| 757 | + .member-name { | |
| 758 | + margin: 0 0 4px 0; | |
| 759 | + font-size: 18px; | |
| 760 | + font-weight: 700; | |
| 761 | + color: #1e293b; | |
| 762 | + } | |
| 763 | + | |
| 764 | + .member-subtitle { | |
| 765 | + margin: 0; | |
| 766 | + font-size: 13px; | |
| 767 | + color: #64748b; | |
| 768 | + font-weight: 500; | |
| 769 | + } | |
| 770 | + } | |
| 771 | + | |
| 772 | + .member-level-badge { | |
| 773 | + display: inline-flex; | |
| 774 | + align-items: center; | |
| 775 | + gap: 6px; | |
| 776 | + padding: 6px 12px; | |
| 777 | + border-radius: 6px; | |
| 778 | + font-weight: 600; | |
| 779 | + font-size: 13px; | |
| 780 | + color: #ffffff; | |
| 781 | + | |
| 782 | + i { | |
| 783 | + font-size: 14px; | |
| 784 | + } | |
| 785 | + | |
| 786 | + &.level-0 { | |
| 787 | + background: #94a3b8; | |
| 788 | + } | |
| 789 | + | |
| 790 | + &.level-1 { | |
| 791 | + background: #22c55e; | |
| 792 | + } | |
| 793 | + | |
| 794 | + &.level-2 { | |
| 795 | + background: #3b82f6; | |
| 796 | + } | |
| 797 | + | |
| 798 | + &.level-3 { | |
| 799 | + background: #f59e0b; | |
| 800 | + } | |
| 801 | + | |
| 802 | + &.level-4 { | |
| 803 | + background: #ef4444; | |
| 804 | + } | |
| 805 | + | |
| 806 | + &.level-5 { | |
| 807 | + background: #ec4899; | |
| 808 | + } | |
| 809 | + } | |
| 810 | +} | |
| 811 | + | |
| 812 | +/* 会员详情内容 */ | |
| 813 | +.member-detail-content { | |
| 814 | + padding: 20px 32px; | |
| 815 | + background: #ffffff; | |
| 816 | + | |
| 817 | + .info-list { | |
| 818 | + .info-row { | |
| 819 | + display: flex; | |
| 820 | + align-items: center; | |
| 821 | + padding: 12px 0; | |
| 822 | + border-bottom: 1px solid #f1f5f9; | |
| 823 | + | |
| 824 | + &:last-child { | |
| 825 | + border-bottom: none; | |
| 826 | + } | |
| 827 | + | |
| 828 | + label { | |
| 829 | + width: 100px; | |
| 830 | + font-size: 14px; | |
| 831 | + font-weight: 600; | |
| 832 | + color: #64748b; | |
| 833 | + flex-shrink: 0; | |
| 834 | + } | |
| 835 | + | |
| 836 | + .value { | |
| 837 | + flex: 1; | |
| 838 | + font-size: 14px; | |
| 839 | + color: #1e293b; | |
| 840 | + font-weight: 500; | |
| 841 | + | |
| 842 | + &.amount { | |
| 843 | + color: #ef4444; | |
| 844 | + font-weight: 700; | |
| 845 | + font-size: 16px; | |
| 846 | + } | |
| 847 | + } | |
| 848 | + } | |
| 849 | + } | |
| 850 | +} | |
| 851 | + | |
| 852 | +/* 自定义底部 */ | |
| 853 | +.dialog-footer-custom { | |
| 854 | + display: flex; | |
| 855 | + justify-content: center; | |
| 856 | + | |
| 857 | + .close-btn { | |
| 858 | + border-radius: 4px; | |
| 859 | + padding: 8px 24px; | |
| 860 | + font-size: 14px; | |
| 861 | + | |
| 862 | + &:hover { | |
| 863 | + border-color: #3b82f6; | |
| 864 | + color: #3b82f6; | |
| 865 | + } | |
| 866 | + } | |
| 867 | +} | |
| 868 | +</style> | ... | ... |
docs/开单表财务报表梳理.md
0 → 100644
| 1 | +# 开单表财务报表梳理 | |
| 2 | + | |
| 3 | +## 概述 | |
| 4 | +基于 `lq_kd_kdjlb`(开单记录表)及相关关联表,梳理可以生成的财务报表类型和统计维度。 | |
| 5 | + | |
| 6 | +--- | |
| 7 | + | |
| 8 | +## 一、开单表核心字段 | |
| 9 | + | |
| 10 | +### 1.1 金额相关字段 | |
| 11 | +- **`zdyj`** (整单业绩) - 订单总金额 | |
| 12 | +- **`sfyj`** (实付业绩) - 实际收款金额(预收款) | |
| 13 | +- **`F_DeductAmount`** (储扣总金额) - 使用会员权益抵扣的金额 | |
| 14 | +- **`qk`** (欠款) - 未付清金额 | |
| 15 | +- **`F_PaidDebt`** (已缴欠款) - 已补缴的欠款金额 | |
| 16 | +- **`F_SupplementAmount`** (补缴金额) - 补缴开单的金额 | |
| 17 | + | |
| 18 | +### 1.2 分类相关字段 | |
| 19 | +- **`gjlx`** (顾客类型) - 新客/老客/会员等 | |
| 20 | +- **`hgjg`** (合作机构) - 合作医院/机构ID | |
| 21 | +- **`fkfs`** (付款方式) - 现金/微信/支付宝/银行卡等 | |
| 22 | +- **`fkyy`** (付款医院) - 付款医院ID(用于合作医院付款) | |
| 23 | +- **`fkpd`** (付款判断) - 付款状态判断 | |
| 24 | +- **`khly`** (客户来源) - 客户获取渠道 | |
| 25 | +- **`tjr`** (推荐人) - 推荐人ID | |
| 26 | + | |
| 27 | +### 1.3 时间相关字段 | |
| 28 | +- **`kdrq`** (开单日期) - 开单日期时间 | |
| 29 | +- **`F_CreateTime`** (创建时间) - 记录创建时间 | |
| 30 | + | |
| 31 | +### 1.4 业务相关字段 | |
| 32 | +- **`djmd`** (单据门店) - 开单门店ID | |
| 33 | +- **`jsj`** (金三角) - 金三角ID | |
| 34 | +- **`kdhy`** (开单会员) - 会员ID | |
| 35 | +- **`F_ActivityId`** (营销活动ID) - 关联的营销活动 | |
| 36 | +- **`F_AppointmentId`** (预约记录ID) - 关联的预约记录 | |
| 37 | +- **`sfskdd`** (是否首开订单) - 是否首次开单 | |
| 38 | +- **`F_IsEffective`** (是否有效) - 是否有效记录 | |
| 39 | +- **`F_UpgradeLifeBeauty`** (升生美) - 是否升级生美 | |
| 40 | +- **`F_UpgradeTechBeauty`** (升科美) - 是否升级科美 | |
| 41 | +- **`F_UpgradeMedicalBeauty`** (升医美) - 是否升级医美 | |
| 42 | + | |
| 43 | +### 1.5 关联表 | |
| 44 | +- **`lq_kd_pxmx`** (开单品项明细表) - 品项详情、品项分类(生美/科美/医美/产品)、项目次数 | |
| 45 | +- **`lq_kd_deductinfo`** (开单扣减信息表) - 储扣明细、扣减类型 | |
| 46 | +- **`lq_kd_jksyj`** (健康师业绩表) - 健康师业绩分配 | |
| 47 | +- **`lq_kd_kjbsyj`** (科技部业绩表) - 科技部老师业绩分配 | |
| 48 | +- **`lq_hytk_hytk`** (退卡表) - 退款金额、实退金额 | |
| 49 | + | |
| 50 | +--- | |
| 51 | + | |
| 52 | +## 二、可生成的财务报表类型 | |
| 53 | + | |
| 54 | +### 2.1 收入类报表 | |
| 55 | + | |
| 56 | +#### 2.1.1 开单收入统计表 | |
| 57 | +**统计维度**:按时间、门店、品项分类 | |
| 58 | +- 整单业绩总额 (`zdyj`) | |
| 59 | +- 实付业绩总额 (`sfyj`) - **预收款** | |
| 60 | +- 储扣金额总额 (`F_DeductAmount`) | |
| 61 | +- 补缴金额总额 (`F_SupplementAmount`) | |
| 62 | +- 已缴欠款总额 (`F_PaidDebt`) | |
| 63 | +- 欠款总额 (`qk`) | |
| 64 | + | |
| 65 | +**关联分析**: | |
| 66 | +- 按品项分类统计(生美/科美/医美/产品) | |
| 67 | +- 按门店统计 | |
| 68 | +- 按时间周期统计(日/周/月/年) | |
| 69 | +- 按顾客类型统计 | |
| 70 | +- 按付款方式统计 | |
| 71 | + | |
| 72 | +#### 2.1.2 应收款报表(合作医院) | |
| 73 | +**统计维度**:按合作机构、门店、时间 | |
| 74 | +- 应收金额 = 实付业绩(`hgjg` 不为空 或 `fkyy` 不为空的开单) | |
| 75 | +- 已收金额 = 合作医院已支付的金额 | |
| 76 | +- 未收金额 = 应收金额 - 已收金额 | |
| 77 | + | |
| 78 | +**数据来源**: | |
| 79 | +- `hgjg` (合作机构) - 标识合作机构 | |
| 80 | +- `fkyy` (付款医院) - 标识付款医院 | |
| 81 | +- `sfyj` (实付业绩) - 应收金额 | |
| 82 | + | |
| 83 | +#### 2.1.3 预收款报表(银行存款) | |
| 84 | +**统计维度**:按门店、时间 | |
| 85 | +- 预收款总额 = 所有开单的实付业绩 (`sfyj`) | |
| 86 | +- 银行存款 = 预收款 - 应收(合作医院) | |
| 87 | +- 按付款方式分类(现金/微信/支付宝/银行卡等) | |
| 88 | + | |
| 89 | +**计算公式**: | |
| 90 | +``` | |
| 91 | +银行存款 = SUM(sfyj) - SUM(应收金额) | |
| 92 | +``` | |
| 93 | + | |
| 94 | +#### 2.1.4 品项分类收入报表 | |
| 95 | +**统计维度**:按品项分类、门店、时间 | |
| 96 | +- 生美收入(`ItemCategory = '生美'`) | |
| 97 | +- 科美收入(`ItemCategory = '科美'`) | |
| 98 | +- 医美收入(`ItemCategory = '医美'`) | |
| 99 | +- 产品收入(`ItemCategory = '产品'`) | |
| 100 | +- 其他收入 | |
| 101 | + | |
| 102 | +**数据来源**:`lq_kd_pxmx.F_ItemCategory` 关联 `lq_kd_kdjlb` | |
| 103 | + | |
| 104 | +--- | |
| 105 | + | |
| 106 | +### 2.2 成本类报表 | |
| 107 | + | |
| 108 | +#### 2.2.1 储扣成本报表 | |
| 109 | +**统计维度**:按门店、品项、时间 | |
| 110 | +- 储扣金额统计(`F_DeductAmount`) | |
| 111 | +- 储扣明细分析(`lq_kd_deductinfo`) | |
| 112 | +- 按扣减类型分类 | |
| 113 | +- 按品项分类统计储扣成本 | |
| 114 | + | |
| 115 | +**数据来源**: | |
| 116 | +- `lq_kd_kdjlb.F_DeductAmount` (储扣总金额) | |
| 117 | +- `lq_kd_deductinfo` (储扣明细表) | |
| 118 | + | |
| 119 | +#### 2.2.2 欠款成本报表 | |
| 120 | +**统计维度**:按门店、会员、时间 | |
| 121 | +- 欠款总额 (`qk`) | |
| 122 | +- 已缴欠款 (`F_PaidDebt`) | |
| 123 | +- 未缴欠款 = 欠款总额 - 已缴欠款 | |
| 124 | +- 欠款账龄分析 | |
| 125 | +- 大额欠款客户清单 | |
| 126 | + | |
| 127 | +--- | |
| 128 | + | |
| 129 | +### 2.3 现金流报表 | |
| 130 | + | |
| 131 | +#### 2.3.1 现金流日报/月报 | |
| 132 | +**统计维度**:按时间、门店 | |
| 133 | +- **期初余额**:上期期末余额 | |
| 134 | +- **本期收入**: | |
| 135 | + - 实付业绩 (`sfyj`) | |
| 136 | + - 补缴金额 (`F_SupplementAmount`) | |
| 137 | + - 已缴欠款 (`F_PaidDebt`) | |
| 138 | +- **本期支出**: | |
| 139 | + - 退款金额(关联 `lq_hytk_hytk.F_ActualRefundAmount`) | |
| 140 | +- **期末余额**:期初余额 + 本期收入 - 本期支出 | |
| 141 | + | |
| 142 | +#### 2.3.2 付款方式统计报表 | |
| 143 | +**统计维度**:按付款方式、门店、时间 | |
| 144 | +- 现金收款 (`fkfs = '现金'`) | |
| 145 | +- 微信收款 (`fkfs = '微信'`) | |
| 146 | +- 支付宝收款 (`fkfs = '支付宝'`) | |
| 147 | +- 银行卡收款 (`fkfs = '银行卡'`) | |
| 148 | +- 其他方式收款 | |
| 149 | + | |
| 150 | +--- | |
| 151 | + | |
| 152 | +### 2.4 应收应付报表 | |
| 153 | + | |
| 154 | +#### 2.4.1 应收账款明细表 | |
| 155 | +**统计维度**:按合作机构、门店、时间 | |
| 156 | +- 合作机构名称(关联 `hgjg`) | |
| 157 | +- 应收金额(该合作机构的开单实付业绩) | |
| 158 | +- 已收金额 | |
| 159 | +- 未收金额 | |
| 160 | +- 账龄分析 | |
| 161 | + | |
| 162 | +**数据来源**: | |
| 163 | +- `lq_kd_kdjlb.hgjg` (合作机构ID) | |
| 164 | +- `lq_kd_kdjlb.sfyj` (实付业绩) | |
| 165 | +- 关联合作机构表获取机构名称 | |
| 166 | + | |
| 167 | +#### 2.4.2 付款医院应收报表 | |
| 168 | +**统计维度**:按付款医院、门店、时间 | |
| 169 | +- 付款医院名称(关联 `fkyy`) | |
| 170 | +- 应收金额 | |
| 171 | +- 已收金额 | |
| 172 | +- 未收金额 | |
| 173 | + | |
| 174 | +--- | |
| 175 | + | |
| 176 | +### 2.5 利润分析报表 | |
| 177 | + | |
| 178 | +#### 2.5.1 开单利润分析表 | |
| 179 | +**统计维度**:按门店、时间、品项分类 | |
| 180 | +- **收入**:实付业绩 (`sfyj`) | |
| 181 | +- **成本**: | |
| 182 | + - 储扣成本 (`F_DeductAmount`) | |
| 183 | + - 退款成本(关联退款表) | |
| 184 | + - 合作成本(科美业绩的30%,参考门店股份统计) | |
| 185 | + - 管理费(业绩的9%,参考门店股份统计) | |
| 186 | +- **利润** = 收入 - 成本 | |
| 187 | + | |
| 188 | +**注意**:完整的利润计算需要关联其他成本表(人工工资、房租、库存成本等) | |
| 189 | + | |
| 190 | +#### 2.5.2 净收益报表 | |
| 191 | +**统计维度**:按门店、时间 | |
| 192 | +- 实付业绩 (`sfyj`) | |
| 193 | +- 退款金额(`lq_hytk_hytk.F_ActualRefundAmount`) | |
| 194 | +- 净收益 = 实付业绩 - 退款金额 | |
| 195 | + | |
| 196 | +--- | |
| 197 | + | |
| 198 | +### 2.6 业务分析报表 | |
| 199 | + | |
| 200 | +#### 2.6.1 首单分析报表 | |
| 201 | +**统计维度**:按门店、时间、顾客类型 | |
| 202 | +- 首单订单数 (`sfskdd = '是'`) | |
| 203 | +- 首单金额 | |
| 204 | +- 首单转化率 | |
| 205 | +- 首单客户后续消费分析 | |
| 206 | + | |
| 207 | +#### 2.6.2 升单分析报表 | |
| 208 | +**统计维度**:按门店、时间 | |
| 209 | +- 升生美订单数 (`F_UpgradeLifeBeauty`) | |
| 210 | +- 升科美订单数 (`F_UpgradeTechBeauty`) | |
| 211 | +- 升医美订单数 (`F_UpgradeMedicalBeauty`) | |
| 212 | +- 升单金额 | |
| 213 | +- 升单率分析 | |
| 214 | + | |
| 215 | +#### 2.6.3 客户来源分析报表 | |
| 216 | +**统计维度**:按客户来源 (`khly`)、门店、时间 | |
| 217 | +- 不同来源的开单数量 | |
| 218 | +- 不同来源的开单金额 | |
| 219 | +- 来源转化效果分析 | |
| 220 | + | |
| 221 | +#### 2.6.4 推荐人业绩报表 | |
| 222 | +**统计维度**:按推荐人 (`tjr`)、门店、时间 | |
| 223 | +- 推荐人推荐的开单数量 | |
| 224 | +- 推荐人推荐的开单金额 | |
| 225 | +- 推荐人排名 | |
| 226 | +- 推荐转化率 | |
| 227 | + | |
| 228 | +#### 2.6.5 营销活动效果报表 | |
| 229 | +**统计维度**:按营销活动 (`F_ActivityId`)、门店、时间 | |
| 230 | +- 活动参与订单数 | |
| 231 | +- 活动订单金额 | |
| 232 | +- 活动ROI分析 | |
| 233 | +- 活动转化率 | |
| 234 | + | |
| 235 | +--- | |
| 236 | + | |
| 237 | +### 2.7 分类统计报表 | |
| 238 | + | |
| 239 | +#### 2.7.1 品项分类统计报表 | |
| 240 | +**统计维度**:按品项分类、门店、时间 | |
| 241 | +- 生美/科美/医美/产品各分类的开单数量 | |
| 242 | +- 各分类的开单金额 | |
| 243 | +- 各分类的占比分析 | |
| 244 | +- 各分类的趋势分析 | |
| 245 | + | |
| 246 | +**数据来源**:`lq_kd_pxmx.F_ItemCategory` | |
| 247 | + | |
| 248 | +#### 2.7.2 顾客类型统计报表 | |
| 249 | +**统计维度**:按顾客类型 (`gjlx`)、门店、时间 | |
| 250 | +- 新客/老客/会员等类型的开单统计 | |
| 251 | +- 各类型客户的平均订单金额 | |
| 252 | +- 客户类型转化分析 | |
| 253 | + | |
| 254 | +#### 2.7.3 金三角业绩报表 | |
| 255 | +**统计维度**:按金三角 (`jsj`)、门店、时间 | |
| 256 | +- 各金三角的开单数量 | |
| 257 | +- 各金三角的开单金额 | |
| 258 | +- 金三角业绩排名 | |
| 259 | +- 金三角转化率 | |
| 260 | + | |
| 261 | +--- | |
| 262 | + | |
| 263 | +### 2.8 趋势分析报表 | |
| 264 | + | |
| 265 | +#### 2.8.1 开单趋势报表 | |
| 266 | +**统计维度**:按时间(日/周/月/年)、门店 | |
| 267 | +- 开单数量趋势 | |
| 268 | +- 开单金额趋势(整单业绩/实付业绩) | |
| 269 | +- 日均/月均开单分析 | |
| 270 | +- 同比增长率 | |
| 271 | + | |
| 272 | +#### 2.8.2 欠款趋势报表 | |
| 273 | +**统计维度**:按时间、门店 | |
| 274 | +- 欠款总额趋势 | |
| 275 | +- 已缴欠款趋势 | |
| 276 | +- 未缴欠款趋势 | |
| 277 | +- 欠款回收率 | |
| 278 | + | |
| 279 | +#### 2.8.3 储扣趋势报表 | |
| 280 | +**统计维度**:按时间、门店 | |
| 281 | +- 储扣金额趋势 | |
| 282 | +- 储扣占比趋势(储扣/实付业绩) | |
| 283 | +- 储扣使用率 | |
| 284 | + | |
| 285 | +--- | |
| 286 | + | |
| 287 | +### 2.9 对比分析报表 | |
| 288 | + | |
| 289 | +#### 2.9.1 门店对比报表 | |
| 290 | +**统计维度**:多门店对比、时间 | |
| 291 | +- 各门店开单数量对比 | |
| 292 | +- 各门店开单金额对比 | |
| 293 | +- 各门店平均订单金额对比 | |
| 294 | +- 各门店增长率对比 | |
| 295 | + | |
| 296 | +#### 2.9.2 时间周期对比报表 | |
| 297 | +**统计维度**:同比/环比 | |
| 298 | +- 去年同期对比 | |
| 299 | +- 上月对比 | |
| 300 | +- 环比增长率 | |
| 301 | +- 同比增长率 | |
| 302 | + | |
| 303 | +--- | |
| 304 | + | |
| 305 | +### 2.10 明细报表 | |
| 306 | + | |
| 307 | +#### 2.10.1 开单明细报表 | |
| 308 | +**统计维度**:按筛选条件 | |
| 309 | +- 开单编号 | |
| 310 | +- 开单日期 | |
| 311 | +- 门店 | |
| 312 | +- 会员信息 | |
| 313 | +- 品项明细(关联 `lq_kd_pxmx`) | |
| 314 | +- 金额明细(整单业绩/实付业绩/储扣/欠款) | |
| 315 | +- 付款方式 | |
| 316 | +- 合作机构 | |
| 317 | +- 推荐人 | |
| 318 | +- 营销活动 | |
| 319 | + | |
| 320 | +#### 2.10.2 储扣明细报表 | |
| 321 | +**统计维度**:按门店、时间、品项 | |
| 322 | +- 储扣记录明细(关联 `lq_kd_deductinfo`) | |
| 323 | +- 扣减类型 | |
| 324 | +- 扣减金额 | |
| 325 | +- 关联的开单信息 | |
| 326 | + | |
| 327 | +#### 2.10.3 补缴明细报表 | |
| 328 | +**统计维度**:按门店、时间、会员 | |
| 329 | +- 补缴开单记录(`F_SupplementBillingId` 不为空) | |
| 330 | +- 补缴金额 (`F_SupplementAmount`) | |
| 331 | +- 关联的原始开单 | |
| 332 | +- 补缴时间 | |
| 333 | + | |
| 334 | +--- | |
| 335 | + | |
| 336 | +## 三、报表所需关联数据 | |
| 337 | + | |
| 338 | +### 3.1 必须关联的表 | |
| 339 | +1. **`lq_kd_pxmx`** - 获取品项分类、品项明细 | |
| 340 | +2. **`lq_mdxx`** - 获取门店名称 | |
| 341 | +3. **`lq_khxx`** - 获取会员信息 | |
| 342 | +4. **`BASE_USER`** - 获取推荐人姓名、健康师姓名 | |
| 343 | +5. **`lq_hytk_hytk`** - 获取退款信息(计算净收益) | |
| 344 | +6. **`BASE_ORGANIZE`** - 获取合作机构名称 | |
| 345 | + | |
| 346 | +### 3.2 可选关联的表 | |
| 347 | +1. **`lq_kd_deductinfo`** - 储扣明细 | |
| 348 | +2. **`lq_kd_jksyj`** - 健康师业绩分配(用于成本分析) | |
| 349 | +3. **`lq_kd_kjbsyj`** - 科技部业绩分配(用于成本分析) | |
| 350 | +4. **`lq_event`** - 营销活动信息 | |
| 351 | +5. **`lq_yyjl`** - 预约记录(用于转化率分析) | |
| 352 | +6. **`lq_jsj_user`** - 金三角信息 | |
| 353 | + | |
| 354 | +--- | |
| 355 | + | |
| 356 | +## 四、财务报表优先级建议 | |
| 357 | + | |
| 358 | +### 4.1 高优先级(核心财务报表) | |
| 359 | +1. **开单收入统计表** - 基础收入数据 | |
| 360 | +2. **现金流报表** - 资金流动情况 | |
| 361 | +3. **净收益报表** - 扣除退款后的净收入 | |
| 362 | +4. **品项分类收入报表** - 业务结构分析 | |
| 363 | +5. **门店对比报表** - 门店经营对比 | |
| 364 | + | |
| 365 | +### 4.2 中优先级(重要分析报表) | |
| 366 | +1. **应收款报表(合作医院)** - 应收账款管理 | |
| 367 | +2. **欠款成本报表** - 资金风险管控 | |
| 368 | +3. **首单/升单分析报表** - 业务增长分析 | |
| 369 | +4. **营销活动效果报表** - 营销投入产出分析 | |
| 370 | +5. **开单趋势报表** - 经营趋势分析 | |
| 371 | + | |
| 372 | +### 4.3 低优先级(辅助分析报表) | |
| 373 | +1. **付款方式统计报表** - 收款渠道分析 | |
| 374 | +2. **客户来源分析报表** - 获客渠道分析 | |
| 375 | +3. **推荐人业绩报表** - 推荐激励机制分析 | |
| 376 | +4. **储扣成本报表** - 成本结构分析 | |
| 377 | + | |
| 378 | +--- | |
| 379 | + | |
| 380 | +## 五、报表统计规则 | |
| 381 | + | |
| 382 | +### 5.1 时间范围 | |
| 383 | +- **日统计**:按 `kdrq` 日期 | |
| 384 | +- **周统计**:按 `kdrq` 所在周 | |
| 385 | +- **月统计**:按 `kdrq` 所在月份 | |
| 386 | +- **年统计**:按 `kdrq` 所在年份 | |
| 387 | +- **自定义周期**:按指定的开始和结束日期 | |
| 388 | + | |
| 389 | +### 5.2 数据过滤条件 | |
| 390 | +- **有效记录**:`F_IsEffective = 1` | |
| 391 | +- **有金额记录**:`sfyj > 0` 或 `zdyj > 0` | |
| 392 | +- **门店筛选**:`djmd IN (门店ID列表)` | |
| 393 | +- **时间筛选**:`kdrq BETWEEN startTime AND endTime` | |
| 394 | + | |
| 395 | +### 5.3 去重规则 | |
| 396 | +- **开单数量**:COUNT(DISTINCT `F_Id`) | |
| 397 | +- **客户数量**:COUNT(DISTINCT `kdhy`) | |
| 398 | +- **品项数量**:COUNT(DISTINCT `lq_kd_pxmx.px`) | |
| 399 | + | |
| 400 | +### 5.4 金额汇总规则 | |
| 401 | +- **整单业绩**:SUM(`zdyj`) WHERE `F_IsEffective = 1` | |
| 402 | +- **实付业绩**:SUM(`sfyj`) WHERE `F_IsEffective = 1` AND `sfyj > 0` | |
| 403 | +- **储扣金额**:SUM(`F_DeductAmount`) WHERE `F_IsEffective = 1` | |
| 404 | +- **欠款金额**:SUM(`qk`) WHERE `F_IsEffective = 1` AND `qk > 0` | |
| 405 | +- **净收益**:SUM(`sfyj`) - SUM(退款金额) | |
| 406 | + | |
| 407 | +--- | |
| 408 | + | |
| 409 | +## 六、注意事项 | |
| 410 | + | |
| 411 | +### 6.1 数据一致性 | |
| 412 | +- 确保 `F_IsEffective = 1` 的记录才是有效开单 | |
| 413 | +- 退款数据需要关联 `lq_hytk_hytk` 表,使用 `F_ActualRefundAmount`(实退金额) | |
| 414 | +- 储扣金额可以从主表 `F_DeductAmount` 获取,或从明细表 `lq_kd_deductinfo` 汇总 | |
| 415 | + | |
| 416 | +### 6.2 时间字段使用 | |
| 417 | +- 统计时间以 `kdrq`(开单日期)为准 | |
| 418 | +- `F_CreateTime` 仅作为记录创建时间,不用于业务统计 | |
| 419 | + | |
| 420 | +### 6.3 金额字段使用 | |
| 421 | +- **预收款**:使用 `sfyj`(实付业绩) | |
| 422 | +- **订单总额**:使用 `zdyj`(整单业绩) | |
| 423 | +- **实际收款**:`sfyj`(已扣除储扣的金额) | |
| 424 | +- **储扣金额**:`F_DeductAmount`(会员权益抵扣) | |
| 425 | + | |
| 426 | +### 6.4 关联数据获取 | |
| 427 | +- 门店名称:关联 `lq_mdxx.dm` | |
| 428 | +- 会员信息:关联 `lq_khxx` | |
| 429 | +- 品项分类:关联 `lq_kd_pxmx.F_ItemCategory` | |
| 430 | +- 合作机构:关联合作机构表(通过 `hgjg`) | |
| 431 | + | |
| 432 | +--- | |
| 433 | + | |
| 434 | +## 七、报表接口建议 | |
| 435 | + | |
| 436 | +### 7.1 统一查询参数 | |
| 437 | +```csharp | |
| 438 | +public class FinancialReportQueryInput | |
| 439 | +{ | |
| 440 | + /// <summary> | |
| 441 | + /// 开始时间 | |
| 442 | + /// </summary> | |
| 443 | + public DateTime? StartTime { get; set; } | |
| 444 | + | |
| 445 | + /// <summary> | |
| 446 | + /// 结束时间 | |
| 447 | + /// </summary> | |
| 448 | + public DateTime? EndTime { get; set; } | |
| 449 | + | |
| 450 | + /// <summary> | |
| 451 | + /// 门店ID列表 | |
| 452 | + /// </summary> | |
| 453 | + public List<string> StoreIds { get; set; } | |
| 454 | + | |
| 455 | + /// <summary> | |
| 456 | + /// 报表类型 | |
| 457 | + /// </summary> | |
| 458 | + public string ReportType { get; set; } | |
| 459 | + | |
| 460 | + /// <summary> | |
| 461 | + /// 统计维度(日/周/月/年) | |
| 462 | + /// </summary> | |
| 463 | + public string PeriodType { get; set; } | |
| 464 | +} | |
| 465 | +``` | |
| 466 | + | |
| 467 | +### 7.2 统一输出格式 | |
| 468 | +- 包含统计周期信息 | |
| 469 | +- 包含筛选条件信息 | |
| 470 | +- 包含汇总数据 | |
| 471 | +- 包含明细数据(如需) | |
| 472 | +- 包含同比/环比数据(如需) | |
| 473 | + | |
| 474 | +--- | |
| 475 | + | |
| 476 | +## 八、扩展建议 | |
| 477 | + | |
| 478 | +### 8.1 报表缓存 | |
| 479 | +- 对于历史月份的数据,可以预计算并缓存 | |
| 480 | +- 减少实时查询压力 | |
| 481 | + | |
| 482 | +### 8.2 报表导出 | |
| 483 | +- 支持 Excel 导出 | |
| 484 | +- 支持 PDF 导出(可选) | |
| 485 | +- 支持自定义报表格式 | |
| 486 | + | |
| 487 | +### 8.3 报表权限 | |
| 488 | +- 不同角色查看不同维度的报表 | |
| 489 | +- 门店只能查看自己门店的报表 | |
| 490 | +- 总部可以查看所有门店报表 | |
| 491 | + | |
| 492 | +--- | |
| 493 | + | |
| 494 | +## 九、总结 | |
| 495 | + | |
| 496 | +基于 `lq_kd_kdjlb` 开单表,可以生成以下类型的财务报表: | |
| 497 | + | |
| 498 | +1. **收入类报表**(5种):开单收入、应收款、预收款、品项分类收入等 | |
| 499 | +2. **成本类报表**(2种):储扣成本、欠款成本 | |
| 500 | +3. **现金流报表**(2种):现金流日报/月报、付款方式统计 | |
| 501 | +4. **应收应付报表**(2种):应收账款明细、付款医院应收 | |
| 502 | +5. **利润分析报表**(2种):开单利润分析、净收益 | |
| 503 | +6. **业务分析报表**(5种):首单分析、升单分析、客户来源、推荐人、营销活动 | |
| 504 | +7. **分类统计报表**(3种):品项分类、顾客类型、金三角 | |
| 505 | +8. **趋势分析报表**(3种):开单趋势、欠款趋势、储扣趋势 | |
| 506 | +9. **对比分析报表**(2种):门店对比、时间周期对比 | |
| 507 | +10. **明细报表**(3种):开单明细、储扣明细、补缴明细 | |
| 508 | + | |
| 509 | +**总计:约29种不同类型的财务报表** | |
| 510 | + | |
| 511 | +这些报表可以满足财务分析、业务分析、经营决策等多种需求。 | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqFinancialReport/StoreCooperationPayableOutput.cs
0 → 100644
| 1 | +using System; | |
| 2 | +using System.Collections.Generic; | |
| 3 | + | |
| 4 | +namespace NCC.Extend.Entitys.Dto.LqFinancialReport | |
| 5 | +{ | |
| 6 | + /// <summary> | |
| 7 | + /// 门店合作机构应付统计输出 | |
| 8 | + /// </summary> | |
| 9 | + public class StoreCooperationPayableOutput | |
| 10 | + { | |
| 11 | + /// <summary> | |
| 12 | + /// 门店ID | |
| 13 | + /// </summary> | |
| 14 | + public string StoreId { get; set; } | |
| 15 | + | |
| 16 | + /// <summary> | |
| 17 | + /// 门店名称 | |
| 18 | + /// </summary> | |
| 19 | + public string StoreName { get; set; } | |
| 20 | + | |
| 21 | + /// <summary> | |
| 22 | + /// 统计日期(格式:yyyy-MM-dd 或 yyyy-MM) | |
| 23 | + /// </summary> | |
| 24 | + public string PeriodDate { get; set; } | |
| 25 | + | |
| 26 | + /// <summary> | |
| 27 | + /// 合作机构应付明细 | |
| 28 | + /// </summary> | |
| 29 | + public List<CooperationPayableItem> CooperationItems { get; set; } | |
| 30 | + | |
| 31 | + /// <summary> | |
| 32 | + /// 应付总额(所有合作机构合计) | |
| 33 | + /// </summary> | |
| 34 | + public decimal TotalPayable { get; set; } | |
| 35 | + } | |
| 36 | + | |
| 37 | + /// <summary> | |
| 38 | + /// 合作机构应付明细项 | |
| 39 | + /// </summary> | |
| 40 | + public class CooperationPayableItem | |
| 41 | + { | |
| 42 | + /// <summary> | |
| 43 | + /// 合作机构ID | |
| 44 | + /// </summary> | |
| 45 | + public string CooperationId { get; set; } | |
| 46 | + | |
| 47 | + /// <summary> | |
| 48 | + /// 合作机构名称 | |
| 49 | + /// </summary> | |
| 50 | + public string CooperationName { get; set; } | |
| 51 | + | |
| 52 | + /// <summary> | |
| 53 | + /// 应付金额(开单金额) | |
| 54 | + /// </summary> | |
| 55 | + public decimal PayableAmount { get; set; } | |
| 56 | + | |
| 57 | + /// <summary> | |
| 58 | + /// 开单笔数 | |
| 59 | + /// </summary> | |
| 60 | + public int BillingCount { get; set; } | |
| 61 | + } | |
| 62 | +} | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqFinancialReport/StoreFinancialReportQueryInput.cs
0 → 100644
| 1 | +using System; | |
| 2 | +using System.Collections.Generic; | |
| 3 | + | |
| 4 | +namespace NCC.Extend.Entitys.Dto.LqFinancialReport | |
| 5 | +{ | |
| 6 | + /// <summary> | |
| 7 | + /// 门店财务报表查询输入 | |
| 8 | + /// </summary> | |
| 9 | + public class StoreFinancialReportQueryInput | |
| 10 | + { | |
| 11 | + /// <summary> | |
| 12 | + /// 开始时间(必填) | |
| 13 | + /// </summary> | |
| 14 | + public DateTime? StartTime { get; set; } | |
| 15 | + | |
| 16 | + /// <summary> | |
| 17 | + /// 结束时间(必填) | |
| 18 | + /// </summary> | |
| 19 | + public DateTime? EndTime { get; set; } | |
| 20 | + | |
| 21 | + /// <summary> | |
| 22 | + /// 门店ID列表(可选,为空则查询所有门店) | |
| 23 | + /// </summary> | |
| 24 | + public List<string> StoreIds { get; set; } | |
| 25 | + | |
| 26 | + /// <summary> | |
| 27 | + /// 统计周期类型:day-按日统计,month-按月统计 | |
| 28 | + /// </summary> | |
| 29 | + public string PeriodType { get; set; } = "day"; | |
| 30 | + } | |
| 31 | +} | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqFinancialReport/StorePaymentChannelIncomeOutput.cs
0 → 100644
| 1 | +using System; | |
| 2 | +using System.Collections.Generic; | |
| 3 | + | |
| 4 | +namespace NCC.Extend.Entitys.Dto.LqFinancialReport | |
| 5 | +{ | |
| 6 | + /// <summary> | |
| 7 | + /// 门店收款渠道收入统计输出 | |
| 8 | + /// </summary> | |
| 9 | + public class StorePaymentChannelIncomeOutput | |
| 10 | + { | |
| 11 | + /// <summary> | |
| 12 | + /// 门店ID | |
| 13 | + /// </summary> | |
| 14 | + public string StoreId { get; set; } | |
| 15 | + | |
| 16 | + /// <summary> | |
| 17 | + /// 门店名称 | |
| 18 | + /// </summary> | |
| 19 | + public string StoreName { get; set; } | |
| 20 | + | |
| 21 | + /// <summary> | |
| 22 | + /// 统计日期(格式:yyyy-MM-dd 或 yyyy-MM) | |
| 23 | + /// </summary> | |
| 24 | + public string PeriodDate { get; set; } | |
| 25 | + | |
| 26 | + /// <summary> | |
| 27 | + /// 收款渠道收入明细 | |
| 28 | + /// </summary> | |
| 29 | + public List<PaymentChannelItem> PaymentChannels { get; set; } | |
| 30 | + | |
| 31 | + /// <summary> | |
| 32 | + /// 总收入(所有渠道合计) | |
| 33 | + /// </summary> | |
| 34 | + public decimal TotalIncome { get; set; } | |
| 35 | + } | |
| 36 | + | |
| 37 | + /// <summary> | |
| 38 | + /// 收款渠道明细项 | |
| 39 | + /// </summary> | |
| 40 | + public class PaymentChannelItem | |
| 41 | + { | |
| 42 | + /// <summary> | |
| 43 | + /// 付款方式(现金/微信/支付宝/银行卡等) | |
| 44 | + /// </summary> | |
| 45 | + public string PaymentMethod { get; set; } | |
| 46 | + | |
| 47 | + /// <summary> | |
| 48 | + /// 收款金额 | |
| 49 | + /// </summary> | |
| 50 | + public decimal Amount { get; set; } | |
| 51 | + | |
| 52 | + /// <summary> | |
| 53 | + /// 收款笔数 | |
| 54 | + /// </summary> | |
| 55 | + public int Count { get; set; } | |
| 56 | + | |
| 57 | + /// <summary> | |
| 58 | + /// 占比(百分比) | |
| 59 | + /// </summary> | |
| 60 | + public decimal Percentage { get; set; } | |
| 61 | + } | |
| 62 | +} | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqFinancialReport/StorePaymentHospitalReceivableOutput.cs
0 → 100644
| 1 | +using System; | |
| 2 | +using System.Collections.Generic; | |
| 3 | + | |
| 4 | +namespace NCC.Extend.Entitys.Dto.LqFinancialReport | |
| 5 | +{ | |
| 6 | + /// <summary> | |
| 7 | + /// 门店付款医院应收统计输出 | |
| 8 | + /// </summary> | |
| 9 | + public class StorePaymentHospitalReceivableOutput | |
| 10 | + { | |
| 11 | + /// <summary> | |
| 12 | + /// 门店ID | |
| 13 | + /// </summary> | |
| 14 | + public string StoreId { get; set; } | |
| 15 | + | |
| 16 | + /// <summary> | |
| 17 | + /// 门店名称 | |
| 18 | + /// </summary> | |
| 19 | + public string StoreName { get; set; } | |
| 20 | + | |
| 21 | + /// <summary> | |
| 22 | + /// 统计日期(格式:yyyy-MM-dd 或 yyyy-MM) | |
| 23 | + /// </summary> | |
| 24 | + public string PeriodDate { get; set; } | |
| 25 | + | |
| 26 | + /// <summary> | |
| 27 | + /// 付款医院应收明细 | |
| 28 | + /// </summary> | |
| 29 | + public List<PaymentHospitalReceivableItem> HospitalItems { get; set; } | |
| 30 | + | |
| 31 | + /// <summary> | |
| 32 | + /// 应收总额(所有付款医院合计) | |
| 33 | + /// </summary> | |
| 34 | + public decimal TotalReceivable { get; set; } | |
| 35 | + } | |
| 36 | + | |
| 37 | + /// <summary> | |
| 38 | + /// 付款医院应收明细项 | |
| 39 | + /// </summary> | |
| 40 | + public class PaymentHospitalReceivableItem | |
| 41 | + { | |
| 42 | + /// <summary> | |
| 43 | + /// 付款医院ID | |
| 44 | + /// </summary> | |
| 45 | + public string HospitalId { get; set; } | |
| 46 | + | |
| 47 | + /// <summary> | |
| 48 | + /// 付款医院名称 | |
| 49 | + /// </summary> | |
| 50 | + public string HospitalName { get; set; } | |
| 51 | + | |
| 52 | + /// <summary> | |
| 53 | + /// 应收金额(开单金额) | |
| 54 | + /// </summary> | |
| 55 | + public decimal ReceivableAmount { get; set; } | |
| 56 | + | |
| 57 | + /// <summary> | |
| 58 | + /// 开单笔数 | |
| 59 | + /// </summary> | |
| 60 | + public int BillingCount { get; set; } | |
| 61 | + } | |
| 62 | +} | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqFinancialReport/StoreTotalIncomeOutput.cs
0 → 100644
| 1 | +using System; | |
| 2 | +using System.Collections.Generic; | |
| 3 | + | |
| 4 | +namespace NCC.Extend.Entitys.Dto.LqFinancialReport | |
| 5 | +{ | |
| 6 | + /// <summary> | |
| 7 | + /// 门店总收入统计输出 | |
| 8 | + /// </summary> | |
| 9 | + public class StoreTotalIncomeOutput | |
| 10 | + { | |
| 11 | + /// <summary> | |
| 12 | + /// 门店ID | |
| 13 | + /// </summary> | |
| 14 | + public string StoreId { get; set; } | |
| 15 | + | |
| 16 | + /// <summary> | |
| 17 | + /// 门店名称 | |
| 18 | + /// </summary> | |
| 19 | + public string StoreName { get; set; } | |
| 20 | + | |
| 21 | + /// <summary> | |
| 22 | + /// 统计日期(格式:yyyy-MM-dd 或 yyyy-MM) | |
| 23 | + /// </summary> | |
| 24 | + public string PeriodDate { get; set; } | |
| 25 | + | |
| 26 | + /// <summary> | |
| 27 | + /// 总收入(所有开单的实付业绩汇总) | |
| 28 | + /// </summary> | |
| 29 | + public decimal TotalIncome { get; set; } | |
| 30 | + | |
| 31 | + /// <summary> | |
| 32 | + /// 开单笔数 | |
| 33 | + /// </summary> | |
| 34 | + public int BillingCount { get; set; } | |
| 35 | + | |
| 36 | + /// <summary> | |
| 37 | + /// 平均单笔金额 | |
| 38 | + /// </summary> | |
| 39 | + public decimal AverageAmount { get; set; } | |
| 40 | + } | |
| 41 | +} | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxBirthdayOutput.cs
0 → 100644
| 1 | +using System; | |
| 2 | + | |
| 3 | +namespace NCC.Extend.Entitys.Dto.LqKhxx | |
| 4 | +{ | |
| 5 | + /// <summary> | |
| 6 | + /// 会员生日信息输出DTO | |
| 7 | + /// </summary> | |
| 8 | + public class LqKhxxBirthdayOutput | |
| 9 | + { | |
| 10 | + /// <summary> | |
| 11 | + /// 会员ID | |
| 12 | + /// </summary> | |
| 13 | + public string id { get; set; } | |
| 14 | + | |
| 15 | + /// <summary> | |
| 16 | + /// 会员名称 | |
| 17 | + /// </summary> | |
| 18 | + public string khmc { get; set; } | |
| 19 | + | |
| 20 | + /// <summary> | |
| 21 | + /// 手机号 | |
| 22 | + /// </summary> | |
| 23 | + public string sjh { get; set; } | |
| 24 | + | |
| 25 | + /// <summary> | |
| 26 | + /// 档案号 | |
| 27 | + /// </summary> | |
| 28 | + public string dah { get; set; } | |
| 29 | + | |
| 30 | + /// <summary> | |
| 31 | + /// 性别 | |
| 32 | + /// </summary> | |
| 33 | + public string xb { get; set; } | |
| 34 | + | |
| 35 | + /// <summary> | |
| 36 | + /// 归属门店ID | |
| 37 | + /// </summary> | |
| 38 | + public string gsmd { get; set; } | |
| 39 | + | |
| 40 | + /// <summary> | |
| 41 | + /// 归属门店名称 | |
| 42 | + /// </summary> | |
| 43 | + public string gsmdName { get; set; } | |
| 44 | + | |
| 45 | + /// <summary> | |
| 46 | + /// 阳历生日 | |
| 47 | + /// </summary> | |
| 48 | + public DateTime? yanglsr { get; set; } | |
| 49 | + | |
| 50 | + /// <summary> | |
| 51 | + /// 阴历生日 | |
| 52 | + /// </summary> | |
| 53 | + public DateTime? yinlsr { get; set; } | |
| 54 | + | |
| 55 | + /// <summary> | |
| 56 | + /// 生日日期(用于日历显示,格式:MM-DD) | |
| 57 | + /// </summary> | |
| 58 | + public string birthdayDate { get; set; } | |
| 59 | + | |
| 60 | + /// <summary> | |
| 61 | + /// 生日完整日期(用于日历显示,格式:YYYY-MM-DD) | |
| 62 | + /// </summary> | |
| 63 | + public DateTime birthdayFullDate { get; set; } | |
| 64 | + | |
| 65 | + /// <summary> | |
| 66 | + /// 消费等级(0=D,1=C,2=B,3=A,4=A+,5=A++) | |
| 67 | + /// </summary> | |
| 68 | + public int consumeLevel { get; set; } | |
| 69 | + | |
| 70 | + /// <summary> | |
| 71 | + /// 消费等级名称 | |
| 72 | + /// </summary> | |
| 73 | + public string consumeLevelName { get; set; } | |
| 74 | + | |
| 75 | + /// <summary> | |
| 76 | + /// 剩余权益总金额 | |
| 77 | + /// </summary> | |
| 78 | + public decimal remainingRightsAmount { get; set; } | |
| 79 | + | |
| 80 | + /// <summary> | |
| 81 | + /// 备注 | |
| 82 | + /// </summary> | |
| 83 | + public string bz { get; set; } | |
| 84 | + | |
| 85 | + /// <summary> | |
| 86 | + /// 年龄 | |
| 87 | + /// </summary> | |
| 88 | + public int? age { get; set; } | |
| 89 | + } | |
| 90 | +} | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxListQueryInput.cs
| ... | ... | @@ -117,5 +117,25 @@ namespace NCC.Extend.Entitys.Dto.LqKhxx |
| 117 | 117 | /// 阴历生日 |
| 118 | 118 | /// </summary> |
| 119 | 119 | public string yinlsr { get; set; } |
| 120 | + | |
| 121 | + /// <summary> | |
| 122 | + /// 消费等级(0=D,1=C,2=B,3=A,4=A+,5=A++) | |
| 123 | + /// </summary> | |
| 124 | + public int? ConsumeLevel { get; set; } | |
| 125 | + | |
| 126 | + /// <summary> | |
| 127 | + /// 沉睡天数范围(格式: "最小值,最大值") | |
| 128 | + /// </summary> | |
| 129 | + public string SleepDaysRange { get; set; } | |
| 130 | + | |
| 131 | + /// <summary> | |
| 132 | + /// 主健康师 | |
| 133 | + /// </summary> | |
| 134 | + public string mainHealthUser { get; set; } | |
| 135 | + | |
| 136 | + /// <summary> | |
| 137 | + /// 副健康师 | |
| 138 | + /// </summary> | |
| 139 | + public string subHealthUser { get; set; } | |
| 120 | 140 | } |
| 121 | 141 | } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTkjlb/ExpansionEmployeeStatisticsInput.cs
0 → 100644
| 1 | +using System; | |
| 2 | + | |
| 3 | +namespace NCC.Extend.Entitys.Dto.LqTkjlb | |
| 4 | +{ | |
| 5 | + /// <summary> | |
| 6 | + /// 拓客部员工统计报表输入 | |
| 7 | + /// </summary> | |
| 8 | + public class ExpansionEmployeeStatisticsInput | |
| 9 | + { | |
| 10 | + /// <summary> | |
| 11 | + /// 开始时间(必填) | |
| 12 | + /// </summary> | |
| 13 | + public DateTime? StartTime { get; set; } | |
| 14 | + | |
| 15 | + /// <summary> | |
| 16 | + /// 结束时间(必填) | |
| 17 | + /// </summary> | |
| 18 | + public DateTime? EndTime { get; set; } | |
| 19 | + | |
| 20 | + /// <summary> | |
| 21 | + /// 活动ID(可选) | |
| 22 | + /// </summary> | |
| 23 | + public string EventId { get; set; } | |
| 24 | + | |
| 25 | + /// <summary> | |
| 26 | + /// 门店ID数组(可选) | |
| 27 | + /// </summary> | |
| 28 | + public string[] StoreId { get; set; } | |
| 29 | + } | |
| 30 | +} | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTkjlb/ExpansionEmployeeStatisticsOutput.cs
0 → 100644
| 1 | +using System; | |
| 2 | +using System.Collections.Generic; | |
| 3 | + | |
| 4 | +namespace NCC.Extend.Entitys.Dto.LqTkjlb | |
| 5 | +{ | |
| 6 | + /// <summary> | |
| 7 | + /// 拓客部员工统计报表输出 | |
| 8 | + /// </summary> | |
| 9 | + public class ExpansionEmployeeStatisticsOutput | |
| 10 | + { | |
| 11 | + /// <summary> | |
| 12 | + /// 周期开始时间 | |
| 13 | + /// </summary> | |
| 14 | + public DateTime? StartTime { get; set; } | |
| 15 | + | |
| 16 | + /// <summary> | |
| 17 | + /// 周期结束时间 | |
| 18 | + /// </summary> | |
| 19 | + public DateTime? EndTime { get; set; } | |
| 20 | + | |
| 21 | + /// <summary> | |
| 22 | + /// 员工列表 | |
| 23 | + /// </summary> | |
| 24 | + public List<ExpansionEmployeeStatisticsItem> Employees { get; set; } | |
| 25 | + } | |
| 26 | + | |
| 27 | + /// <summary> | |
| 28 | + /// 拓客部员工统计数据项 | |
| 29 | + /// </summary> | |
| 30 | + public class ExpansionEmployeeStatisticsItem | |
| 31 | + { | |
| 32 | + /// <summary> | |
| 33 | + /// 员工ID | |
| 34 | + /// </summary> | |
| 35 | + public string EmployeeId { get; set; } | |
| 36 | + | |
| 37 | + /// <summary> | |
| 38 | + /// 员工姓名 | |
| 39 | + /// </summary> | |
| 40 | + public string EmployeeName { get; set; } | |
| 41 | + | |
| 42 | + /// <summary> | |
| 43 | + /// 部门ID | |
| 44 | + /// </summary> | |
| 45 | + public string DepartmentId { get; set; } | |
| 46 | + | |
| 47 | + /// <summary> | |
| 48 | + /// 部门名称 | |
| 49 | + /// </summary> | |
| 50 | + public string DepartmentName { get; set; } | |
| 51 | + | |
| 52 | + /// <summary> | |
| 53 | + /// 岗位 | |
| 54 | + /// </summary> | |
| 55 | + public string Position { get; set; } | |
| 56 | + | |
| 57 | + /// <summary> | |
| 58 | + /// 拓客人数(去重会员数) | |
| 59 | + /// </summary> | |
| 60 | + public int ExpansionCount { get; set; } | |
| 61 | + | |
| 62 | + /// <summary> | |
| 63 | + /// 到店人数(去重会员数) | |
| 64 | + /// </summary> | |
| 65 | + public int VisitCount { get; set; } | |
| 66 | + | |
| 67 | + /// <summary> | |
| 68 | + /// 开单人数(去重会员数) | |
| 69 | + /// </summary> | |
| 70 | + public int BillingCount { get; set; } | |
| 71 | + | |
| 72 | + /// <summary> | |
| 73 | + /// 开单金额 | |
| 74 | + /// </summary> | |
| 75 | + public decimal BillingAmount { get; set; } | |
| 76 | + } | |
| 77 | +} | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqFinancialReportService.cs
0 → 100644
| 1 | +using System; | |
| 2 | +using System.Collections.Generic; | |
| 3 | +using System.Linq; | |
| 4 | +using System.Threading.Tasks; | |
| 5 | +using Microsoft.AspNetCore.Mvc; | |
| 6 | +using Microsoft.Extensions.Logging; | |
| 7 | +using NCC.Common.Core.Manager; | |
| 8 | +using NCC.Common.Enum; | |
| 9 | +using NCC.Dependency; | |
| 10 | +using NCC.DynamicApiController; | |
| 11 | +using NCC.FriendlyException; | |
| 12 | +using NCC.Extend.Entitys.Dto.LqFinancialReport; | |
| 13 | +using NCC.Extend.Entitys.Enum; | |
| 14 | +using NCC.Extend.Entitys.lq_kd_kdjlb; | |
| 15 | +using NCC.Extend.Entitys.lq_mdxx; | |
| 16 | +using NCC.Extend.Entitys.lq_hzf; | |
| 17 | +using SqlSugar; | |
| 18 | + | |
| 19 | +namespace NCC.Extend | |
| 20 | +{ | |
| 21 | + /// <summary> | |
| 22 | + /// 财务报表服务 | |
| 23 | + /// </summary> | |
| 24 | + [ApiDescriptionSettings(Tag = "绿纤财务报表服务", Name = "LqFinancialReport", Order = 201)] | |
| 25 | + [Route("api/Extend/[controller]")] | |
| 26 | + public class LqFinancialReportService : IDynamicApiController, ITransient | |
| 27 | + { | |
| 28 | + private readonly ISqlSugarClient _db; | |
| 29 | + private readonly IUserManager _userManager; | |
| 30 | + private readonly ILogger<LqFinancialReportService> _logger; | |
| 31 | + | |
| 32 | + /// <summary> | |
| 33 | + /// 构造函数 | |
| 34 | + /// </summary> | |
| 35 | + public LqFinancialReportService(ISqlSugarClient db, IUserManager userManager, ILogger<LqFinancialReportService> logger) | |
| 36 | + { | |
| 37 | + _db = db; | |
| 38 | + _userManager = userManager; | |
| 39 | + _logger = logger; | |
| 40 | + } | |
| 41 | + | |
| 42 | + #region 门店收款渠道收入统计 | |
| 43 | + | |
| 44 | + /// <summary> | |
| 45 | + /// 获取门店收款渠道收入统计 | |
| 46 | + /// </summary> | |
| 47 | + /// <remarks> | |
| 48 | + /// 统计每个门店不同收款渠道的收入(按日或按月) | |
| 49 | + /// | |
| 50 | + /// 示例请求: | |
| 51 | + /// ```json | |
| 52 | + /// { | |
| 53 | + /// "startTime": "2025-01-01T00:00:00", | |
| 54 | + /// "endTime": "2025-01-31T23:59:59", | |
| 55 | + /// "periodType": "day", | |
| 56 | + /// "storeIds": ["门店ID1", "门店ID2"] | |
| 57 | + /// } | |
| 58 | + /// ``` | |
| 59 | + /// | |
| 60 | + /// 参数说明: | |
| 61 | + /// - startTime: 开始时间(必填) | |
| 62 | + /// - endTime: 结束时间(必填) | |
| 63 | + /// - periodType: 统计周期类型,day-按日统计,month-按月统计(默认day) | |
| 64 | + /// - storeIds: 门店ID列表(可选,为空则查询所有门店) | |
| 65 | + /// | |
| 66 | + /// 返回字段说明: | |
| 67 | + /// - StoreId: 门店ID | |
| 68 | + /// - StoreName: 门店名称 | |
| 69 | + /// - PeriodDate: 统计日期(yyyy-MM-dd 或 yyyy-MM) | |
| 70 | + /// - PaymentChannels: 收款渠道明细列表 | |
| 71 | + /// - TotalIncome: 总收入 | |
| 72 | + /// </remarks> | |
| 73 | + /// <param name="input">查询参数</param> | |
| 74 | + /// <returns>门店收款渠道收入统计数据</returns> | |
| 75 | + /// <response code="200">成功返回统计数据</response> | |
| 76 | + /// <response code="400">参数错误</response> | |
| 77 | + /// <response code="500">服务器内部错误</response> | |
| 78 | + [HttpPost("get-store-payment-channel-income")] | |
| 79 | + public async Task<List<StorePaymentChannelIncomeOutput>> GetStorePaymentChannelIncome(StoreFinancialReportQueryInput input) | |
| 80 | + { | |
| 81 | + try | |
| 82 | + { | |
| 83 | + if (input == null || !input.StartTime.HasValue || !input.EndTime.HasValue) | |
| 84 | + { | |
| 85 | + throw NCCException.Oh("开始时间和结束时间不能为空"); | |
| 86 | + } | |
| 87 | + | |
| 88 | + var startTime = input.StartTime.Value; | |
| 89 | + var endTime = input.EndTime.Value; | |
| 90 | + var periodType = string.IsNullOrEmpty(input.PeriodType) ? "day" : input.PeriodType.ToLower(); | |
| 91 | + | |
| 92 | + // 构建门店过滤条件 | |
| 93 | + string storeFilter = ""; | |
| 94 | + if (input.StoreIds != null && input.StoreIds.Any() && input.StoreIds.Any(s => !string.IsNullOrWhiteSpace(s))) | |
| 95 | + { | |
| 96 | + var storeIdsStr = string.Join("','", input.StoreIds.Where(s => !string.IsNullOrWhiteSpace(s))); | |
| 97 | + storeFilter = $"AND kd.djmd IN ('{storeIdsStr}')"; | |
| 98 | + } | |
| 99 | + | |
| 100 | + // 根据统计周期类型构建SQL | |
| 101 | + string dateFormat = periodType == "month" ? "%Y-%m" : "%Y-%m-%d"; | |
| 102 | + string dateGroupBy = periodType == "month" ? "DATE_FORMAT(kd.kdrq, '%Y-%m')" : "DATE_FORMAT(kd.kdrq, '%Y-%m-%d')"; | |
| 103 | + | |
| 104 | + var sql = $@" | |
| 105 | + SELECT | |
| 106 | + kd.djmd AS StoreId, | |
| 107 | + md.dm AS StoreName, | |
| 108 | + {dateGroupBy} AS PeriodDate, | |
| 109 | + COALESCE(kd.fkfs, '未填写') AS PaymentMethod, | |
| 110 | + COUNT(DISTINCT kd.F_Id) AS Count, | |
| 111 | + SUM(kd.sfyj) AS Amount | |
| 112 | + FROM lq_kd_kdjlb kd | |
| 113 | + INNER JOIN lq_mdxx md ON kd.djmd = md.F_Id | |
| 114 | + WHERE kd.F_IsEffective = 1 | |
| 115 | + AND kd.sfyj > 0 | |
| 116 | + AND kd.kdrq >= '{startTime:yyyy-MM-dd 00:00:00}' | |
| 117 | + AND kd.kdrq <= '{endTime:yyyy-MM-dd 23:59:59}' | |
| 118 | + {storeFilter} | |
| 119 | + GROUP BY kd.djmd, md.dm, {dateGroupBy}, kd.fkfs | |
| 120 | + ORDER BY kd.djmd, PeriodDate, Amount DESC"; | |
| 121 | + | |
| 122 | + var rawData = await _db.Ado.SqlQueryAsync<dynamic>(sql); | |
| 123 | + | |
| 124 | + // 按门店和日期分组处理数据 | |
| 125 | + var result = new List<StorePaymentChannelIncomeOutput>(); | |
| 126 | + var groupedData = rawData | |
| 127 | + .GroupBy(x => new { StoreId = x.StoreId?.ToString(), StoreName = x.StoreName?.ToString(), PeriodDate = x.PeriodDate?.ToString() }); | |
| 128 | + | |
| 129 | + foreach (var group in groupedData) | |
| 130 | + { | |
| 131 | + var channelItems = group.Select(item => new PaymentChannelItem | |
| 132 | + { | |
| 133 | + PaymentMethod = item.PaymentMethod?.ToString() ?? "未填写", | |
| 134 | + Amount = Convert.ToDecimal(item.Amount ?? 0), | |
| 135 | + Count = Convert.ToInt32(item.Count ?? 0) | |
| 136 | + }).ToList(); | |
| 137 | + | |
| 138 | + var totalIncome = channelItems.Sum(x => x.Amount); | |
| 139 | + foreach (var item in channelItems) | |
| 140 | + { | |
| 141 | + item.Percentage = totalIncome > 0 ? Math.Round(item.Amount / totalIncome * 100, 2) : 0; | |
| 142 | + } | |
| 143 | + | |
| 144 | + result.Add(new StorePaymentChannelIncomeOutput | |
| 145 | + { | |
| 146 | + StoreId = group.Key.StoreId ?? "", | |
| 147 | + StoreName = group.Key.StoreName ?? "", | |
| 148 | + PeriodDate = group.Key.PeriodDate ?? "", | |
| 149 | + PaymentChannels = channelItems, | |
| 150 | + TotalIncome = totalIncome | |
| 151 | + }); | |
| 152 | + } | |
| 153 | + | |
| 154 | + return result; | |
| 155 | + } | |
| 156 | + catch (Exception ex) | |
| 157 | + { | |
| 158 | + _logger.LogError(ex, "获取门店收款渠道收入统计失败"); | |
| 159 | + throw NCCException.Oh($"获取门店收款渠道收入统计失败: {ex.Message}"); | |
| 160 | + } | |
| 161 | + } | |
| 162 | + | |
| 163 | + #endregion | |
| 164 | + | |
| 165 | + #region 门店合作机构应付统计 | |
| 166 | + | |
| 167 | + /// <summary> | |
| 168 | + /// 获取门店合作机构应付统计 | |
| 169 | + /// </summary> | |
| 170 | + /// <remarks> | |
| 171 | + /// 统计每个门店合作机构的应付金额(按日或按月) | |
| 172 | + /// 合作机构是指选择了合作机构的开单,需要付款给合作机构的金额 | |
| 173 | + /// | |
| 174 | + /// 示例请求: | |
| 175 | + /// ```json | |
| 176 | + /// { | |
| 177 | + /// "startTime": "2025-01-01T00:00:00", | |
| 178 | + /// "endTime": "2025-01-31T23:59:59", | |
| 179 | + /// "periodType": "month", | |
| 180 | + /// "storeIds": ["门店ID1"] | |
| 181 | + /// } | |
| 182 | + /// ``` | |
| 183 | + /// | |
| 184 | + /// 参数说明: | |
| 185 | + /// - startTime: 开始时间(必填) | |
| 186 | + /// - endTime: 结束时间(必填) | |
| 187 | + /// - periodType: 统计周期类型,day-按日统计,month-按月统计(默认day) | |
| 188 | + /// - storeIds: 门店ID列表(可选) | |
| 189 | + /// | |
| 190 | + /// 返回字段说明: | |
| 191 | + /// - StoreId: 门店ID | |
| 192 | + /// - StoreName: 门店名称 | |
| 193 | + /// - PeriodDate: 统计日期 | |
| 194 | + /// - CooperationItems: 合作机构应付明细列表 | |
| 195 | + /// - TotalPayable: 应付总额 | |
| 196 | + /// </remarks> | |
| 197 | + /// <param name="input">查询参数</param> | |
| 198 | + /// <returns>门店合作机构应付统计数据</returns> | |
| 199 | + /// <response code="200">成功返回统计数据</response> | |
| 200 | + /// <response code="400">参数错误</response> | |
| 201 | + /// <response code="500">服务器内部错误</response> | |
| 202 | + [HttpPost("get-store-cooperation-payable")] | |
| 203 | + public async Task<List<StoreCooperationPayableOutput>> GetStoreCooperationPayable(StoreFinancialReportQueryInput input) | |
| 204 | + { | |
| 205 | + try | |
| 206 | + { | |
| 207 | + if (input == null || !input.StartTime.HasValue || !input.EndTime.HasValue) | |
| 208 | + { | |
| 209 | + throw NCCException.Oh("开始时间和结束时间不能为空"); | |
| 210 | + } | |
| 211 | + | |
| 212 | + var startTime = input.StartTime.Value; | |
| 213 | + var endTime = input.EndTime.Value; | |
| 214 | + var periodType = string.IsNullOrEmpty(input.PeriodType) ? "day" : input.PeriodType.ToLower(); | |
| 215 | + | |
| 216 | + // 构建门店过滤条件 | |
| 217 | + string storeFilter = ""; | |
| 218 | + if (input.StoreIds != null && input.StoreIds.Any() && input.StoreIds.Any(s => !string.IsNullOrWhiteSpace(s))) | |
| 219 | + { | |
| 220 | + var storeIdsStr = string.Join("','", input.StoreIds.Where(s => !string.IsNullOrWhiteSpace(s))); | |
| 221 | + storeFilter = $"AND kd.djmd IN ('{storeIdsStr}')"; | |
| 222 | + } | |
| 223 | + | |
| 224 | + // 根据统计周期类型构建SQL | |
| 225 | + string dateGroupBy = periodType == "month" ? "DATE_FORMAT(kd.kdrq, '%Y-%m')" : "DATE_FORMAT(kd.kdrq, '%Y-%m-%d')"; | |
| 226 | + | |
| 227 | + var sql = $@" | |
| 228 | + SELECT | |
| 229 | + kd.djmd AS StoreId, | |
| 230 | + md.dm AS StoreName, | |
| 231 | + {dateGroupBy} AS PeriodDate, | |
| 232 | + kd.hgjg AS CooperationId, | |
| 233 | + COALESCE(hz.hzmc, '未知合作机构') AS CooperationName, | |
| 234 | + COUNT(DISTINCT kd.F_Id) AS BillingCount, | |
| 235 | + SUM(kd.sfyj) AS PayableAmount | |
| 236 | + FROM lq_kd_kdjlb kd | |
| 237 | + INNER JOIN lq_mdxx md ON kd.djmd = md.F_Id | |
| 238 | + LEFT JOIN lq_hzf hz ON kd.hgjg = hz.F_Id | |
| 239 | + WHERE kd.F_IsEffective = 1 | |
| 240 | + AND kd.sfyj > 0 | |
| 241 | + AND kd.hgjg IS NOT NULL | |
| 242 | + AND kd.hgjg != '' | |
| 243 | + AND kd.kdrq >= '{startTime:yyyy-MM-dd 00:00:00}' | |
| 244 | + AND kd.kdrq <= '{endTime:yyyy-MM-dd 23:59:59}' | |
| 245 | + {storeFilter} | |
| 246 | + GROUP BY kd.djmd, md.dm, {dateGroupBy}, kd.hgjg, hz.hzmc | |
| 247 | + ORDER BY kd.djmd, PeriodDate, PayableAmount DESC"; | |
| 248 | + | |
| 249 | + var rawData = await _db.Ado.SqlQueryAsync<dynamic>(sql); | |
| 250 | + | |
| 251 | + // 按门店和日期分组处理数据 | |
| 252 | + var result = new List<StoreCooperationPayableOutput>(); | |
| 253 | + var groupedData = rawData | |
| 254 | + .GroupBy(x => new { StoreId = x.StoreId?.ToString(), StoreName = x.StoreName?.ToString(), PeriodDate = x.PeriodDate?.ToString() }); | |
| 255 | + | |
| 256 | + foreach (var group in groupedData) | |
| 257 | + { | |
| 258 | + var cooperationItems = group.Select(item => new CooperationPayableItem | |
| 259 | + { | |
| 260 | + CooperationId = item.CooperationId?.ToString() ?? "", | |
| 261 | + CooperationName = item.CooperationName?.ToString() ?? "未知合作机构", | |
| 262 | + PayableAmount = Convert.ToDecimal(item.PayableAmount ?? 0), | |
| 263 | + BillingCount = Convert.ToInt32(item.BillingCount ?? 0) | |
| 264 | + }).ToList(); | |
| 265 | + | |
| 266 | + result.Add(new StoreCooperationPayableOutput | |
| 267 | + { | |
| 268 | + StoreId = group.Key.StoreId ?? "", | |
| 269 | + StoreName = group.Key.StoreName ?? "", | |
| 270 | + PeriodDate = group.Key.PeriodDate ?? "", | |
| 271 | + CooperationItems = cooperationItems, | |
| 272 | + TotalPayable = cooperationItems.Sum(x => x.PayableAmount) | |
| 273 | + }); | |
| 274 | + } | |
| 275 | + | |
| 276 | + return result; | |
| 277 | + } | |
| 278 | + catch (Exception ex) | |
| 279 | + { | |
| 280 | + _logger.LogError(ex, "获取门店合作机构应付统计失败"); | |
| 281 | + throw NCCException.Oh($"获取门店合作机构应付统计失败: {ex.Message}"); | |
| 282 | + } | |
| 283 | + } | |
| 284 | + | |
| 285 | + #endregion | |
| 286 | + | |
| 287 | + #region 门店付款医院应收统计 | |
| 288 | + | |
| 289 | + /// <summary> | |
| 290 | + /// 获取门店付款医院应收统计 | |
| 291 | + /// </summary> | |
| 292 | + /// <remarks> | |
| 293 | + /// 统计每个门店付款医院的应收金额(按日或按月) | |
| 294 | + /// 付款医院是指选择了付款医院的开单,需要从医院收款的金额 | |
| 295 | + /// | |
| 296 | + /// 示例请求: | |
| 297 | + /// ```json | |
| 298 | + /// { | |
| 299 | + /// "startTime": "2025-01-01T00:00:00", | |
| 300 | + /// "endTime": "2025-01-31T23:59:59", | |
| 301 | + /// "periodType": "month", | |
| 302 | + /// "storeIds": ["门店ID1"] | |
| 303 | + /// } | |
| 304 | + /// ``` | |
| 305 | + /// | |
| 306 | + /// 参数说明: | |
| 307 | + /// - startTime: 开始时间(必填) | |
| 308 | + /// - endTime: 结束时间(必填) | |
| 309 | + /// - periodType: 统计周期类型,day-按日统计,month-按月统计(默认day) | |
| 310 | + /// - storeIds: 门店ID列表(可选) | |
| 311 | + /// | |
| 312 | + /// 返回字段说明: | |
| 313 | + /// - StoreId: 门店ID | |
| 314 | + /// - StoreName: 门店名称 | |
| 315 | + /// - PeriodDate: 统计日期 | |
| 316 | + /// - HospitalItems: 付款医院应收明细列表 | |
| 317 | + /// - TotalReceivable: 应收总额 | |
| 318 | + /// </remarks> | |
| 319 | + /// <param name="input">查询参数</param> | |
| 320 | + /// <returns>门店付款医院应收统计数据</returns> | |
| 321 | + /// <response code="200">成功返回统计数据</response> | |
| 322 | + /// <response code="400">参数错误</response> | |
| 323 | + /// <response code="500">服务器内部错误</response> | |
| 324 | + [HttpPost("get-store-payment-hospital-receivable")] | |
| 325 | + public async Task<List<StorePaymentHospitalReceivableOutput>> GetStorePaymentHospitalReceivable(StoreFinancialReportQueryInput input) | |
| 326 | + { | |
| 327 | + try | |
| 328 | + { | |
| 329 | + if (input == null || !input.StartTime.HasValue || !input.EndTime.HasValue) | |
| 330 | + { | |
| 331 | + throw NCCException.Oh("开始时间和结束时间不能为空"); | |
| 332 | + } | |
| 333 | + | |
| 334 | + var startTime = input.StartTime.Value; | |
| 335 | + var endTime = input.EndTime.Value; | |
| 336 | + var periodType = string.IsNullOrEmpty(input.PeriodType) ? "day" : input.PeriodType.ToLower(); | |
| 337 | + | |
| 338 | + // 构建门店过滤条件 | |
| 339 | + string storeFilter = ""; | |
| 340 | + if (input.StoreIds != null && input.StoreIds.Any() && input.StoreIds.Any(s => !string.IsNullOrWhiteSpace(s))) | |
| 341 | + { | |
| 342 | + var storeIdsStr = string.Join("','", input.StoreIds.Where(s => !string.IsNullOrWhiteSpace(s))); | |
| 343 | + storeFilter = $"AND kd.djmd IN ('{storeIdsStr}')"; | |
| 344 | + } | |
| 345 | + | |
| 346 | + // 根据统计周期类型构建SQL | |
| 347 | + string dateGroupBy = periodType == "month" ? "DATE_FORMAT(kd.kdrq, '%Y-%m')" : "DATE_FORMAT(kd.kdrq, '%Y-%m-%d')"; | |
| 348 | + | |
| 349 | + var sql = $@" | |
| 350 | + SELECT | |
| 351 | + kd.djmd AS StoreId, | |
| 352 | + md.dm AS StoreName, | |
| 353 | + {dateGroupBy} AS PeriodDate, | |
| 354 | + kd.fkyy AS HospitalId, | |
| 355 | + COALESCE(hz.hzmc, '未知付款医院') AS HospitalName, | |
| 356 | + COUNT(DISTINCT kd.F_Id) AS BillingCount, | |
| 357 | + SUM(kd.sfyj) AS ReceivableAmount | |
| 358 | + FROM lq_kd_kdjlb kd | |
| 359 | + INNER JOIN lq_mdxx md ON kd.djmd = md.F_Id | |
| 360 | + LEFT JOIN lq_hzf hz ON kd.fkyy = hz.F_Id | |
| 361 | + WHERE kd.F_IsEffective = 1 | |
| 362 | + AND kd.sfyj > 0 | |
| 363 | + AND kd.fkyy IS NOT NULL | |
| 364 | + AND kd.fkyy != '' | |
| 365 | + AND kd.kdrq >= '{startTime:yyyy-MM-dd 00:00:00}' | |
| 366 | + AND kd.kdrq <= '{endTime:yyyy-MM-dd 23:59:59}' | |
| 367 | + {storeFilter} | |
| 368 | + GROUP BY kd.djmd, md.dm, {dateGroupBy}, kd.fkyy, hz.hzmc | |
| 369 | + ORDER BY kd.djmd, PeriodDate, ReceivableAmount DESC"; | |
| 370 | + | |
| 371 | + var rawData = await _db.Ado.SqlQueryAsync<dynamic>(sql); | |
| 372 | + | |
| 373 | + // 按门店和日期分组处理数据 | |
| 374 | + var result = new List<StorePaymentHospitalReceivableOutput>(); | |
| 375 | + var groupedData = rawData | |
| 376 | + .GroupBy(x => new { StoreId = x.StoreId?.ToString(), StoreName = x.StoreName?.ToString(), PeriodDate = x.PeriodDate?.ToString() }); | |
| 377 | + | |
| 378 | + foreach (var group in groupedData) | |
| 379 | + { | |
| 380 | + var hospitalItems = group.Select(item => new PaymentHospitalReceivableItem | |
| 381 | + { | |
| 382 | + HospitalId = item.HospitalId?.ToString() ?? "", | |
| 383 | + HospitalName = item.HospitalName?.ToString() ?? "未知付款医院", | |
| 384 | + ReceivableAmount = Convert.ToDecimal(item.ReceivableAmount ?? 0), | |
| 385 | + BillingCount = Convert.ToInt32(item.BillingCount ?? 0) | |
| 386 | + }).ToList(); | |
| 387 | + | |
| 388 | + result.Add(new StorePaymentHospitalReceivableOutput | |
| 389 | + { | |
| 390 | + StoreId = group.Key.StoreId ?? "", | |
| 391 | + StoreName = group.Key.StoreName ?? "", | |
| 392 | + PeriodDate = group.Key.PeriodDate ?? "", | |
| 393 | + HospitalItems = hospitalItems, | |
| 394 | + TotalReceivable = hospitalItems.Sum(x => x.ReceivableAmount) | |
| 395 | + }); | |
| 396 | + } | |
| 397 | + | |
| 398 | + return result; | |
| 399 | + } | |
| 400 | + catch (Exception ex) | |
| 401 | + { | |
| 402 | + _logger.LogError(ex, "获取门店付款医院应收统计失败"); | |
| 403 | + throw NCCException.Oh($"获取门店付款医院应收统计失败: {ex.Message}"); | |
| 404 | + } | |
| 405 | + } | |
| 406 | + | |
| 407 | + #endregion | |
| 408 | + | |
| 409 | + #region 门店总收入统计 | |
| 410 | + | |
| 411 | + /// <summary> | |
| 412 | + /// 获取门店总收入统计 | |
| 413 | + /// </summary> | |
| 414 | + /// <remarks> | |
| 415 | + /// 统计每个门店的总收入(按日或按月) | |
| 416 | + /// 总收入 = 所有开单的实付业绩(sfyj)汇总 | |
| 417 | + /// | |
| 418 | + /// 示例请求: | |
| 419 | + /// ```json | |
| 420 | + /// { | |
| 421 | + /// "startTime": "2025-01-01T00:00:00", | |
| 422 | + /// "endTime": "2025-01-31T23:59:59", | |
| 423 | + /// "periodType": "month", | |
| 424 | + /// "storeIds": ["门店ID1", "门店ID2"] | |
| 425 | + /// } | |
| 426 | + /// ``` | |
| 427 | + /// | |
| 428 | + /// 参数说明: | |
| 429 | + /// - startTime: 开始时间(必填) | |
| 430 | + /// - endTime: 结束时间(必填) | |
| 431 | + /// - periodType: 统计周期类型,day-按日统计,month-按月统计(默认day) | |
| 432 | + /// - storeIds: 门店ID列表(可选,为空则查询所有门店) | |
| 433 | + /// | |
| 434 | + /// 返回字段说明: | |
| 435 | + /// - StoreId: 门店ID | |
| 436 | + /// - StoreName: 门店名称 | |
| 437 | + /// - PeriodDate: 统计日期(yyyy-MM-dd 或 yyyy-MM) | |
| 438 | + /// - TotalIncome: 总收入 | |
| 439 | + /// - BillingCount: 开单笔数 | |
| 440 | + /// - AverageAmount: 平均单笔金额 | |
| 441 | + /// </remarks> | |
| 442 | + /// <param name="input">查询参数</param> | |
| 443 | + /// <returns>门店总收入统计数据</returns> | |
| 444 | + /// <response code="200">成功返回统计数据</response> | |
| 445 | + /// <response code="400">参数错误</response> | |
| 446 | + /// <response code="500">服务器内部错误</response> | |
| 447 | + [HttpPost("get-store-total-income")] | |
| 448 | + public async Task<List<StoreTotalIncomeOutput>> GetStoreTotalIncome(StoreFinancialReportQueryInput input) | |
| 449 | + { | |
| 450 | + try | |
| 451 | + { | |
| 452 | + if (input == null || !input.StartTime.HasValue || !input.EndTime.HasValue) | |
| 453 | + { | |
| 454 | + throw NCCException.Oh("开始时间和结束时间不能为空"); | |
| 455 | + } | |
| 456 | + | |
| 457 | + var startTime = input.StartTime.Value; | |
| 458 | + var endTime = input.EndTime.Value; | |
| 459 | + var periodType = string.IsNullOrEmpty(input.PeriodType) ? "day" : input.PeriodType.ToLower(); | |
| 460 | + | |
| 461 | + // 构建门店过滤条件 | |
| 462 | + string storeFilter = ""; | |
| 463 | + if (input.StoreIds != null && input.StoreIds.Any() && input.StoreIds.Any(s => !string.IsNullOrWhiteSpace(s))) | |
| 464 | + { | |
| 465 | + var storeIdsStr = string.Join("','", input.StoreIds.Where(s => !string.IsNullOrWhiteSpace(s))); | |
| 466 | + storeFilter = $"AND kd.djmd IN ('{storeIdsStr}')"; | |
| 467 | + } | |
| 468 | + | |
| 469 | + // 根据统计周期类型构建SQL | |
| 470 | + string dateGroupBy = periodType == "month" ? "DATE_FORMAT(kd.kdrq, '%Y-%m')" : "DATE_FORMAT(kd.kdrq, '%Y-%m-%d')"; | |
| 471 | + | |
| 472 | + var sql = $@" | |
| 473 | + SELECT | |
| 474 | + kd.djmd AS StoreId, | |
| 475 | + md.dm AS StoreName, | |
| 476 | + {dateGroupBy} AS PeriodDate, | |
| 477 | + COUNT(DISTINCT kd.F_Id) AS BillingCount, | |
| 478 | + SUM(kd.sfyj) AS TotalIncome | |
| 479 | + FROM lq_kd_kdjlb kd | |
| 480 | + INNER JOIN lq_mdxx md ON kd.djmd = md.F_Id | |
| 481 | + WHERE kd.F_IsEffective = 1 | |
| 482 | + AND kd.sfyj > 0 | |
| 483 | + AND kd.kdrq >= '{startTime:yyyy-MM-dd 00:00:00}' | |
| 484 | + AND kd.kdrq <= '{endTime:yyyy-MM-dd 23:59:59}' | |
| 485 | + {storeFilter} | |
| 486 | + GROUP BY kd.djmd, md.dm, {dateGroupBy} | |
| 487 | + ORDER BY kd.djmd, PeriodDate"; | |
| 488 | + | |
| 489 | + var rawData = await _db.Ado.SqlQueryAsync<dynamic>(sql); | |
| 490 | + | |
| 491 | + var result = rawData.Select(item => new StoreTotalIncomeOutput | |
| 492 | + { | |
| 493 | + StoreId = item.StoreId?.ToString() ?? "", | |
| 494 | + StoreName = item.StoreName?.ToString() ?? "", | |
| 495 | + PeriodDate = item.PeriodDate?.ToString() ?? "", | |
| 496 | + TotalIncome = Convert.ToDecimal(item.TotalIncome ?? 0), | |
| 497 | + BillingCount = Convert.ToInt32(item.BillingCount ?? 0), | |
| 498 | + AverageAmount = Convert.ToInt32(item.BillingCount ?? 0) > 0 | |
| 499 | + ? Math.Round(Convert.ToDecimal(item.TotalIncome ?? 0) / Convert.ToInt32(item.BillingCount ?? 0), 2) | |
| 500 | + : 0 | |
| 501 | + }).ToList(); | |
| 502 | + | |
| 503 | + return result; | |
| 504 | + } | |
| 505 | + catch (Exception ex) | |
| 506 | + { | |
| 507 | + _logger.LogError(ex, "获取门店总收入统计失败"); | |
| 508 | + throw NCCException.Oh($"获取门店总收入统计失败: {ex.Message}"); | |
| 509 | + } | |
| 510 | + } | |
| 511 | + | |
| 512 | + #endregion | |
| 513 | + } | |
| 514 | +} | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs
| ... | ... | @@ -124,6 +124,19 @@ namespace NCC.Extend.LqKhxx |
| 124 | 124 | List<string> queryZcsj = input.zcsj != null ? input.zcsj.Split(',').ToObeject<List<string>>() : null; |
| 125 | 125 | DateTime? startZcsj = queryZcsj != null ? Ext.GetDateTime(queryZcsj.First()) : null; |
| 126 | 126 | DateTime? endZcsj = queryZcsj != null ? Ext.GetDateTime(queryZcsj.Last()) : null; |
| 127 | + | |
| 128 | + // 处理沉睡天数范围 | |
| 129 | + List<string> querySleepDaysRange = input.SleepDaysRange != null ? input.SleepDaysRange.Split(',').ToObeject<List<string>>() : null; | |
| 130 | + int? minSleepDays = null; | |
| 131 | + int? maxSleepDays = null; | |
| 132 | + if (querySleepDaysRange != null && querySleepDaysRange.Count >= 2) | |
| 133 | + { | |
| 134 | + int.TryParse(querySleepDaysRange[0], out int min); | |
| 135 | + int.TryParse(querySleepDaysRange[1], out int max); | |
| 136 | + minSleepDays = min; | |
| 137 | + maxSleepDays = max; | |
| 138 | + } | |
| 139 | + | |
| 127 | 140 | var data = await _db.Queryable<LqKhxxEntity>() |
| 128 | 141 | .WhereIF(!string.IsNullOrEmpty(input.keyWord), p => p.Khmc.Contains(input.keyWord) || p.Sjh.Contains(input.keyWord) || p.Dah.Contains(input.keyWord)) |
| 129 | 142 | .WhereIF(input.IsTechMemberbh.HasValue, p => p.IsTechMember == input.IsTechMemberbh) |
| ... | ... | @@ -134,6 +147,8 @@ namespace NCC.Extend.LqKhxx |
| 134 | 147 | .WhereIF(!string.IsNullOrEmpty(input.dah), p => p.Dah.Contains(input.dah)) |
| 135 | 148 | .WhereIF(!string.IsNullOrEmpty(input.xb), p => p.Xb.Equals(input.xb)) |
| 136 | 149 | .WhereIF(!string.IsNullOrEmpty(input.gsmd), p => p.Gsmd.Contains(input.gsmd)) |
| 150 | + .WhereIF(!string.IsNullOrEmpty(input.mainHealthUser), p => p.MainHealthUser.Contains(input.mainHealthUser)) | |
| 151 | + .WhereIF(!string.IsNullOrEmpty(input.subHealthUser), p => p.SubHealthUser.Contains(input.subHealthUser)) | |
| 137 | 152 | .WhereIF(!string.IsNullOrEmpty(input.khlx), p => p.Khlx.Equals(input.khlx)) |
| 138 | 153 | .WhereIF(!string.IsNullOrEmpty(input.khjd), p => p.Khjd.Equals(input.khjd)) |
| 139 | 154 | .WhereIF(!string.IsNullOrEmpty(input.khxf), p => p.Khxf.Contains(input.khxf)) |
| ... | ... | @@ -142,6 +157,9 @@ namespace NCC.Extend.LqKhxx |
| 142 | 157 | .WhereIF(!string.IsNullOrEmpty(input.jdqd), p => p.Jdqd.Contains(input.jdqd)) |
| 143 | 158 | .WhereIF(!string.IsNullOrEmpty(input.lxdz), p => p.Lxdz.Contains(input.lxdz)) |
| 144 | 159 | .WhereIF(!string.IsNullOrEmpty(input.bz), p => p.Bz.Contains(input.bz)) |
| 160 | + .WhereIF(input.ConsumeLevel.HasValue, p => p.ConsumeLevel == input.ConsumeLevel.Value) | |
| 161 | + .WhereIF(minSleepDays.HasValue, p => p.SleepDays >= minSleepDays.Value) | |
| 162 | + .WhereIF(maxSleepDays.HasValue, p => p.SleepDays <= maxSleepDays.Value) | |
| 145 | 163 | .WhereIF(queryZcsj != null, p => p.CreateTime >= new DateTime(startZcsj.ToDate().Year, startZcsj.ToDate().Month, startZcsj.ToDate().Day, 0, 0, 0)) |
| 146 | 164 | .WhereIF(queryZcsj != null, p => p.CreateTime <= new DateTime(endZcsj.ToDate().Year, endZcsj.ToDate().Month, endZcsj.ToDate().Day, 23, 59, 59)) |
| 147 | 165 | .Select(it => new LqKhxxListOutput |
| ... | ... | @@ -251,6 +269,19 @@ namespace NCC.Extend.LqKhxx |
| 251 | 269 | List<string> queryYinlsr = input.yinlsr != null ? input.yinlsr.Split(',').ToObeject<List<string>>() : null; |
| 252 | 270 | DateTime? startYinlsr = queryYinlsr != null ? Ext.GetDateTime(queryYinlsr.First()) : null; |
| 253 | 271 | DateTime? endYinlsr = queryYinlsr != null ? Ext.GetDateTime(queryYinlsr.Last()) : null; |
| 272 | + | |
| 273 | + // 处理沉睡天数范围 | |
| 274 | + List<string> querySleepDaysRange = input.SleepDaysRange != null ? input.SleepDaysRange.Split(',').ToObeject<List<string>>() : null; | |
| 275 | + int? minSleepDays = null; | |
| 276 | + int? maxSleepDays = null; | |
| 277 | + if (querySleepDaysRange != null && querySleepDaysRange.Count >= 2) | |
| 278 | + { | |
| 279 | + int.TryParse(querySleepDaysRange[0], out int min); | |
| 280 | + int.TryParse(querySleepDaysRange[1], out int max); | |
| 281 | + minSleepDays = min; | |
| 282 | + maxSleepDays = max; | |
| 283 | + } | |
| 284 | + | |
| 254 | 285 | var data = await _db.Queryable<LqKhxxEntity>() |
| 255 | 286 | .WhereIF(!string.IsNullOrEmpty(input.id), p => p.Id.Contains(input.id)) |
| 256 | 287 | .WhereIF(!string.IsNullOrEmpty(input.khmc), p => p.Khmc.Contains(input.khmc)) |
| ... | ... | @@ -258,6 +289,8 @@ namespace NCC.Extend.LqKhxx |
| 258 | 289 | .WhereIF(!string.IsNullOrEmpty(input.dah), p => p.Dah.Contains(input.dah)) |
| 259 | 290 | .WhereIF(!string.IsNullOrEmpty(input.xb), p => p.Xb.Equals(input.xb)) |
| 260 | 291 | .WhereIF(!string.IsNullOrEmpty(input.gsmd), p => p.Gsmd.Contains(input.gsmd)) |
| 292 | + .WhereIF(!string.IsNullOrEmpty(input.mainHealthUser), p => p.MainHealthUser.Contains(input.mainHealthUser)) | |
| 293 | + .WhereIF(!string.IsNullOrEmpty(input.subHealthUser), p => p.SubHealthUser.Contains(input.subHealthUser)) | |
| 261 | 294 | .WhereIF(queryZcsj != null, p => p.Zcsj >= new DateTime(startZcsj.ToDate().Year, startZcsj.ToDate().Month, startZcsj.ToDate().Day, 0, 0, 0)) |
| 262 | 295 | .WhereIF(queryZcsj != null, p => p.Zcsj <= new DateTime(endZcsj.ToDate().Year, endZcsj.ToDate().Month, endZcsj.ToDate().Day, 23, 59, 59)) |
| 263 | 296 | .WhereIF(!string.IsNullOrEmpty(input.khlx), p => p.Khlx.Equals(input.khlx)) |
| ... | ... | @@ -268,6 +301,9 @@ namespace NCC.Extend.LqKhxx |
| 268 | 301 | .WhereIF(!string.IsNullOrEmpty(input.jdqd), p => p.Jdqd.Contains(input.jdqd)) |
| 269 | 302 | .WhereIF(!string.IsNullOrEmpty(input.lxdz), p => p.Lxdz.Contains(input.lxdz)) |
| 270 | 303 | .WhereIF(!string.IsNullOrEmpty(input.bz), p => p.Bz.Contains(input.bz)) |
| 304 | + .WhereIF(input.ConsumeLevel.HasValue, p => p.ConsumeLevel == input.ConsumeLevel.Value) | |
| 305 | + .WhereIF(minSleepDays.HasValue, p => p.SleepDays >= minSleepDays.Value) | |
| 306 | + .WhereIF(maxSleepDays.HasValue, p => p.SleepDays <= maxSleepDays.Value) | |
| 271 | 307 | .WhereIF(queryYanglsr != null, p => p.Yanglsr >= new DateTime(startYanglsr.ToDate().Year, startYanglsr.ToDate().Month, startYanglsr.ToDate().Day, 0, 0, 0)) |
| 272 | 308 | .WhereIF(queryYanglsr != null, p => p.Yanglsr <= new DateTime(endYanglsr.ToDate().Year, endYanglsr.ToDate().Month, endYanglsr.ToDate().Day, 23, 59, 59)) |
| 273 | 309 | .WhereIF(queryYinlsr != null, p => p.Yinlsr >= new DateTime(startYinlsr.ToDate().Year, startYinlsr.ToDate().Month, startYinlsr.ToDate().Day, 0, 0, 0)) |
| ... | ... | @@ -629,6 +665,18 @@ namespace NCC.Extend.LqKhxx |
| 629 | 665 | List<string> queryYanglsr = input.yanglsr != null ? input.yanglsr.Split(',').ToObeject<List<string>>() : null; |
| 630 | 666 | DateTime? startYanglsr = queryYanglsr != null ? Ext.GetDateTime(queryYanglsr.First()) : null; |
| 631 | 667 | DateTime? endYanglsr = queryYanglsr != null ? Ext.GetDateTime(queryYanglsr.Last()) : null; |
| 668 | + | |
| 669 | + // 处理沉睡天数范围 | |
| 670 | + List<string> querySleepDaysRange = input.SleepDaysRange != null ? input.SleepDaysRange.Split(',').ToObeject<List<string>>() : null; | |
| 671 | + int? minSleepDays = null; | |
| 672 | + int? maxSleepDays = null; | |
| 673 | + if (querySleepDaysRange != null && querySleepDaysRange.Count >= 2) | |
| 674 | + { | |
| 675 | + int.TryParse(querySleepDaysRange[0], out int min); | |
| 676 | + int.TryParse(querySleepDaysRange[1], out int max); | |
| 677 | + minSleepDays = min; | |
| 678 | + maxSleepDays = max; | |
| 679 | + } | |
| 632 | 680 | |
| 633 | 681 | // 先查询客户基础数据(使用子查询方式,类似GetNoPagingList,但优化为批量查询关联数据) |
| 634 | 682 | var customerList = await _db.Queryable<LqKhxxEntity>() |
| ... | ... | @@ -638,6 +686,8 @@ namespace NCC.Extend.LqKhxx |
| 638 | 686 | .WhereIF(!string.IsNullOrEmpty(input.dah), p => p.Dah.Contains(input.dah)) |
| 639 | 687 | .WhereIF(!string.IsNullOrEmpty(input.xb), p => p.Xb.Equals(input.xb)) |
| 640 | 688 | .WhereIF(!string.IsNullOrEmpty(input.gsmd), p => p.Gsmd.Contains(input.gsmd)) |
| 689 | + .WhereIF(!string.IsNullOrEmpty(input.mainHealthUser), p => p.MainHealthUser.Contains(input.mainHealthUser)) | |
| 690 | + .WhereIF(!string.IsNullOrEmpty(input.subHealthUser), p => p.SubHealthUser.Contains(input.subHealthUser)) | |
| 641 | 691 | .WhereIF(queryZcsj != null, p => p.Zcsj >= new DateTime(startZcsj.ToDate().Year, startZcsj.ToDate().Month, startZcsj.ToDate().Day, 0, 0, 0)) |
| 642 | 692 | .WhereIF(queryZcsj != null, p => p.Zcsj <= new DateTime(endZcsj.ToDate().Year, endZcsj.ToDate().Month, endZcsj.ToDate().Day, 23, 59, 59)) |
| 643 | 693 | .WhereIF(!string.IsNullOrEmpty(input.khlx), p => p.Khlx.Equals(input.khlx)) |
| ... | ... | @@ -647,6 +697,9 @@ namespace NCC.Extend.LqKhxx |
| 647 | 697 | .WhereIF(!string.IsNullOrEmpty(input.tjr), p => p.Tjr.Contains(input.tjr)) |
| 648 | 698 | .WhereIF(!string.IsNullOrEmpty(input.jdqd), p => p.Jdqd.Contains(input.jdqd)) |
| 649 | 699 | .WhereIF(!string.IsNullOrEmpty(input.lxdz), p => p.Lxdz.Contains(input.lxdz)) |
| 700 | + .WhereIF(input.ConsumeLevel.HasValue, p => p.ConsumeLevel == input.ConsumeLevel.Value) | |
| 701 | + .WhereIF(minSleepDays.HasValue, p => p.SleepDays >= minSleepDays.Value) | |
| 702 | + .WhereIF(maxSleepDays.HasValue, p => p.SleepDays <= maxSleepDays.Value) | |
| 650 | 703 | .WhereIF(!string.IsNullOrEmpty(input.bz), p => p.Bz.Contains(input.bz)) |
| 651 | 704 | .WhereIF(queryYanglsr != null, p => p.Yanglsr >= new DateTime(startYanglsr.ToDate().Year, startYanglsr.ToDate().Month, startYanglsr.ToDate().Day, 0, 0, 0)) |
| 652 | 705 | .WhereIF(queryYanglsr != null, p => p.Yanglsr <= new DateTime(endYanglsr.ToDate().Year, endYanglsr.ToDate().Month, endYanglsr.ToDate().Day, 23, 59, 59)) |
| ... | ... | @@ -2486,5 +2539,163 @@ WHERE kh.F_IsEffective = 1"; |
| 2486 | 2539 | } |
| 2487 | 2540 | #endregion |
| 2488 | 2541 | |
| 2542 | + #region 会员生日管理 | |
| 2543 | + | |
| 2544 | + /// <summary> | |
| 2545 | + /// 获取门店未来30天过生日的会员列表 | |
| 2546 | + /// </summary> | |
| 2547 | + /// <remarks> | |
| 2548 | + /// 根据门店ID获取未来30天内过生日的会员信息,支持按阳历生日查询 | |
| 2549 | + /// | |
| 2550 | + /// 会员等级说明:0=D,1=C,2=B,3=A,4=A+,5=A++ | |
| 2551 | + /// | |
| 2552 | + /// 示例请求: | |
| 2553 | + /// ```http | |
| 2554 | + /// GET /api/Extend/LqKhxx/GetUpcomingBirthdays?storeId=门店ID | |
| 2555 | + /// ``` | |
| 2556 | + /// | |
| 2557 | + /// 参数说明: | |
| 2558 | + /// - storeId: 门店ID(可选,不传则查询所有门店) | |
| 2559 | + /// </remarks> | |
| 2560 | + /// <param name="storeId">门店ID(可选)</param> | |
| 2561 | + /// <returns>会员生日信息列表,包含会员等级、生日日期、剩余权益等信息</returns> | |
| 2562 | + /// <response code="200">成功返回会员生日列表</response> | |
| 2563 | + /// <response code="500">服务器错误</response> | |
| 2564 | + [HttpGet("GetUpcomingBirthdays")] | |
| 2565 | + public async Task<dynamic> GetUpcomingBirthdays(string storeId = null) | |
| 2566 | + { | |
| 2567 | + try | |
| 2568 | + { | |
| 2569 | + var now = DateTime.Now; | |
| 2570 | + var endDate = now.AddDays(30); | |
| 2571 | + | |
| 2572 | + // 构建查询 | |
| 2573 | + var query = _db.Queryable<LqKhxxEntity>() | |
| 2574 | + .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode()) | |
| 2575 | + .Where(x => x.Yanglsr != null); // 必须有阳历生日 | |
| 2576 | + | |
| 2577 | + // 门店过滤 | |
| 2578 | + if (!string.IsNullOrEmpty(storeId)) | |
| 2579 | + { | |
| 2580 | + query = query.Where(x => x.Gsmd == storeId); | |
| 2581 | + } | |
| 2582 | + | |
| 2583 | + // 获取所有会员数据 | |
| 2584 | + var members = await query.ToListAsync(); | |
| 2585 | + | |
| 2586 | + // 在内存中筛选未来30天过生日的会员 | |
| 2587 | + var upcomingBirthdays = new List<LqKhxxBirthdayOutput>(); | |
| 2588 | + | |
| 2589 | + foreach (var member in members) | |
| 2590 | + { | |
| 2591 | + if (!member.Yanglsr.HasValue) continue; | |
| 2592 | + | |
| 2593 | + var birthday = member.Yanglsr.Value; | |
| 2594 | + | |
| 2595 | + // 计算今年和明年的生日 | |
| 2596 | + var thisYearBirthday = new DateTime(now.Year, birthday.Month, birthday.Day); | |
| 2597 | + var nextYearBirthday = new DateTime(now.Year + 1, birthday.Month, birthday.Day); | |
| 2598 | + | |
| 2599 | + DateTime upcomingBirthday; | |
| 2600 | + | |
| 2601 | + // 判断生日是今年还是明年 | |
| 2602 | + if (thisYearBirthday >= now.Date && thisYearBirthday <= endDate.Date) | |
| 2603 | + { | |
| 2604 | + upcomingBirthday = thisYearBirthday; | |
| 2605 | + } | |
| 2606 | + else if (nextYearBirthday >= now.Date && nextYearBirthday <= endDate.Date) | |
| 2607 | + { | |
| 2608 | + upcomingBirthday = nextYearBirthday; | |
| 2609 | + } | |
| 2610 | + else | |
| 2611 | + { | |
| 2612 | + continue; // 不在未来30天内 | |
| 2613 | + } | |
| 2614 | + | |
| 2615 | + // 计算年龄 | |
| 2616 | + var age = now.Year - birthday.Year; | |
| 2617 | + if (now.Month < birthday.Month || (now.Month == birthday.Month && now.Day < birthday.Day)) | |
| 2618 | + { | |
| 2619 | + age--; | |
| 2620 | + } | |
| 2621 | + | |
| 2622 | + var output = new LqKhxxBirthdayOutput | |
| 2623 | + { | |
| 2624 | + id = member.Id, | |
| 2625 | + khmc = member.Khmc, | |
| 2626 | + sjh = member.Sjh, | |
| 2627 | + dah = member.Dah, | |
| 2628 | + xb = member.Xb, | |
| 2629 | + gsmd = member.Gsmd, | |
| 2630 | + yanglsr = member.Yanglsr, | |
| 2631 | + yinlsr = member.Yinlsr, | |
| 2632 | + birthdayDate = birthday.ToString("MM-dd"), | |
| 2633 | + birthdayFullDate = upcomingBirthday, | |
| 2634 | + consumeLevel = member.ConsumeLevel, | |
| 2635 | + consumeLevelName = GetConsumeLevelName(member.ConsumeLevel), | |
| 2636 | + remainingRightsAmount = member.RemainingRightsAmount, | |
| 2637 | + bz = member.Bz, | |
| 2638 | + age = age | |
| 2639 | + }; | |
| 2640 | + | |
| 2641 | + upcomingBirthdays.Add(output); | |
| 2642 | + } | |
| 2643 | + | |
| 2644 | + // 获取门店名称 | |
| 2645 | + if (upcomingBirthdays.Any()) | |
| 2646 | + { | |
| 2647 | + var storeIds = upcomingBirthdays.Select(x => x.gsmd).Distinct().Where(x => !string.IsNullOrEmpty(x)).ToList(); | |
| 2648 | + var stores = await _db.Queryable<LqMdxxEntity>().Where(s => storeIds.Contains(s.Id)).ToListAsync(); | |
| 2649 | + | |
| 2650 | + foreach (var item in upcomingBirthdays) | |
| 2651 | + { | |
| 2652 | + if (!string.IsNullOrEmpty(item.gsmd)) | |
| 2653 | + { | |
| 2654 | + var store = stores.FirstOrDefault(s => s.Id == item.gsmd); | |
| 2655 | + item.gsmdName = store?.Dm ?? ""; | |
| 2656 | + } | |
| 2657 | + } | |
| 2658 | + } | |
| 2659 | + | |
| 2660 | + // 按生日日期排序 | |
| 2661 | + upcomingBirthdays = upcomingBirthdays.OrderBy(x => x.birthdayFullDate).ToList(); | |
| 2662 | + | |
| 2663 | + return new | |
| 2664 | + { | |
| 2665 | + code = 200, | |
| 2666 | + msg = "获取成功", | |
| 2667 | + data = upcomingBirthdays | |
| 2668 | + }; | |
| 2669 | + } | |
| 2670 | + catch (Exception ex) | |
| 2671 | + { | |
| 2672 | + _logger.LogError(ex, "获取会员生日列表失败"); | |
| 2673 | + return new | |
| 2674 | + { | |
| 2675 | + code = 500, | |
| 2676 | + msg = $"获取失败:{ex.Message}" | |
| 2677 | + }; | |
| 2678 | + } | |
| 2679 | + } | |
| 2680 | + | |
| 2681 | + /// <summary> | |
| 2682 | + /// 获取消费等级名称 | |
| 2683 | + /// </summary> | |
| 2684 | + private string GetConsumeLevelName(int level) | |
| 2685 | + { | |
| 2686 | + return level switch | |
| 2687 | + { | |
| 2688 | + 0 => "D级会员", | |
| 2689 | + 1 => "C级会员", | |
| 2690 | + 2 => "B级会员", | |
| 2691 | + 3 => "A级会员", | |
| 2692 | + 4 => "A+级会员", | |
| 2693 | + 5 => "A++级会员", | |
| 2694 | + _ => "D级会员" | |
| 2695 | + }; | |
| 2696 | + } | |
| 2697 | + | |
| 2698 | + #endregion | |
| 2699 | + | |
| 2489 | 2700 | } |
| 2490 | 2701 | } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs
| ... | ... | @@ -1304,5 +1304,185 @@ namespace NCC.Extend.LqTkjlb |
| 1304 | 1304 | } |
| 1305 | 1305 | #endregion |
| 1306 | 1306 | |
| 1307 | + #region 拓客部员工统计报表 | |
| 1308 | + /// <summary> | |
| 1309 | + /// 获取拓客部员工统计报表 | |
| 1310 | + /// </summary> | |
| 1311 | + /// <remarks> | |
| 1312 | + /// 统计指定时间周期内参与拓客的所有员工的统计数据 | |
| 1313 | + /// 包括:拓客人数、到店人数、开单人数、开单金额 | |
| 1314 | + /// | |
| 1315 | + /// 示例请求: | |
| 1316 | + /// ```json | |
| 1317 | + /// { | |
| 1318 | + /// "startTime": "2025-01-01", | |
| 1319 | + /// "endTime": "2025-01-31", | |
| 1320 | + /// "eventId": "活动ID", | |
| 1321 | + /// "storeId": ["门店ID1", "门店ID2"] | |
| 1322 | + /// } | |
| 1323 | + /// ``` | |
| 1324 | + /// | |
| 1325 | + /// 参数说明: | |
| 1326 | + /// - startTime: 开始时间(必填) | |
| 1327 | + /// - endTime: 结束时间(必填) | |
| 1328 | + /// - eventId: 活动ID(可选) | |
| 1329 | + /// - storeId: 门店ID数组(可选) | |
| 1330 | + /// | |
| 1331 | + /// 返回字段说明: | |
| 1332 | + /// - EmployeeId: 员工ID | |
| 1333 | + /// - EmployeeName: 员工姓名 | |
| 1334 | + /// - DepartmentId: 部门ID | |
| 1335 | + /// - DepartmentName: 部门名称 | |
| 1336 | + /// - Position: 岗位 | |
| 1337 | + /// - ExpansionCount: 拓客人数(去重会员数) | |
| 1338 | + /// - VisitCount: 到店人数(去重会员数,状态为已确认) | |
| 1339 | + /// - BillingCount: 开单人数(去重会员数) | |
| 1340 | + /// - BillingAmount: 开单金额 | |
| 1341 | + /// </remarks> | |
| 1342 | + /// <param name="input">查询参数</param> | |
| 1343 | + /// <returns>拓客部员工统计数据</returns> | |
| 1344 | + /// <response code="200">成功返回统计数据</response> | |
| 1345 | + /// <response code="400">参数错误</response> | |
| 1346 | + /// <response code="500">服务器内部错误</response> | |
| 1347 | + [HttpPost("get-expansion-employee-statistics")] | |
| 1348 | + public async Task<ExpansionEmployeeStatisticsOutput> GetExpansionEmployeeStatistics(ExpansionEmployeeStatisticsInput input) | |
| 1349 | + { | |
| 1350 | + try | |
| 1351 | + { | |
| 1352 | + if (input == null) | |
| 1353 | + { | |
| 1354 | + throw NCCException.Oh("查询参数不能为空"); | |
| 1355 | + } | |
| 1356 | + | |
| 1357 | + if (!input.StartTime.HasValue || !input.EndTime.HasValue) | |
| 1358 | + { | |
| 1359 | + throw NCCException.Oh("开始时间和结束时间不能为空"); | |
| 1360 | + } | |
| 1361 | + | |
| 1362 | + // 构建时间过滤条件 | |
| 1363 | + var timeFilter = $"AND tk.F_ExpansionTime >= '{input.StartTime.Value:yyyy-MM-dd 00:00:00}' AND tk.F_ExpansionTime <= '{input.EndTime.Value:yyyy-MM-dd 23:59:59}'"; | |
| 1364 | + | |
| 1365 | + // 构建活动过滤条件 | |
| 1366 | + string eventFilter = ""; | |
| 1367 | + if (!string.IsNullOrWhiteSpace(input.EventId)) | |
| 1368 | + { | |
| 1369 | + eventFilter = $"AND tk.F_EventId = '{input.EventId}'"; | |
| 1370 | + } | |
| 1371 | + | |
| 1372 | + // 构建门店过滤条件 | |
| 1373 | + string storeFilter = ""; | |
| 1374 | + if (input.StoreId != null && input.StoreId.Any() && input.StoreId.Any(s => !string.IsNullOrWhiteSpace(s))) | |
| 1375 | + { | |
| 1376 | + var storeIdsStr = string.Join("','", input.StoreId.Where(s => !string.IsNullOrWhiteSpace(s))); | |
| 1377 | + storeFilter = $"AND tk.F_StoreId IN ('{storeIdsStr}')"; | |
| 1378 | + } | |
| 1379 | + | |
| 1380 | + // 构建完整的SQL查询,按员工分组统计各项指标 | |
| 1381 | + // 使用子查询避免JOIN导致的数据重复问题 | |
| 1382 | + var sql = $@" | |
| 1383 | + SELECT | |
| 1384 | + emp.EmployeeId, | |
| 1385 | + emp.EmployeeName, | |
| 1386 | + emp.DepartmentId, | |
| 1387 | + emp.DepartmentName, | |
| 1388 | + emp.Position, | |
| 1389 | + emp.ExpansionCount, | |
| 1390 | + COALESCE(visit.VisitCount, 0) AS VisitCount, | |
| 1391 | + COALESCE(billing.BillingCount, 0) AS BillingCount, | |
| 1392 | + COALESCE(billing.BillingAmount, 0) AS BillingAmount | |
| 1393 | + FROM ( | |
| 1394 | + -- 基础数据:按员工统计拓客人数 | |
| 1395 | + SELECT | |
| 1396 | + u.F_Id AS EmployeeId, | |
| 1397 | + u.F_REALNAME AS EmployeeName, | |
| 1398 | + COALESCE(u.F_OrganizeId, '') AS DepartmentId, | |
| 1399 | + COALESCE(org.F_FullName, '') AS DepartmentName, | |
| 1400 | + COALESCE(u.F_GW, '') AS Position, | |
| 1401 | + COUNT(DISTINCT tk.F_MemberId) AS ExpansionCount | |
| 1402 | + FROM lq_tkjlb tk | |
| 1403 | + INNER JOIN BASE_USER u ON tk.F_ExpansionUserId = u.F_Id | |
| 1404 | + LEFT JOIN BASE_ORGANIZE org ON u.F_OrganizeId = org.F_Id | |
| 1405 | + WHERE 1=1 | |
| 1406 | + {timeFilter} | |
| 1407 | + {eventFilter} | |
| 1408 | + {storeFilter} | |
| 1409 | + AND u.F_EnabledMark = 1 | |
| 1410 | + AND u.F_DeleteMark IS NULL | |
| 1411 | + AND (org.F_DeleteMark IS NULL OR org.F_DeleteMark = 0) | |
| 1412 | + GROUP BY | |
| 1413 | + u.F_Id, | |
| 1414 | + u.F_REALNAME, | |
| 1415 | + u.F_OrganizeId, | |
| 1416 | + org.F_FullName, | |
| 1417 | + u.F_GW | |
| 1418 | + ) emp | |
| 1419 | + LEFT JOIN ( | |
| 1420 | + -- 到店人数统计(预约状态为已确认) | |
| 1421 | + SELECT | |
| 1422 | + tk.F_ExpansionUserId AS EmployeeId, | |
| 1423 | + COUNT(DISTINCT tk.F_MemberId) AS VisitCount | |
| 1424 | + FROM lq_tkjlb tk | |
| 1425 | + INNER JOIN BASE_USER u ON tk.F_ExpansionUserId = u.F_Id | |
| 1426 | + INNER JOIN lq_yyjl yyjl ON tk.F_MemberId = yyjl.gk | |
| 1427 | + AND yyjl.F_Status = '已确认' | |
| 1428 | + WHERE 1=1 | |
| 1429 | + {timeFilter} | |
| 1430 | + {eventFilter} | |
| 1431 | + {storeFilter} | |
| 1432 | + AND u.F_EnabledMark = 1 | |
| 1433 | + AND u.F_DeleteMark IS NULL | |
| 1434 | + GROUP BY tk.F_ExpansionUserId | |
| 1435 | + ) visit ON emp.EmployeeId = visit.EmployeeId | |
| 1436 | + LEFT JOIN ( | |
| 1437 | + -- 开单人数和金额统计(开单时间在拓客时间之后) | |
| 1438 | + SELECT | |
| 1439 | + tk.F_ExpansionUserId AS EmployeeId, | |
| 1440 | + COUNT(DISTINCT tk.F_MemberId) AS BillingCount, | |
| 1441 | + SUM(kd.sfyj) AS BillingAmount | |
| 1442 | + FROM lq_tkjlb tk | |
| 1443 | + INNER JOIN BASE_USER u ON tk.F_ExpansionUserId = u.F_Id | |
| 1444 | + INNER JOIN lq_kd_kdjlb kd ON tk.F_MemberId = kd.kdhy | |
| 1445 | + AND kd.F_IsEffective = 1 | |
| 1446 | + AND kd.sfyj > 0 | |
| 1447 | + AND DATE_FORMAT(kd.kdrq, '%Y-%m-%d') >= DATE_FORMAT(tk.F_ExpansionTime, '%Y-%m-%d') | |
| 1448 | + WHERE 1=1 | |
| 1449 | + {timeFilter} | |
| 1450 | + {eventFilter} | |
| 1451 | + {storeFilter} | |
| 1452 | + AND u.F_EnabledMark = 1 | |
| 1453 | + AND u.F_DeleteMark IS NULL | |
| 1454 | + GROUP BY tk.F_ExpansionUserId | |
| 1455 | + ) billing ON emp.EmployeeId = billing.EmployeeId | |
| 1456 | + ORDER BY emp.ExpansionCount DESC, billing.BillingAmount DESC"; | |
| 1457 | + | |
| 1458 | + var result = await _db.Ado.SqlQueryAsync<dynamic>(sql); | |
| 1459 | + | |
| 1460 | + var employeeList = result.Select(item => new ExpansionEmployeeStatisticsItem | |
| 1461 | + { | |
| 1462 | + EmployeeId = item.EmployeeId?.ToString() ?? "", | |
| 1463 | + EmployeeName = item.EmployeeName?.ToString() ?? "", | |
| 1464 | + DepartmentId = item.DepartmentId?.ToString() ?? "", | |
| 1465 | + DepartmentName = item.DepartmentName?.ToString() ?? "", | |
| 1466 | + Position = item.Position?.ToString() ?? "", | |
| 1467 | + ExpansionCount = Convert.ToInt32(item.ExpansionCount ?? 0), | |
| 1468 | + VisitCount = Convert.ToInt32(item.VisitCount ?? 0), | |
| 1469 | + BillingCount = Convert.ToInt32(item.BillingCount ?? 0), | |
| 1470 | + BillingAmount = Convert.ToDecimal(item.BillingAmount ?? 0) | |
| 1471 | + }).ToList(); | |
| 1472 | + | |
| 1473 | + return new ExpansionEmployeeStatisticsOutput | |
| 1474 | + { | |
| 1475 | + StartTime = input.StartTime, | |
| 1476 | + EndTime = input.EndTime, | |
| 1477 | + Employees = employeeList | |
| 1478 | + }; | |
| 1479 | + } | |
| 1480 | + catch (Exception ex) | |
| 1481 | + { | |
| 1482 | + throw NCCException.Oh($"获取拓客部员工统计数据失败: {ex.Message}"); | |
| 1483 | + } | |
| 1484 | + } | |
| 1485 | + #endregion | |
| 1486 | + | |
| 1307 | 1487 | } |
| 1308 | 1488 | } | ... | ... |
sql/会员生日日历菜单配置.sql
0 → 100644
| 1 | +-- ============================================= | |
| 2 | +-- 会员生日日历功能菜单配置SQL | |
| 3 | +-- 功能说明:为系统添加"会员生日日历"菜单,支持查看未来30天过生日的会员 | |
| 4 | +-- 创建时间:2026-01-08 | |
| 5 | +-- ============================================= | |
| 6 | + | |
| 7 | +-- 说明: | |
| 8 | +-- 1. 请根据实际情况修改 F_ParentId(父菜单ID) | |
| 9 | +-- 2. 请根据实际情况修改 F_SortCode(排序码) | |
| 10 | +-- 3. 菜单ID使用固定值,如需修改请同步修改按钮配置中的 F_ParentId | |
| 11 | + | |
| 12 | +-- ============================================= | |
| 13 | +-- 1. 添加菜单 | |
| 14 | +-- ============================================= | |
| 15 | +-- 假设父菜单ID为客户管理模块的ID,请根据实际情况修改 | |
| 16 | +-- 查询客户管理模块ID的SQL:SELECT F_Id, F_FullName FROM base_module WHERE F_FullName LIKE '%客户%' OR F_EnCode LIKE '%khxx%'; | |
| 17 | + | |
| 18 | +INSERT INTO `base_module` ( | |
| 19 | + `F_Id`, | |
| 20 | + `F_ParentId`, | |
| 21 | + `F_Layers`, | |
| 22 | + `F_EnCode`, | |
| 23 | + `F_FullName`, | |
| 24 | + `F_Icon`, | |
| 25 | + `F_UrlAddress`, | |
| 26 | + `F_Target`, | |
| 27 | + `F_IsMenu`, | |
| 28 | + `F_AllowExpand`, | |
| 29 | + `F_IsPublic`, | |
| 30 | + `F_AllowEdit`, | |
| 31 | + `F_AllowDelete`, | |
| 32 | + `F_SortCode`, | |
| 33 | + `F_DeleteMark`, | |
| 34 | + `F_EnabledMark`, | |
| 35 | + `F_CreateTime`, | |
| 36 | + `F_CreateUserId`, | |
| 37 | + `F_Type`, | |
| 38 | + `F_Category`, | |
| 39 | + `F_Description` | |
| 40 | +) VALUES ( | |
| 41 | + '1876543210987654321', -- 菜单ID(固定值) | |
| 42 | + 'YOUR_PARENT_MODULE_ID', -- 父菜单ID,请修改为实际的客户管理模块ID | |
| 43 | + 2, -- 层级,根据实际父菜单层级+1 | |
| 44 | + 'lqKhxxBirthday', -- 编码 | |
| 45 | + '会员生日日历', -- 菜单名称 | |
| 46 | + 'el-icon-date', -- 图标 | |
| 47 | + '/lqKhxxBirthday', -- 路由地址 | |
| 48 | + 'iframe', -- 目标 | |
| 49 | + 1, -- 是否菜单(1=是) | |
| 50 | + 1, -- 允许展开 | |
| 51 | + 0, -- 是否公开 | |
| 52 | + 0, -- 允许编辑 | |
| 53 | + 0, -- 允许删除 | |
| 54 | + 999, -- 排序码,请根据实际情况修改 | |
| 55 | + NULL, -- 删除标记 | |
| 56 | + 1, -- 启用标记(1=启用) | |
| 57 | + NOW(), -- 创建时间 | |
| 58 | + 'admin', -- 创建用户ID | |
| 59 | + 1, -- 类型 | |
| 60 | + 1, -- 分类 | |
| 61 | + '查看未来30天过生日的会员,支持按门店筛选,按会员等级显示不同颜色(0=D,1=C,2=B,3=A,4=A+,5=A++)' -- 描述 | |
| 62 | +); | |
| 63 | + | |
| 64 | +-- ============================================= | |
| 65 | +-- 2. 添加功能按钮(可选) | |
| 66 | +-- ============================================= | |
| 67 | +-- 查看按钮 | |
| 68 | +INSERT INTO `base_module` ( | |
| 69 | + `F_Id`, | |
| 70 | + `F_ParentId`, | |
| 71 | + `F_Layers`, | |
| 72 | + `F_EnCode`, | |
| 73 | + `F_FullName`, | |
| 74 | + `F_Icon`, | |
| 75 | + `F_UrlAddress`, | |
| 76 | + `F_Target`, | |
| 77 | + `F_IsMenu`, | |
| 78 | + `F_AllowExpand`, | |
| 79 | + `F_IsPublic`, | |
| 80 | + `F_AllowEdit`, | |
| 81 | + `F_AllowDelete`, | |
| 82 | + `F_SortCode`, | |
| 83 | + `F_DeleteMark`, | |
| 84 | + `F_EnabledMark`, | |
| 85 | + `F_CreateTime`, | |
| 86 | + `F_CreateUserId`, | |
| 87 | + `F_Type`, | |
| 88 | + `F_Category`, | |
| 89 | + `F_Description` | |
| 90 | +) VALUES ( | |
| 91 | + '1876543210987654322', -- 按钮ID | |
| 92 | + '1876543210987654321', -- 父菜单ID(会员生日日历) | |
| 93 | + 3, -- 层级 | |
| 94 | + 'btn_detail', -- 编码 | |
| 95 | + '查看详情', -- 按钮名称 | |
| 96 | + 'el-icon-view', -- 图标 | |
| 97 | + NULL, -- 路由地址 | |
| 98 | + NULL, -- 目标 | |
| 99 | + 0, -- 是否菜单(0=否,按钮) | |
| 100 | + NULL, -- 允许展开 | |
| 101 | + 0, -- 是否公开 | |
| 102 | + 0, -- 允许编辑 | |
| 103 | + 0, -- 允许删除 | |
| 104 | + 1, -- 排序码 | |
| 105 | + NULL, -- 删除标记 | |
| 106 | + 1, -- 启用标记(1=启用) | |
| 107 | + NOW(), -- 创建时间 | |
| 108 | + 'admin', -- 创建用户ID | |
| 109 | + 2, -- 类型(2=按钮) | |
| 110 | + 1, -- 分类 | |
| 111 | + '查看会员生日详细信息' -- 描述 | |
| 112 | +); | |
| 113 | + | |
| 114 | +-- 刷新按钮 | |
| 115 | +INSERT INTO `base_module` ( | |
| 116 | + `F_Id`, | |
| 117 | + `F_ParentId`, | |
| 118 | + `F_Layers`, | |
| 119 | + `F_EnCode`, | |
| 120 | + `F_FullName`, | |
| 121 | + `F_Icon`, | |
| 122 | + `F_UrlAddress`, | |
| 123 | + `F_Target`, | |
| 124 | + `F_IsMenu`, | |
| 125 | + `F_AllowExpand`, | |
| 126 | + `F_IsPublic`, | |
| 127 | + `F_AllowEdit`, | |
| 128 | + `F_AllowDelete`, | |
| 129 | + `F_SortCode`, | |
| 130 | + `F_DeleteMark`, | |
| 131 | + `F_EnabledMark`, | |
| 132 | + `F_CreateTime`, | |
| 133 | + `F_CreateUserId`, | |
| 134 | + `F_Type`, | |
| 135 | + `F_Category`, | |
| 136 | + `F_Description` | |
| 137 | +) VALUES ( | |
| 138 | + '1876543210987654323', -- 按钮ID | |
| 139 | + '1876543210987654321', -- 父菜单ID(会员生日日历) | |
| 140 | + 3, -- 层级 | |
| 141 | + 'btn_refresh', -- 编码 | |
| 142 | + '刷新', -- 按钮名称 | |
| 143 | + 'el-icon-refresh', -- 图标 | |
| 144 | + NULL, -- 路由地址 | |
| 145 | + NULL, -- 目标 | |
| 146 | + 0, -- 是否菜单(0=否,按钮) | |
| 147 | + NULL, -- 允许展开 | |
| 148 | + 0, -- 是否公开 | |
| 149 | + 0, -- 允许编辑 | |
| 150 | + 0, -- 允许删除 | |
| 151 | + 2, -- 排序码 | |
| 152 | + NULL, -- 删除标记 | |
| 153 | + 1, -- 启用标记(1=启用) | |
| 154 | + NOW(), -- 创建时间 | |
| 155 | + 'admin', -- 创建用户ID | |
| 156 | + 2, -- 类型(2=按钮) | |
| 157 | + 1, -- 分类 | |
| 158 | + '刷新会员生日数据' -- 描述 | |
| 159 | +); | |
| 160 | + | |
| 161 | +-- ============================================= | |
| 162 | +-- 3. 授权给管理员角色(可选) | |
| 163 | +-- ============================================= | |
| 164 | +-- 查询管理员角色ID:SELECT F_Id, F_FullName FROM base_role WHERE F_FullName LIKE '%管理员%'; | |
| 165 | + | |
| 166 | +-- 为管理员角色授权菜单 | |
| 167 | +INSERT INTO `base_authorize` ( | |
| 168 | + `F_Id`, | |
| 169 | + `F_ItemType`, | |
| 170 | + `F_ItemId`, | |
| 171 | + `F_ObjectType`, | |
| 172 | + `F_ObjectId`, | |
| 173 | + `F_SortCode`, | |
| 174 | + `F_CreateTime`, | |
| 175 | + `F_CreateUserId` | |
| 176 | +) VALUES ( | |
| 177 | + REPLACE(UUID(), '-', ''), -- 授权ID | |
| 178 | + 1, -- 项目类型(1=菜单) | |
| 179 | + '1876543210987654321', -- 菜单ID | |
| 180 | + 1, -- 对象类型(1=角色) | |
| 181 | + 'YOUR_ADMIN_ROLE_ID', -- 角色ID,请修改为实际的管理员角色ID | |
| 182 | + 0, -- 排序码 | |
| 183 | + NOW(), -- 创建时间 | |
| 184 | + 'admin' -- 创建用户ID | |
| 185 | +); | |
| 186 | + | |
| 187 | +-- 为管理员角色授权查看详情按钮 | |
| 188 | +INSERT INTO `base_authorize` ( | |
| 189 | + `F_Id`, | |
| 190 | + `F_ItemType`, | |
| 191 | + `F_ItemId`, | |
| 192 | + `F_ObjectType`, | |
| 193 | + `F_ObjectId`, | |
| 194 | + `F_SortCode`, | |
| 195 | + `F_CreateTime`, | |
| 196 | + `F_CreateUserId` | |
| 197 | +) VALUES ( | |
| 198 | + REPLACE(UUID(), '-', ''), -- 授权ID | |
| 199 | + 2, -- 项目类型(2=按钮) | |
| 200 | + '1876543210987654322', -- 按钮ID | |
| 201 | + 1, -- 对象类型(1=角色) | |
| 202 | + 'YOUR_ADMIN_ROLE_ID', -- 角色ID,请修改为实际的管理员角色ID | |
| 203 | + 0, -- 排序码 | |
| 204 | + NOW(), -- 创建时间 | |
| 205 | + 'admin' -- 创建用户ID | |
| 206 | +); | |
| 207 | + | |
| 208 | +-- 为管理员角色授权刷新按钮 | |
| 209 | +INSERT INTO `base_authorize` ( | |
| 210 | + `F_Id`, | |
| 211 | + `F_ItemType`, | |
| 212 | + `F_ItemId`, | |
| 213 | + `F_ObjectType`, | |
| 214 | + `F_ObjectId`, | |
| 215 | + `F_SortCode`, | |
| 216 | + `F_CreateTime`, | |
| 217 | + `F_CreateUserId` | |
| 218 | +) VALUES ( | |
| 219 | + REPLACE(UUID(), '-', ''), -- 授权ID | |
| 220 | + 2, -- 项目类型(2=按钮) | |
| 221 | + '1876543210987654323', -- 按钮ID | |
| 222 | + 1, -- 对象类型(1=角色) | |
| 223 | + 'YOUR_ADMIN_ROLE_ID', -- 角色ID,请修改为实际的管理员角色ID | |
| 224 | + 0, -- 排序码 | |
| 225 | + NOW(), -- 创建时间 | |
| 226 | + 'admin' -- 创建用户ID | |
| 227 | +); | |
| 228 | + | |
| 229 | +-- ============================================= | |
| 230 | +-- 使用说明 | |
| 231 | +-- ============================================= | |
| 232 | +-- 1. 执行前请先备份数据库 | |
| 233 | +-- 2. 根据实际情况修改以下内容: | |
| 234 | +-- - YOUR_PARENT_MODULE_ID:父菜单ID(客户管理模块) | |
| 235 | +-- - YOUR_ADMIN_ROLE_ID:管理员角色ID | |
| 236 | +-- - F_SortCode:排序码 | |
| 237 | +-- - F_Layers:层级 | |
| 238 | +-- 3. 执行完成后,清除缓存或重新登录系统以查看新菜单 | |
| 239 | +-- 4. 如需删除菜单,执行: | |
| 240 | +-- DELETE FROM base_module WHERE F_Id IN ('1876543210987654321', '1876543210987654322', '1876543210987654323'); | |
| 241 | +-- DELETE FROM base_authorize WHERE F_ItemId IN ('1876543210987654321', '1876543210987654322', '1876543210987654323'); | ... | ... |