Commit 3fbec03db27b59175a7eddab8045a570cf6bd4cd

Authored by “wangming”
1 parent 6713600a

新增财务报表功能、会员生日提醒功能、拓客员工统计功能;优化会员画像对话框和会员列表查询

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&quot;;
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');
... ...