Commit ac14c8dbe991d749b70a5a05dff87bb1dd975c90

Authored by “wangming”
1 parent dc67d3d0

chore: update dashboard modal and docs

antis-ncc-admin/src/views/report/index.vue
... ... @@ -57,6 +57,119 @@
57 57 </el-row>
58 58 </div>
59 59  
  60 + <!-- 会员统计概览 -->
  61 + <div class="member-statistics-overview" style="margin-top: 20px;">
  62 + <el-row :gutter="20">
  63 + <!-- 总会员数 -->
  64 + <el-col :span="6">
  65 + <el-card class="member-card card-member-total">
  66 + <div class="card-content">
  67 + <div class="card-icon">
  68 + <i class="el-icon-user-solid"></i>
  69 + </div>
  70 + <div class="card-info">
  71 + <div class="card-title">总会员数</div>
  72 + <div class="card-value">{{ formatNumber(memberCards.totalMembers) }}</div>
  73 + <div class="card-unit">人</div>
  74 + <div class="card-subtitle">本月新增:{{ formatNumber(memberCards.newMembers) }}人</div>
  75 + </div>
  76 + </div>
  77 + </el-card>
  78 + </el-col>
  79 +
  80 + <!-- 活跃会员数 -->
  81 + <el-col :span="6">
  82 + <el-card class="member-card card-member-active">
  83 + <div class="card-content">
  84 + <div class="card-icon">
  85 + <i class="el-icon-success"></i>
  86 + </div>
  87 + <div class="card-info">
  88 + <div class="card-title">活跃会员数</div>
  89 + <div class="card-value">{{ formatNumber(memberCards.activeMembers) }}</div>
  90 + <div class="card-unit">人</div>
  91 + <div class="card-subtitle">活跃率:{{ memberCards.activeRate }}%</div>
  92 + </div>
  93 + </div>
  94 + </el-card>
  95 + </el-col>
  96 +
  97 + <!-- 剩余权益总金额 -->
  98 + <el-col :span="6">
  99 + <el-card class="member-card card-member-amount">
  100 + <div class="card-content">
  101 + <div class="card-icon">
  102 + <i class="el-icon-wallet"></i>
  103 + </div>
  104 + <div class="card-info">
  105 + <div class="card-title">剩余权益总金额</div>
  106 + <div class="card-value">{{ formatNumber(memberCards.totalRemainingAmount / 10000) }}
  107 + </div>
  108 + <div class="card-unit">万元</div>
  109 + <div class="card-subtitle">人均:{{ formatNumber(memberCards.avgRemainingAmount) }}元</div>
  110 + </div>
  111 + </div>
  112 + </el-card>
  113 + </el-col>
  114 +
  115 + <!-- 沉睡会员数 -->
  116 + <el-col :span="6">
  117 + <el-card class="member-card card-member-sleep">
  118 + <div class="card-content">
  119 + <div class="card-icon">
  120 + <i class="el-icon-warning"></i>
  121 + </div>
  122 + <div class="card-info">
  123 + <div class="card-title">沉睡会员数</div>
  124 + <div class="card-value">{{ formatNumber(memberCards.totalSleepMembers) }}</div>
  125 + <div class="card-unit">人</div>
  126 + <div class="card-subtitle">30-90天:{{ formatNumber(memberCards.sleep30_90) }} | 90天+:{{
  127 + formatNumber(memberCards.sleepOver90) }}</div>
  128 + </div>
  129 + </div>
  130 + </el-card>
  131 + </el-col>
  132 + </el-row>
  133 +
  134 + <el-row :gutter="20" style="margin-top: 20px;">
  135 + <!-- 会员类型分布 -->
  136 + <el-col :span="12">
  137 + <el-card class="chart-card">
  138 + <div slot="header" class="chart-header">
  139 + <span class="chart-title">
  140 + <i class="el-icon-pie-chart"></i>
  141 + 会员类型分布
  142 + </span>
  143 + </div>
  144 + <div class="chart-container" ref="memberTypeChart"></div>
  145 + </el-card>
  146 + </el-col>
  147 +
  148 + <!-- 会员分类统计 -->
  149 + <el-col :span="12">
  150 + <el-card class="chart-card">
  151 + <div slot="header" class="chart-header">
  152 + <span class="chart-title">
  153 + <i class="el-icon-s-grid"></i>
  154 + 会员分类统计
  155 + </span>
  156 + </div>
  157 + <div class="member-category-list">
  158 + <div class="category-item" v-for="(item, index) in memberCategoryList" :key="index">
  159 + <div class="category-icon" :class="`category-${index + 1}`">
  160 + <i :class="item.icon"></i>
  161 + </div>
  162 + <div class="category-info">
  163 + <div class="category-name">{{ item.name }}</div>
  164 + <div class="category-value">{{ formatNumber(item.value) }}人</div>
  165 + </div>
  166 + </div>
  167 + </div>
  168 + </el-card>
  169 + </el-col>
  170 + </el-row>
  171 + </div>
  172 +
60 173 <!-- 图表区域 -->
61 174 <div class="charts-section">
62 175 <el-row :gutter="20">
... ... @@ -263,6 +376,30 @@ export default {
263 376 }
264 377 ],
265 378  
  379 + // 会员统计卡片数据
  380 + memberCards: {
  381 + totalMembers: 0,
  382 + newMembers: 0,
  383 + activeMembers: 0,
  384 + activeRate: 0,
  385 + totalRemainingAmount: 0,
  386 + avgRemainingAmount: 0,
  387 + totalSleepMembers: 0,
  388 + sleep30_90: 0,
  389 + sleepOver90: 0
  390 + },
  391 +
  392 + // 会员分类列表
  393 + memberCategoryList: [
  394 + { name: '生美会员', value: 0, icon: 'el-icon-star-on' },
  395 + { name: '医美会员', value: 0, icon: 'el-icon-medicine-box' },
  396 + { name: '科技部会员', value: 0, icon: 'el-icon-cpu' },
  397 + { name: '教育部会员', value: 0, icon: 'el-icon-reading' }
  398 + ],
  399 +
  400 + // 会员类型分布数据
  401 + memberTypeDistribution: [],
  402 +
266 403 // 图表实例
267 404 charts: {}
268 405 }
... ... @@ -307,6 +444,34 @@ export default {
307 444 this.overviewCards[1].value = Math.round(data.StorePerformance.TotalPerformance / 10000 * 100) / 100
308 445 this.overviewCards[2].value = data.HealthCoachPerformance.HealthCoachCount
309 446 this.overviewCards[3].value = data.GoldTrianglePerformance.GoldTriangleCount
  447 +
  448 + // 更新会员统计数据
  449 + if (data.MemberStatistics) {
  450 + const memberStats = data.MemberStatistics
  451 + this.memberCards.totalMembers = memberStats.TotalMembers || 0
  452 + this.memberCards.newMembers = memberStats.NewMembersThisMonth || 0
  453 + this.memberCards.activeMembers = memberStats.ActiveMembers || 0
  454 + this.memberCards.activeRate = memberStats.ActiveRate || 0
  455 + this.memberCards.totalRemainingAmount = memberStats.TotalRemainingAmount || 0
  456 + this.memberCards.avgRemainingAmount = memberStats.AvgRemainingAmount || 0
  457 + this.memberCards.totalSleepMembers = memberStats.TotalSleepMembers || 0
  458 + this.memberCards.sleep30_90 = memberStats.SleepMembers30_90 || 0
  459 + this.memberCards.sleepOver90 = memberStats.SleepMembersOver90 || 0
  460 +
  461 + // 更新会员分类列表
  462 + this.memberCategoryList[0].value = memberStats.BeautyMembers || 0
  463 + this.memberCategoryList[1].value = memberStats.MedicalMembers || 0
  464 + this.memberCategoryList[2].value = memberStats.TechMembers || 0
  465 + this.memberCategoryList[3].value = memberStats.EducationMembers || 0
  466 +
  467 + // 更新会员类型分布
  468 + this.memberTypeDistribution = memberStats.MemberTypeDistribution || []
  469 +
  470 + // 加载会员类型分布饼图
  471 + this.$nextTick(() => {
  472 + this.loadMemberTypeChart()
  473 + })
  474 + }
310 475 }
311 476 } catch (error) {
312 477 this.$message.error('加载仪表盘数据失败')
... ... @@ -327,6 +492,68 @@ export default {
327 492 ])
328 493 },
329 494  
  495 + // 加载会员类型分布饼图
  496 + loadMemberTypeChart() {
  497 + if (!this.$refs.memberTypeChart) return
  498 +
  499 + // 如果图表已存在,先销毁
  500 + if (this.charts.memberTypeChart) {
  501 + this.charts.memberTypeChart.dispose()
  502 + }
  503 +
  504 + const chartDom = this.$refs.memberTypeChart
  505 + this.charts.memberTypeChart = echarts.init(chartDom)
  506 +
  507 + const option = {
  508 + tooltip: {
  509 + trigger: 'item',
  510 + formatter: '{a} <br/>{b}: {c} ({d}%)'
  511 + },
  512 + legend: {
  513 + orient: 'vertical',
  514 + left: 'left',
  515 + top: 'middle'
  516 + },
  517 + series: [
  518 + {
  519 + name: '会员类型',
  520 + type: 'pie',
  521 + radius: ['40%', '70%'],
  522 + avoidLabelOverlap: false,
  523 + itemStyle: {
  524 + borderRadius: 10,
  525 + borderColor: '#fff',
  526 + borderWidth: 2
  527 + },
  528 + label: {
  529 + show: true,
  530 + formatter: '{b}\n{c}人 ({d}%)'
  531 + },
  532 + emphasis: {
  533 + label: {
  534 + show: true,
  535 + fontSize: 16,
  536 + fontWeight: 'bold'
  537 + }
  538 + },
  539 + data: this.memberTypeDistribution.map(item => ({
  540 + value: item.Count,
  541 + name: item.MemberType
  542 + }))
  543 + }
  544 + ]
  545 + }
  546 +
  547 + this.charts.memberTypeChart.setOption(option)
  548 +
  549 + // 响应式调整
  550 + window.addEventListener('resize', () => {
  551 + if (this.charts.memberTypeChart) {
  552 + this.charts.memberTypeChart.resize()
  553 + }
  554 + })
  555 + },
  556 +
330 557 // 加载门店业绩趋势图
331 558 async loadStoreTrendChart() {
332 559 try {
... ... @@ -973,6 +1200,152 @@ export default {
973 1200 }
974 1201 }
975 1202  
  1203 + .member-statistics-overview {
  1204 + margin-bottom: 30px;
  1205 +
  1206 + .member-card {
  1207 + background: rgba(255, 255, 255, 0.95);
  1208 + backdrop-filter: blur(10px);
  1209 + border-radius: 15px;
  1210 + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  1211 + transition: all 0.3s ease;
  1212 +
  1213 + &:hover {
  1214 + transform: translateY(-5px);
  1215 + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
  1216 + }
  1217 +
  1218 + &.card-member-total {
  1219 + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  1220 + color: white;
  1221 + }
  1222 +
  1223 + &.card-member-active {
  1224 + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  1225 + color: white;
  1226 + }
  1227 +
  1228 + &.card-member-amount {
  1229 + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
  1230 + color: white;
  1231 + }
  1232 +
  1233 + &.card-member-sleep {
  1234 + background: linear-gradient(135deg, #ff9a56 0%, #ff6a88 100%);
  1235 + color: white;
  1236 + }
  1237 +
  1238 + .card-content {
  1239 + display: flex;
  1240 + align-items: center;
  1241 + padding: 20px;
  1242 +
  1243 + .card-icon {
  1244 + font-size: 48px;
  1245 + margin-right: 20px;
  1246 + opacity: 0.8;
  1247 + }
  1248 +
  1249 + .card-info {
  1250 + flex: 1;
  1251 +
  1252 + .card-title {
  1253 + font-size: 14px;
  1254 + font-weight: 600;
  1255 + margin-bottom: 8px;
  1256 + opacity: 0.9;
  1257 + }
  1258 +
  1259 + .card-value {
  1260 + font-size: 32px;
  1261 + font-weight: bold;
  1262 + margin-bottom: 4px;
  1263 + }
  1264 +
  1265 + .card-unit {
  1266 + font-size: 12px;
  1267 + opacity: 0.8;
  1268 + margin-bottom: 4px;
  1269 + }
  1270 +
  1271 + .card-subtitle {
  1272 + font-size: 12px;
  1273 + opacity: 0.8;
  1274 + margin-top: 4px;
  1275 + }
  1276 + }
  1277 + }
  1278 + }
  1279 +
  1280 + .member-category-list {
  1281 + padding: 20px;
  1282 +
  1283 + .category-item {
  1284 + display: flex;
  1285 + align-items: center;
  1286 + padding: 15px;
  1287 + margin-bottom: 12px;
  1288 + background: rgba(245, 247, 250, 0.8);
  1289 + border-radius: 10px;
  1290 + transition: all 0.3s ease;
  1291 +
  1292 + &:hover {
  1293 + background: rgba(245, 247, 250, 1);
  1294 + transform: translateX(5px);
  1295 + }
  1296 +
  1297 + &:last-child {
  1298 + margin-bottom: 0;
  1299 + }
  1300 +
  1301 + .category-icon {
  1302 + width: 50px;
  1303 + height: 50px;
  1304 + border-radius: 10px;
  1305 + display: flex;
  1306 + align-items: center;
  1307 + justify-content: center;
  1308 + margin-right: 15px;
  1309 + font-size: 24px;
  1310 + color: white;
  1311 +
  1312 + &.category-1 {
  1313 + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  1314 + }
  1315 +
  1316 + &.category-2 {
  1317 + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
  1318 + }
  1319 +
  1320 + &.category-3 {
  1321 + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  1322 + }
  1323 +
  1324 + &.category-4 {
  1325 + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
  1326 + }
  1327 + }
  1328 +
  1329 + .category-info {
  1330 + flex: 1;
  1331 +
  1332 + .category-name {
  1333 + font-size: 16px;
  1334 + font-weight: 600;
  1335 + color: #333;
  1336 + margin-bottom: 4px;
  1337 + }
  1338 +
  1339 + .category-value {
  1340 + font-size: 20px;
  1341 + font-weight: bold;
  1342 + color: #409EFF;
  1343 + }
  1344 + }
  1345 + }
  1346 + }
  1347 + }
  1348 +
976 1349 .dashboard-overview {
977 1350 margin-bottom: 30px;
978 1351  
... ...
antis-ncc-admin/src/views/statisticsList/form9.vue
... ... @@ -3,7 +3,7 @@
3 3 <!-- 顶部状态栏 & 筛选器 -->
4 4 <div class="cockpit-header">
5 5 <div class="header-left">
6   - <h1 class="page-title">领导驾驶舱 <span class="subtitle">本月经营动态监测</span></h1>
  6 + <h1 class="page-title">集团驾驶舱 <span class="subtitle">本月经营动态监测</span></h1>
7 7 </div>
8 8 <div class="header-right">
9 9 <el-form :inline="true" class="search-form-compact">
... ... @@ -22,6 +22,11 @@
22 22 <el-form-item>
23 23 <el-button type="primary" size="mini" icon="el-icon-refresh" @click="search()">更新看板</el-button>
24 24 </el-form-item>
  25 + <el-form-item>
  26 + <el-button type="success" size="mini" icon="el-icon-magic-stick" @click="openTechModal">
  27 + 测试弹窗
  28 + </el-button>
  29 + </el-form-item>
25 30 </el-form>
26 31 </div>
27 32 </div>
... ... @@ -46,6 +51,232 @@
46 51 </el-col>
47 52 </el-row>
48 53  
  54 + <!-- 会员统计区域 - 统一一行展示 -->
  55 + <el-row :gutter="16" class="member-statistics-section">
  56 + <!-- 会员核心指标 -->
  57 + <el-col :span="8">
  58 + <el-card class="dashboard-card member-overview-card" shadow="hover">
  59 + <div slot="header" class="card-title">
  60 + <span><i class="el-icon-user-solid"></i> 会员核心指标</span>
  61 + </div>
  62 + <div class="member-overview-grid">
  63 + <div class="member-stat-item stat-item-1">
  64 + <div class="stat-main">
  65 + <div class="stat-icon">
  66 + <i class="el-icon-user-solid"></i>
  67 + </div>
  68 + <div class="stat-content">
  69 + <div class="stat-label">总会员数</div>
  70 + <div class="stat-value">{{ formatNumber(memberStatistics.totalMembers) }}</div>
  71 + </div>
  72 + </div>
  73 + <div class="stat-tags">
  74 + <span class="stat-tag">
  75 + <span class="stat-tag-inner">
  76 + <i class="el-icon-arrow-up"></i>
  77 + <span class="stat-tag-text">
  78 + 本月新增: {{ formatNumber(memberStatistics.newMembers) }}人
  79 + </span>
  80 + </span>
  81 + </span>
  82 + <span class="stat-tag" v-if="memberStatistics.newMembersLastMonth > 0">
  83 + <span class="stat-tag-inner">
  84 + <i class="el-icon-arrow-down"></i>
  85 + <span class="stat-tag-text">
  86 + 上月新增: {{ formatNumber(memberStatistics.newMembersLastMonth) }}人
  87 + </span>
  88 + </span>
  89 + </span>
  90 + </div>
  91 + </div>
  92 + <div class="member-stat-item stat-item-2">
  93 + <div class="stat-main">
  94 + <div class="stat-icon">
  95 + <i class="el-icon-success"></i>
  96 + </div>
  97 + <div class="stat-content">
  98 + <div class="stat-label">活跃会员数</div>
  99 + <div class="stat-value">{{ formatNumber(memberStatistics.activeMembers) }}</div>
  100 + </div>
  101 + </div>
  102 + <div class="stat-tags">
  103 + <span class="stat-tag">
  104 + <span class="stat-tag-inner">
  105 + <i class="el-icon-success"></i>
  106 + <span class="stat-tag-text">
  107 + 活跃(≤3天): {{ formatNumber(memberStatistics.active_0_3) }}
  108 + </span>
  109 + </span>
  110 + </span>
  111 + <span class="stat-tag">
  112 + <span class="stat-tag-inner">
  113 + <i class="el-icon-date"></i>
  114 + <span class="stat-tag-text">
  115 + 常到店(4-59天): {{ formatNumber(memberStatistics.active_4_59) }}
  116 + </span>
  117 + </span>
  118 + </span>
  119 + <span class="stat-tag">
  120 + <span class="stat-tag-inner">
  121 + <i class="el-icon-data-line"></i>
  122 + <span class="stat-tag-text">
  123 + 60天活跃率: {{ memberStatistics.activeRate }}%
  124 + </span>
  125 + </span>
  126 + </span>
  127 + <span class="stat-tag">
  128 + <span class="stat-tag-inner">
  129 + <i class="el-icon-aim"></i>
  130 + <span class="stat-tag-text">
  131 + 30天活跃率: {{ memberStatistics.activeRate30 }}%
  132 + </span>
  133 + </span>
  134 + </span>
  135 + </div>
  136 + </div>
  137 + <div class="member-stat-item stat-item-3">
  138 + <div class="stat-main">
  139 + <div class="stat-icon">
  140 + <i class="el-icon-wallet"></i>
  141 + </div>
  142 + <div class="stat-content">
  143 + <div class="stat-label">剩余权益总金额</div>
  144 + <div class="stat-value">¥{{ formatMoney(memberStatistics.totalRemainingAmount) }}</div>
  145 + </div>
  146 + </div>
  147 + <div class="stat-tags">
  148 + <span class="stat-tag">
  149 + <span class="stat-tag-inner">
  150 + <i class="el-icon-user"></i>
  151 + <span class="stat-tag-text">
  152 + 人均: {{ formatMoney(memberStatistics.avgRemainingAmount) }}元
  153 + </span>
  154 + </span>
  155 + </span>
  156 + <span class="stat-tag" v-if="memberStatistics.topRemainingAmount > 0">
  157 + <span class="stat-tag-inner">
  158 + <i class="el-icon-user-solid"></i>
  159 + <span class="stat-tag-text">
  160 + 最高剩余权益金额: {{ memberStatistics.topRemainingMemberName || '无' }} ¥{{
  161 + formatMoney(memberStatistics.topRemainingAmount) }}
  162 + </span>
  163 + </span>
  164 + </span>
  165 + <span class="stat-tag" v-if="memberStatistics.topBillingAmount > 0">
  166 + <span class="stat-tag-inner">
  167 + <i class="el-icon-wallet"></i>
  168 + <span class="stat-tag-text">
  169 + 本月开单最高: {{ memberStatistics.topBillingMemberName || '无' }} ¥{{
  170 + formatMoney(memberStatistics.topBillingAmount) }}
  171 + </span>
  172 + </span>
  173 + </span>
  174 + <span class="stat-tag" v-if="memberStatistics.topConsumeAmount > 0">
  175 + <span class="stat-tag-inner">
  176 + <i class="el-icon-medal"></i>
  177 + <span class="stat-tag-text">
  178 + 本月消耗最高: {{ memberStatistics.topConsumeMemberName || '无' }} ¥{{
  179 + formatMoney(memberStatistics.topConsumeAmount) }}
  180 + </span>
  181 + </span>
  182 + </span>
  183 + </div>
  184 + </div>
  185 + <div class="member-stat-item stat-item-4">
  186 + <div class="stat-main">
  187 + <div class="stat-icon">
  188 + <i class="el-icon-warning"></i>
  189 + </div>
  190 + <div class="stat-content">
  191 + <div class="stat-label">沉睡会员数</div>
  192 + <div class="stat-value">{{ formatNumber(memberStatistics.totalSleepMembers) }}</div>
  193 + </div>
  194 + </div>
  195 + <div class="stat-tags">
  196 + <span class="stat-tag">
  197 + <span class="stat-tag-inner">
  198 + <i class="el-icon-time"></i>
  199 + <span class="stat-tag-text">
  200 + 60-89天: {{ formatNumber(memberStatistics.sleep_60_89) }}
  201 + </span>
  202 + </span>
  203 + </span>
  204 + <span class="stat-tag">
  205 + <span class="stat-tag-inner">
  206 + <i class="el-icon-warning-outline"></i>
  207 + <span class="stat-tag-text">
  208 + 90-179天: {{ formatNumber(memberStatistics.sleep_90_179) }}
  209 + </span>
  210 + </span>
  211 + </span>
  212 + <span class="stat-tag">
  213 + <span class="stat-tag-inner">
  214 + <i class="el-icon-warning-outline"></i>
  215 + <span class="stat-tag-text">
  216 + 180-359天: {{ formatNumber(memberStatistics.sleep_180_359) }}
  217 + </span>
  218 + </span>
  219 + </span>
  220 + <span class="stat-tag">
  221 + <span class="stat-tag-inner">
  222 + <i class="el-icon-warning-outline"></i>
  223 + <span class="stat-tag-text">
  224 + 360天+: {{ formatNumber(memberStatistics.sleep_360_plus) }}
  225 + </span>
  226 + </span>
  227 + </span>
  228 + </div>
  229 + </div>
  230 + </div>
  231 + </el-card>
  232 + </el-col>
  233 +
  234 + <!-- 会员类型分布 -->
  235 + <el-col :span="8">
  236 + <el-card class="dashboard-card member-type-card" shadow="hover">
  237 + <div slot="header" class="card-title">
  238 + <span><i class="el-icon-pie-chart"></i> 会员类型分布</span>
  239 + </div>
  240 + <div ref="memberTypeChart" class="member-type-chart"></div>
  241 + </el-card>
  242 + </el-col>
  243 +
  244 + <!-- 会员分类统计 -->
  245 + <el-col :span="8">
  246 + <el-card class="dashboard-card member-category-card" shadow="hover">
  247 + <div slot="header" class="card-title">
  248 + <span><i class="el-icon-data-analysis"></i> 会员分类统计</span>
  249 + </div>
  250 + <div class="member-category-content">
  251 + <div ref="memberCategoryChart" class="member-category-chart"></div>
  252 + <div class="member-category-legend">
  253 + <div class="legend-item">
  254 + <div class="legend-color" style="background: #409EFF;"></div>
  255 + <div class="legend-text">
  256 + <div class="legend-name">生美会员</div>
  257 + <div class="legend-value">{{ formatNumber(memberStatistics.beautyMembers) }}人</div>
  258 + </div>
  259 + </div>
  260 + <div class="legend-item">
  261 + <div class="legend-color" style="background: #F56C6C;"></div>
  262 + <div class="legend-text">
  263 + <div class="legend-name">医美会员</div>
  264 + <div class="legend-value">{{ formatNumber(memberStatistics.medicalMembers) }}人</div>
  265 + </div>
  266 + </div>
  267 + <div class="legend-item">
  268 + <div class="legend-color" style="background: #67C23A;"></div>
  269 + <div class="legend-text">
  270 + <div class="legend-name">科技部会员</div>
  271 + <div class="legend-value">{{ formatNumber(memberStatistics.techMembers) }}人</div>
  272 + </div>
  273 + </div>
  274 + </div>
  275 + </div>
  276 + </el-card>
  277 + </el-col>
  278 + </el-row>
  279 +
49 280 <!-- 第二层:深度分析区 (趋势 + 流量) -->
50 281 <el-row :gutter="16" class="analysis-row">
51 282 <!-- 营收/消耗趋势 -->
... ... @@ -190,6 +421,52 @@
190 421 <el-button type="primary" size="mini" @click="confirmFieldConfig">确 定</el-button>
191 422 </div>
192 423 </el-dialog>
  424 +
  425 + <!-- 科技感弹窗预览 -->
  426 + <el-dialog :visible.sync="showTechModal" :title="techModalTitle" :width="techModalWidth" custom-class="tech-dialog"
  427 + append-to-body :close-on-click-modal="false">
  428 + <div class="tech-dialog-controls">
  429 + <el-input v-model="techModalTitle" size="mini" placeholder="弹窗标题" class="control-item" />
  430 + <el-input v-model="techModalWidth" size="mini" placeholder="宽度 如 960px / 80%" class="control-item" />
  431 + <el-input v-model="techModalHeight" size="mini" placeholder="高度 如 70vh" class="control-item" />
  432 + <el-button size="mini" icon="el-icon-refresh" @click="resetTechModal">重置</el-button>
  433 + </div>
  434 + <div class="tech-dialog-body" :style="{ maxHeight: techModalHeight }">
  435 + <div class="tech-grid">
  436 + <div class="tech-card neon-blue">
  437 + <div class="tech-card-title">核心权益</div>
  438 + <div class="tech-card-value">¥{{ formatMoney(memberStatistics.totalRemainingAmount) }}</div>
  439 + <div class="tech-card-desc">剩余权益总额</div>
  440 + </div>
  441 + <div class="tech-card neon-green">
  442 + <div class="tech-card-title">活跃度</div>
  443 + <div class="tech-card-value">{{ memberStatistics.activeRate }}%</div>
  444 + <div class="tech-card-desc">60天活跃率</div>
  445 + </div>
  446 + <div class="tech-card neon-orange">
  447 + <div class="tech-card-title">睡眠会员</div>
  448 + <div class="tech-card-value">{{ formatNumber(memberStatistics.totalSleepMembers) }}</div>
  449 + <div class="tech-card-desc">需唤醒用户</div>
  450 + </div>
  451 + </div>
  452 + <div class="tech-section">
  453 + <div class="tech-section-title">
  454 + <span>组件内容区域</span>
  455 + <span class="subtitle">超出高度自动出现滚动条</span>
  456 + </div>
  457 + <div class="tech-timeline">
  458 + <div class="tech-timeline-item" v-for="i in 6" :key="i">
  459 + <div class="dot"></div>
  460 + <div class="content">
  461 + <div class="title">事件 {{ i }}</div>
  462 + <div class="desc">这里放任意组件或文本,模拟实际穿透内容。</div>
  463 + </div>
  464 + <div class="time">T-{{ i }}</div>
  465 + </div>
  466 + </div>
  467 + </div>
  468 + </div>
  469 + </el-dialog>
193 470 </div>
194 471 </template>
195 472  
... ... @@ -209,7 +486,7 @@ export default {
209 486 storeOptions: [],
210 487 // 核心 KPI 数据
211 488 kpiData: {},
212   - trendType: 'month',
  489 + trendType: 'day',
213 490 coachRankType: 'billing',
214 491 // 看板核心数据
215 492 trendData: [],
... ... @@ -224,9 +501,43 @@ export default {
224 501 billingItemTop10: [], // 开单品项TOP10
225 502 tkStatisticsData: null,
226 503 customerVisitFrequencyData: [],
  504 + // 会员统计数据
  505 + memberStatistics: {
  506 + totalMembers: 0,
  507 + newMembers: 0,
  508 + newMembersLastMonth: 0,
  509 + active_0_3: 0,
  510 + active_4_59: 0,
  511 + activeMembers: 0,
  512 + activeRate: 0,
  513 + activeRate30: 0,
  514 + totalRemainingAmount: 0,
  515 + avgRemainingAmount: 0,
  516 + topRemainingMemberName: '',
  517 + topRemainingAmount: 0,
  518 + topBillingMemberName: '',
  519 + topBillingAmount: 0,
  520 + topConsumeMemberName: '',
  521 + topConsumeAmount: 0,
  522 + totalSleepMembers: 0,
  523 + sleep_60_89: 0,
  524 + sleep_90_179: 0,
  525 + sleep_180_359: 0,
  526 + sleep_360_plus: 0,
  527 + beautyMembers: 0,
  528 + medicalMembers: 0,
  529 + techMembers: 0,
  530 + educationMembers: 0
  531 + },
  532 + memberTypeDistribution: [], // 会员类型分布
227 533 // 内部状态
228 534 loading: false,
229 535 showFieldConfigDialog: false,
  536 + // 科技感弹窗
  537 + showTechModal: false,
  538 + techModalTitle: '会员画像穿透预览',
  539 + techModalWidth: '960px',
  540 + techModalHeight: '70vh',
230 541 availableFields: [
231 542 { prop: 'StoreName', label: '门店名称' },
232 543 { prop: 'BillingAmount', label: '开单金额' },
... ... @@ -242,7 +553,7 @@ export default {
242 553 const d = this.kpiData || {}
243 554 return [
244 555 { label: '本月成交总额', value: this.formatMoney(d.TotalBillingAmount), icon: 'el-icon-wallet', type: 'primary', isMoney: true },
245   - { label: '本月消耗价值', value: this.formatMoney(d.TotalConsumeAmount), icon: 'el-icon-medal', type: 'success', isMoney: true },
  556 + { label: '本月消耗金额', value: this.formatMoney(d.TotalConsumeAmount), icon: 'el-icon-medal', type: 'success', isMoney: true },
246 557 { label: '完成业绩(净额)', value: this.formatMoney(d.CompletedBillingAmount), icon: 'el-icon-trophy', type: 'warning', isMoney: true },
247 558 { label: '开单目标达成', value: d.BillingCompletionRate || 0, icon: 'el-icon-pie-chart', type: 'info', isPercent: true, target: this.formatMoney(d.TargetBillingAmount), status: (d.BillingCompletionRate >= 100) ? 'up' : 'down' },
248 559 { label: '本月拓客人数', value: (this.tkStatisticsData && this.tkStatisticsData.TkCount) ? this.tkStatisticsData.TkCount : 0, icon: 'el-icon-user-solid', type: 'danger', isPercent: false, target: null, status: null },
... ... @@ -296,12 +607,28 @@ export default {
296 607 beforeDestroy() {
297 608 window.removeEventListener('resize', this.handleResize)
298 609 Object.values(this.charts).forEach(c => c && c.dispose())
  610 + if (this.charts.memberTypeChart) {
  611 + this.charts.memberTypeChart.dispose()
  612 + }
  613 + if (this.charts.memberCategoryChart) {
  614 + this.charts.memberCategoryChart.dispose()
  615 + }
299 616 },
300 617 methods: {
301 618 // 设置默认查询范围:本月
302 619 setDefaultTimeRange() {
303 620 this.query.month = dayjs().format('YYYY-MM')
304 621 },
  622 + // 打开科技感弹窗
  623 + openTechModal() {
  624 + this.showTechModal = true
  625 + },
  626 + // 重置弹窗大小与标题
  627 + resetTechModal() {
  628 + this.techModalTitle = '会员画像穿透预览'
  629 + this.techModalWidth = '960px'
  630 + this.techModalHeight = '70vh'
  631 + },
305 632 // 年月选择器变化处理
306 633 handleMonthChange() {
307 634 // 自动触发查询
... ... @@ -391,7 +718,8 @@ export default {
391 718 this.loadTrends(trendParams),
392 719 this.loadTkFunnel(dateParams),
393 720 this.loadRankings({ ...monthParams, ...dateParams }),
394   - this.loadInsights(dateParams)
  721 + this.loadInsights(dateParams),
  722 + this.loadMemberStatistics({ statisticsMonth: currentMonth })
395 723 ])
396 724 // 确保数据加载完成后再渲染图表
397 725 await this.$nextTick()
... ... @@ -603,6 +931,82 @@ export default {
603 931 this.goldTriangleRankings = []
604 932 }
605 933 },
  934 + async loadMemberStatistics(p) {
  935 + try {
  936 + const res = await request({
  937 + url: '/api/Extend/LqReport/get-dashboard-data',
  938 + method: 'POST',
  939 + data: {
  940 + StatisticsMonth: p.statisticsMonth
  941 + }
  942 + })
  943 + if (res.data && res.data.Success && res.data.Data && res.data.Data.MemberStatistics) {
  944 + const ms = res.data.Data.MemberStatistics
  945 + const active_0_3 = ms.ActiveMembers0_3 || 0
  946 + const active_4_59 = ms.ActiveMembers4_59 || 0
  947 + const totalMembers = ms.TotalMembers || 0
  948 + const active30 = ms.ActiveMembers || 0
  949 + const active60 = active_0_3 + active_4_59
  950 + const activeRate60 = totalMembers > 0 ? Math.round((active60 / totalMembers) * 10000) / 100 : 0
  951 + const activeRate30 = totalMembers > 0 ? Math.round((active30 / totalMembers) * 10000) / 100 : 0
  952 + this.memberStatistics = {
  953 + totalMembers,
  954 + newMembers: ms.NewMembersThisMonth || 0,
  955 + newMembersLastMonth: ms.NewMembersLastMonth || 0,
  956 + active_0_3,
  957 + active_4_59,
  958 + // 活跃会员数:沉睡天数 ≤ 59 天(即活跃 + 常到店)
  959 + activeMembers: active60,
  960 + // 60天活跃率
  961 + activeRate: activeRate60,
  962 + // 30天活跃率(后端ActiveMembers本身为30天口径)
  963 + activeRate30,
  964 + totalRemainingAmount: ms.TotalRemainingAmount || 0,
  965 + avgRemainingAmount: ms.AvgRemainingAmount || 0,
  966 + topRemainingMemberName: ms.TopRemainingMemberName || '',
  967 + topRemainingAmount: ms.TopRemainingAmount || 0,
  968 + topBillingMemberName: ms.TopBillingMemberName || '',
  969 + topBillingAmount: ms.TopBillingAmount || 0,
  970 + topConsumeMemberName: ms.TopConsumeMemberName || '',
  971 + topConsumeAmount: ms.TopConsumeAmount || 0,
  972 + totalSleepMembers: ms.TotalSleepMembers || 0,
  973 + sleep_60_89: ms.SleepMembers60_89 || 0,
  974 + sleep_90_179: ms.SleepMembers90_179 || 0,
  975 + sleep_180_359: ms.SleepMembers180_359 || 0,
  976 + sleep_360_plus: ms.SleepMembers360Plus || 0,
  977 + beautyMembers: ms.BeautyMembers || 0,
  978 + medicalMembers: ms.MedicalMembers || 0,
  979 + techMembers: ms.TechMembers || 0,
  980 + educationMembers: ms.EducationMembers || 0
  981 + }
  982 + this.memberTypeDistribution = ms.MemberTypeDistribution || []
  983 + // 等待DOM更新后渲染会员类型分布饼图和分类雷达图,并更新tag滚动
  984 + this.$nextTick(() => {
  985 + this.initMemberTypeChart()
  986 + this.initMemberCategoryChart()
  987 + this.updateStatTagMarquee()
  988 + })
  989 + }
  990 + } catch (error) {
  991 + console.error('加载会员统计数据失败:', error)
  992 + this.memberStatistics = {}
  993 + this.memberTypeDistribution = []
  994 + }
  995 + },
  996 + updateStatTagMarquee() {
  997 + if (!this.$el) return
  998 + const tags = this.$el.querySelectorAll('.member-stat-item .stat-tag')
  999 + tags.forEach(tag => {
  1000 + const inner = tag.querySelector('.stat-tag-inner')
  1001 + if (!inner) return
  1002 + // 以整个tag的可视宽度作为判断标准,内容超过tag宽度才滚动
  1003 + if (inner.scrollWidth > tag.clientWidth + 1) {
  1004 + inner.classList.add('is-marquee')
  1005 + } else {
  1006 + inner.classList.remove('is-marquee')
  1007 + }
  1008 + })
  1009 + },
606 1010 async loadInsights(p) {
607 1011 try {
608 1012 const [freq, item] = await Promise.all([
... ... @@ -673,6 +1077,201 @@ export default {
673 1077 // renderGoldChart 已移除,改为列表显示
674 1078 this.renderVisitChart()
675 1079 // renderCategoryChart 已移除,改为列表显示
  1080 + this.initMemberTypeChart()
  1081 + this.initMemberCategoryChart()
  1082 + },
  1083 + // 初始化会员类型分布饼图
  1084 + initMemberTypeChart() {
  1085 + if (!this.$refs.memberTypeChart || !this.memberTypeDistribution || this.memberTypeDistribution.length === 0) {
  1086 + return
  1087 + }
  1088 + if (this.charts.memberTypeChart) {
  1089 + this.charts.memberTypeChart.dispose()
  1090 + }
  1091 + const chartDom = this.$refs.memberTypeChart
  1092 + if (!chartDom) return
  1093 + this.charts.memberTypeChart = echarts.init(chartDom)
  1094 + const option = {
  1095 + tooltip: {
  1096 + trigger: 'item',
  1097 + formatter: '{a} <br/>{b}: {c}人 ({d}%)'
  1098 + },
  1099 + legend: {
  1100 + orient: 'vertical',
  1101 + left: '5%',
  1102 + top: 'middle',
  1103 + itemWidth: 14,
  1104 + itemHeight: 14,
  1105 + textStyle: {
  1106 + fontSize: 13,
  1107 + color: '#606266',
  1108 + fontWeight: 'normal'
  1109 + },
  1110 + itemGap: 12
  1111 + },
  1112 + series: [
  1113 + {
  1114 + name: '会员类型',
  1115 + type: 'pie',
  1116 + radius: ['50%', '85%'],
  1117 + center: ['65%', '50%'],
  1118 + avoidLabelOverlap: false,
  1119 + itemStyle: {
  1120 + borderRadius: 8,
  1121 + borderColor: '#fff',
  1122 + borderWidth: 3
  1123 + },
  1124 + label: {
  1125 + show: true,
  1126 + formatter: '{b}\n{c}人\n({d}%)',
  1127 + fontSize: 12,
  1128 + fontWeight: 'normal',
  1129 + color: '#303133',
  1130 + lineHeight: 16
  1131 + },
  1132 + labelLine: {
  1133 + show: true,
  1134 + length: 15,
  1135 + length2: 8,
  1136 + lineStyle: {
  1137 + width: 1
  1138 + }
  1139 + },
  1140 + emphasis: {
  1141 + label: {
  1142 + show: true,
  1143 + fontSize: 13,
  1144 + fontWeight: 'bold'
  1145 + },
  1146 + itemStyle: {
  1147 + shadowBlur: 10,
  1148 + shadowOffsetX: 0,
  1149 + shadowColor: 'rgba(0, 0, 0, 0.3)'
  1150 + }
  1151 + },
  1152 + data: this.memberTypeDistribution.map((item, index) => {
  1153 + const colors = ['#F56C6C', '#67C23A', '#409EFF', '#E6A23C']
  1154 + return {
  1155 + value: item.Count || 0,
  1156 + name: item.MemberType || '未知',
  1157 + itemStyle: {
  1158 + color: colors[index % colors.length]
  1159 + }
  1160 + }
  1161 + })
  1162 + }
  1163 + ]
  1164 + }
  1165 + this.charts.memberTypeChart.setOption(option)
  1166 +
  1167 + // 响应式调整
  1168 + window.addEventListener('resize', () => {
  1169 + if (this.charts.memberTypeChart) {
  1170 + this.charts.memberTypeChart.resize()
  1171 + }
  1172 + })
  1173 + },
  1174 + // 初始化会员分类雷达图(移除教育部会员)
  1175 + initMemberCategoryChart() {
  1176 + if (!this.$refs.memberCategoryChart) {
  1177 + return
  1178 + }
  1179 + if (this.charts.memberCategoryChart) {
  1180 + this.charts.memberCategoryChart.dispose()
  1181 + }
  1182 + const chartDom = this.$refs.memberCategoryChart
  1183 + if (!chartDom) return
  1184 + this.charts.memberCategoryChart = echarts.init(chartDom)
  1185 +
  1186 + // 计算最大值用于归一化(只计算三个分类)
  1187 + const maxValue = Math.max(
  1188 + this.memberStatistics.beautyMembers || 0,
  1189 + this.memberStatistics.medicalMembers || 0,
  1190 + this.memberStatistics.techMembers || 0
  1191 + ) || 100
  1192 +
  1193 + const option = {
  1194 + tooltip: {
  1195 + trigger: 'item',
  1196 + formatter: (params) => {
  1197 + const name = params.name
  1198 + let actualValue = 0
  1199 + switch (name) {
  1200 + case '生美会员': actualValue = this.memberStatistics.beautyMembers || 0; break
  1201 + case '医美会员': actualValue = this.memberStatistics.medicalMembers || 0; break
  1202 + case '科技部会员': actualValue = this.memberStatistics.techMembers || 0; break
  1203 + }
  1204 + return `${name}<br/>${actualValue}人`
  1205 + }
  1206 + },
  1207 + radar: {
  1208 + indicator: [
  1209 + { name: '生美会员', max: maxValue },
  1210 + { name: '医美会员', max: maxValue },
  1211 + { name: '科技部会员', max: maxValue }
  1212 + ],
  1213 + center: ['50%', '55%'],
  1214 + radius: '90%',
  1215 + nameGap: 10,
  1216 + splitNumber: 4,
  1217 + axisName: {
  1218 + color: '#303133',
  1219 + fontSize: 13,
  1220 + fontWeight: 'bold'
  1221 + },
  1222 + splitArea: {
  1223 + areaStyle: {
  1224 + color: ['rgba(64, 158, 255, 0.1)', 'rgba(64, 158, 255, 0.05)']
  1225 + }
  1226 + },
  1227 + splitLine: {
  1228 + lineStyle: {
  1229 + color: 'rgba(64, 158, 255, 0.2)'
  1230 + }
  1231 + },
  1232 + axisLine: {
  1233 + lineStyle: {
  1234 + color: 'rgba(64, 158, 255, 0.3)'
  1235 + }
  1236 + }
  1237 + },
  1238 + series: [
  1239 + {
  1240 + name: '会员分类统计',
  1241 + type: 'radar',
  1242 + data: [
  1243 + {
  1244 + value: [
  1245 + this.memberStatistics.beautyMembers || 0,
  1246 + this.memberStatistics.medicalMembers || 0,
  1247 + this.memberStatistics.techMembers || 0
  1248 + ],
  1249 + name: '会员分布',
  1250 + areaStyle: {
  1251 + color: 'rgba(64, 158, 255, 0.2)'
  1252 + },
  1253 + lineStyle: {
  1254 + color: '#409EFF',
  1255 + width: 2
  1256 + },
  1257 + itemStyle: {
  1258 + color: '#409EFF'
  1259 + },
  1260 + symbol: 'circle',
  1261 + symbolSize: 6
  1262 + }
  1263 + ]
  1264 + }
  1265 + ]
  1266 + }
  1267 + this.charts.memberCategoryChart.setOption(option)
  1268 +
  1269 + // 响应式调整
  1270 + window.addEventListener('resize', () => {
  1271 + if (this.charts.memberCategoryChart) {
  1272 + this.charts.memberCategoryChart.resize()
  1273 + }
  1274 + })
676 1275 },
677 1276 renderTrendChart() {
678 1277 const chart = this.getChart('revenueTrendChart')
... ... @@ -917,6 +1516,10 @@ export default {
917 1516 }
918 1517 },
919 1518 formatMoney(v) { return Number(v || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) },
  1519 + formatNumber(v) {
  1520 + const num = Number(v || 0)
  1521 + return num.toLocaleString()
  1522 + },
920 1523 formatPercent(v) { return Number(v || 0).toFixed(1) },
921 1524 getTkInviteRate() {
922 1525 if (!this.tkStatisticsData || !this.tkStatisticsData.TkCount) return 0
... ... @@ -1124,6 +1727,240 @@ export default {
1124 1727 width: 100%;
1125 1728 }
1126 1729  
  1730 + /* 会员统计区域样式 */
  1731 + .member-statistics-section {
  1732 + width: 100%;
  1733 + margin-right: 0px !important;
  1734 + margin-left: 0px !important;
  1735 + }
  1736 +
  1737 + /* 统一三个卡片的高度 */
  1738 + .member-overview-card,
  1739 + .member-type-card,
  1740 + .member-category-card {
  1741 + &::v-deep .el-card__body {
  1742 + height: 360px;
  1743 + padding: 16px;
  1744 + display: flex;
  1745 + flex-direction: column;
  1746 + }
  1747 + }
  1748 +
  1749 + .member-overview-grid {
  1750 + display: grid;
  1751 + grid-template-columns: repeat(2, minmax(0, 1fr));
  1752 + gap: 12px;
  1753 + flex: 1;
  1754 + width: 100%;
  1755 + }
  1756 +
  1757 + .member-stat-item {
  1758 + background: #f5f7fa;
  1759 + border-radius: 8px;
  1760 + padding: 16px;
  1761 + display: flex;
  1762 + flex-direction: column;
  1763 + transition: all 0.3s ease;
  1764 + border: none;
  1765 + box-shadow: none;
  1766 + height: 160px;
  1767 +
  1768 + &:hover {
  1769 + box-shadow: 0 8px 20px rgba(31, 45, 61, 0.12);
  1770 + transform: translateY(-2px);
  1771 + }
  1772 +
  1773 + .stat-main {
  1774 + display: flex;
  1775 + align-items: flex-start;
  1776 + margin-bottom: 12px;
  1777 + }
  1778 +
  1779 + .stat-icon {
  1780 + width: 56px;
  1781 + height: 56px;
  1782 + border-radius: 50%;
  1783 + display: flex;
  1784 + align-items: center;
  1785 + justify-content: center;
  1786 + font-size: 28px;
  1787 + margin-right: 14px;
  1788 + flex-shrink: 0;
  1789 + }
  1790 +
  1791 + &.stat-item-1 .stat-icon {
  1792 + background: rgba(64, 158, 255, 0.1);
  1793 + color: #409EFF;
  1794 + }
  1795 +
  1796 + &.stat-item-2 .stat-icon {
  1797 + background: rgba(103, 194, 58, 0.1);
  1798 + color: #67C23A;
  1799 + }
  1800 +
  1801 + &.stat-item-3 .stat-icon {
  1802 + background: rgba(230, 162, 60, 0.1);
  1803 + color: #E6A23C;
  1804 + }
  1805 +
  1806 + &.stat-item-4 .stat-icon {
  1807 + background: rgba(245, 108, 108, 0.1);
  1808 + color: #F56C6C;
  1809 + }
  1810 +
  1811 + .stat-content {
  1812 + flex: 1;
  1813 + min-width: 0;
  1814 + display: flex;
  1815 + flex-direction: column;
  1816 + justify-content: center;
  1817 + }
  1818 +
  1819 + .stat-label {
  1820 + font-size: 13px;
  1821 + color: #909399;
  1822 + font-weight: normal;
  1823 + margin-bottom: 6px;
  1824 + }
  1825 +
  1826 + .stat-value {
  1827 + font-size: 26px;
  1828 + font-weight: bold;
  1829 + color: #303133;
  1830 + line-height: 1.2;
  1831 + }
  1832 +
  1833 + .stat-tags {
  1834 + display: grid;
  1835 + grid-template-columns: repeat(2, minmax(0, 1fr));
  1836 + gap: 6px 8px;
  1837 + margin-top: 8px;
  1838 + }
  1839 +
  1840 + .stat-tag {
  1841 + display: block;
  1842 + background: #fff;
  1843 + border-radius: 12px;
  1844 + padding: 6px 10px;
  1845 + font-size: 12px;
  1846 + font-weight: 500;
  1847 + color: #303133;
  1848 + overflow: hidden;
  1849 + }
  1850 +
  1851 + .stat-tag-inner {
  1852 + display: inline-flex;
  1853 + align-items: center;
  1854 + }
  1855 +
  1856 + .stat-tag-text {
  1857 + display: inline-block;
  1858 + white-space: nowrap;
  1859 + }
  1860 +
  1861 + .stat-tag-inner.is-marquee {
  1862 + animation: stat-tag-marquee 8s linear infinite;
  1863 + }
  1864 +
  1865 + &.stat-item-1 .stat-tag-inner i {
  1866 + color: #409EFF;
  1867 + }
  1868 +
  1869 + &.stat-item-2 .stat-tag-inner i {
  1870 + color: #67C23A;
  1871 + }
  1872 +
  1873 + &.stat-item-3 .stat-tag-inner i {
  1874 + color: #E6A23C;
  1875 + }
  1876 +
  1877 + &.stat-item-4 .stat-tag-inner i {
  1878 + color: #f56c6c;
  1879 + }
  1880 + }
  1881 +
  1882 + @keyframes stat-tag-marquee {
  1883 + 0% {
  1884 + transform: translateX(0);
  1885 + }
  1886 +
  1887 + 100% {
  1888 + transform: translateX(-100%);
  1889 + }
  1890 + }
  1891 +
  1892 + /* 会员类型分布图表 */
  1893 + .member-type-chart {
  1894 + height: 100%;
  1895 + width: 100%;
  1896 + flex: 1;
  1897 + min-height: 0;
  1898 + }
  1899 +
  1900 + /* 会员分类统计 */
  1901 + .member-category-content {
  1902 + display: flex;
  1903 + align-items: center;
  1904 + gap: 12px;
  1905 + height: 100%;
  1906 + flex: 1;
  1907 + min-height: 0;
  1908 + }
  1909 +
  1910 + .member-category-chart {
  1911 + flex: 2;
  1912 + height: 100%;
  1913 + min-width: 0;
  1914 + }
  1915 +
  1916 + .member-category-legend {
  1917 + width: 140px;
  1918 + display: flex;
  1919 + flex-direction: column;
  1920 + gap: 12px;
  1921 + padding: 12px;
  1922 + background: #f5f7fa;
  1923 + border-radius: 6px;
  1924 + }
  1925 +
  1926 + .legend-item {
  1927 + display: flex;
  1928 + align-items: center;
  1929 + gap: 10px;
  1930 + padding: 8px;
  1931 + background: white;
  1932 + border-radius: 4px;
  1933 + transition: all 0.3s ease;
  1934 +
  1935 + &:hover {
  1936 + background: #f0f2f5;
  1937 + }
  1938 + }
  1939 +
  1940 + .legend-color {
  1941 + width: 24px;
  1942 + height: 24px;
  1943 + border-radius: 4px;
  1944 + flex-shrink: 0;
  1945 + }
  1946 +
  1947 + .legend-text {
  1948 + flex: 1;
  1949 +
  1950 + .legend-name {
  1951 + font-size: 12px;
  1952 + font-weight: normal;
  1953 + color: #606266;
  1954 + margin-bottom: 2px;
  1955 + }
  1956 +
  1957 + .legend-value {
  1958 + font-size: 14px;
  1959 + font-weight: bold;
  1960 + color: #303133;
  1961 + }
  1962 + }
  1963 +
1127 1964 /* 统一卡片高度 */
1128 1965 .analysis-row {
1129 1966 .dashboard-card {
... ... @@ -1301,5 +2138,152 @@ export default {
1301 2138 }
1302 2139 }
1303 2140 }
  2141 +
  2142 + /* 科技感弹窗 */
  2143 + ::v-deep .tech-dialog {
  2144 + .el-dialog__header {
  2145 + background: linear-gradient(90deg, rgba(64, 158, 255, 0.18), rgba(103, 194, 58, 0.12));
  2146 + border-bottom: 1px solid rgba(64, 158, 255, 0.15);
  2147 + }
  2148 +
  2149 + .el-dialog__body {
  2150 + background: radial-gradient(circle at 20% 20%, rgba(64, 158, 255, 0.08), transparent 35%),
  2151 + radial-gradient(circle at 80% 20%, rgba(103, 194, 58, 0.08), transparent 35%),
  2152 + #0f172a;
  2153 + color: #e5eaf3;
  2154 + padding: 16px;
  2155 + }
  2156 +
  2157 + .el-dialog {
  2158 + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
  2159 + border: 1px solid rgba(64, 158, 255, 0.25);
  2160 + }
  2161 + }
  2162 +
  2163 + .tech-dialog-controls {
  2164 + display: flex;
  2165 + gap: 8px;
  2166 + margin-bottom: 12px;
  2167 +
  2168 + .control-item {
  2169 + width: 180px;
  2170 + }
  2171 + }
  2172 +
  2173 + .tech-dialog-body {
  2174 + border: 1px solid rgba(255, 255, 255, 0.08);
  2175 + border-radius: 12px;
  2176 + padding: 12px;
  2177 + background: rgba(255, 255, 255, 0.03);
  2178 + overflow: auto;
  2179 + }
  2180 +
  2181 + .tech-grid {
  2182 + display: grid;
  2183 + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  2184 + gap: 12px;
  2185 + }
  2186 +
  2187 + .tech-card {
  2188 + border-radius: 12px;
  2189 + padding: 14px;
  2190 + color: #fff;
  2191 + position: relative;
  2192 + overflow: hidden;
  2193 + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
  2194 + }
  2195 +
  2196 + .neon-blue {
  2197 + background: linear-gradient(135deg, #1d4ed8, #0ea5e9);
  2198 + }
  2199 +
  2200 + .neon-green {
  2201 + background: linear-gradient(135deg, #15803d, #22c55e);
  2202 + }
  2203 +
  2204 + .neon-orange {
  2205 + background: linear-gradient(135deg, #c2410c, #f97316);
  2206 + }
  2207 +
  2208 + .tech-card-title {
  2209 + font-size: 13px;
  2210 + opacity: 0.9;
  2211 + }
  2212 +
  2213 + .tech-card-value {
  2214 + margin-top: 6px;
  2215 + font-size: 24px;
  2216 + font-weight: 700;
  2217 + }
  2218 +
  2219 + .tech-card-desc {
  2220 + margin-top: 4px;
  2221 + font-size: 12px;
  2222 + opacity: 0.8;
  2223 + }
  2224 +
  2225 + .tech-section {
  2226 + margin-top: 14px;
  2227 + border-radius: 12px;
  2228 + padding: 12px;
  2229 + background: rgba(255, 255, 255, 0.04);
  2230 + border: 1px solid rgba(255, 255, 255, 0.06);
  2231 + }
  2232 +
  2233 + .tech-section-title {
  2234 + display: flex;
  2235 + justify-content: space-between;
  2236 + color: #9ca3af;
  2237 + margin-bottom: 10px;
  2238 +
  2239 + .subtitle {
  2240 + font-size: 12px;
  2241 + opacity: 0.8;
  2242 + }
  2243 + }
  2244 +
  2245 + .tech-timeline {
  2246 + display: flex;
  2247 + flex-direction: column;
  2248 + gap: 10px;
  2249 + }
  2250 +
  2251 + .tech-timeline-item {
  2252 + display: grid;
  2253 + grid-template-columns: 16px 1fr 80px;
  2254 + align-items: center;
  2255 + gap: 10px;
  2256 + padding: 8px 10px;
  2257 + border-radius: 10px;
  2258 + background: rgba(255, 255, 255, 0.03);
  2259 + border: 1px solid rgba(255, 255, 255, 0.05);
  2260 + }
  2261 +
  2262 + .tech-timeline-item .dot {
  2263 + width: 10px;
  2264 + height: 10px;
  2265 + border-radius: 50%;
  2266 + background: #22c55e;
  2267 + box-shadow: 0 0 8px #22c55e;
  2268 + margin-left: 3px;
  2269 + }
  2270 +
  2271 + .tech-timeline-item .content .title {
  2272 + color: #e5e7eb;
  2273 + font-size: 13px;
  2274 + font-weight: 600;
  2275 + }
  2276 +
  2277 + .tech-timeline-item .content .desc {
  2278 + color: #9ca3af;
  2279 + font-size: 12px;
  2280 + margin-top: 2px;
  2281 + }
  2282 +
  2283 + .tech-timeline-item .time {
  2284 + text-align: right;
  2285 + color: #60a5fa;
  2286 + font-size: 12px;
  2287 + }
1304 2288 }
1305 2289 </style>
1306 2290 \ No newline at end of file
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs
... ... @@ -658,7 +658,10 @@ namespace NCC.Extend.LqReimbursementApplication
658 658 excelconfig.HeadPoint = 10;
659 659 excelconfig.IsAllSizeColumn = true;
660 660 excelconfig.ColumnModel = new List<ExcelColumnModel>();
661   - List<string> selectKeyList = input.selectKey.Split(',').ToList();
  661 + // 当未传入 selectKey 时,默认导出全部字段,避免空引用异常
  662 + List<string> selectKeyList = !string.IsNullOrEmpty(input.selectKey)
  663 + ? input.selectKey.Split(',').ToList()
  664 + : paramList.Select(p => p.field).ToList();
662 665 foreach (var item in selectKeyList)
663 666 {
664 667 var isExist = paramList.Find(p => p.field == item);
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqReportService.cs
... ... @@ -890,6 +890,109 @@ namespace NCC.Extend
890 890  
891 891 var consumePerformance = await _db.Ado.SqlQueryAsync<dynamic>(consumePerformanceSql, new { statisticsMonth });
892 892  
  893 + // 5. 会员统计汇总
  894 + var lastMonth = DateTime.ParseExact(statisticsMonth, "yyyyMM", null).AddMonths(-1).ToString("yyyyMM");
  895 + var memberStatisticsSql = $@"
  896 + SELECT
  897 + COUNT(*) as TotalMembers,
  898 + SUM(CASE WHEN DATE_FORMAT(F_CreateTime, '%Y%m') = @statisticsMonth THEN 1 ELSE 0 END) as NewMembersThisMonth,
  899 + SUM(CASE WHEN DATE_FORMAT(F_CreateTime, '%Y%m') = '{lastMonth}' THEN 1 ELSE 0 END) as NewMembersLastMonth,
  900 + SUM(CASE WHEN F_SleepDays IS NULL OR F_SleepDays <= 3 THEN 1 ELSE 0 END) as ActiveMembers0_3,
  901 + SUM(CASE WHEN F_SleepDays > 3 AND F_SleepDays < 60 THEN 1 ELSE 0 END) as ActiveMembers4_59,
  902 + SUM(CASE WHEN F_LastVisitTime IS NOT NULL AND DATEDIFF(NOW(), F_LastVisitTime) <= 30 THEN 1 ELSE 0 END) as ActiveMembers30d,
  903 + SUM(CASE WHEN F_SleepDays >= 60 AND F_SleepDays < 90 THEN 1 ELSE 0 END) as SleepMembers60_89,
  904 + SUM(CASE WHEN F_SleepDays >= 90 AND F_SleepDays < 180 THEN 1 ELSE 0 END) as SleepMembers90_179,
  905 + SUM(CASE WHEN F_SleepDays >= 180 AND F_SleepDays < 360 THEN 1 ELSE 0 END) as SleepMembers180_359,
  906 + SUM(CASE WHEN F_SleepDays >= 360 THEN 1 ELSE 0 END) as SleepMembers360Plus,
  907 + COALESCE(SUM(F_RemainingRightsAmount), 0) as TotalRemainingAmount,
  908 + SUM(CASE WHEN F_IsBeautyMember = 1 THEN 1 ELSE 0 END) as BeautyMembers,
  909 + SUM(CASE WHEN F_IsMedicalMember = 1 THEN 1 ELSE 0 END) as MedicalMembers,
  910 + SUM(CASE WHEN F_IsTechMember = 1 THEN 1 ELSE 0 END) as TechMembers,
  911 + SUM(CASE WHEN F_IsEducationMember = 1 THEN 1 ELSE 0 END) as EducationMembers
  912 + FROM lq_khxx
  913 + WHERE F_IsEffective = 1";
  914 +
  915 + var memberStatistics = await _db.Ado.SqlQueryAsync<dynamic>(memberStatisticsSql, new { statisticsMonth });
  916 + var memberStats = memberStatistics.FirstOrDefault();
  917 +
  918 + // 6. 会员类型分布统计
  919 + var memberTypeDistributionSql = @"
  920 + SELECT
  921 + CASE
  922 + WHEN khlx = '0' THEN '线索'
  923 + WHEN khlx = '1' THEN '新客'
  924 + WHEN khlx = '2' THEN '散客'
  925 + WHEN khlx = '3' THEN '会员'
  926 + ELSE '未知'
  927 + END as MemberType,
  928 + COUNT(*) as Count
  929 + FROM lq_khxx
  930 + WHERE F_IsEffective = 1
  931 + GROUP BY khlx
  932 + ORDER BY
  933 + CASE khlx
  934 + WHEN '0' THEN 1
  935 + WHEN '1' THEN 2
  936 + WHEN '2' THEN 3
  937 + WHEN '3' THEN 4
  938 + ELSE 5
  939 + END";
  940 +
  941 + var memberTypeDistribution = await _db.Ado.SqlQueryAsync<dynamic>(memberTypeDistributionSql);
  942 +
  943 + // 7. 最高剩余权益会员、本月开单金额最高会员、本月消耗金额最高会员
  944 + var topRemainingSql = @"
  945 + SELECT
  946 + kh.Khmc as MemberName,
  947 + COALESCE(kh.F_RemainingRightsAmount, 0) as Amount
  948 + FROM lq_khxx kh
  949 + WHERE kh.F_IsEffective = 1
  950 + ORDER BY Amount DESC
  951 + LIMIT 1";
  952 +
  953 + var topRemaining = (await _db.Ado.SqlQueryAsync<dynamic>(topRemainingSql)).FirstOrDefault();
  954 +
  955 + var topBillingSql = @"
  956 + SELECT
  957 + kh.Khmc as MemberName,
  958 + COALESCE(SUM(CAST(kd.sfyj AS DECIMAL(18,2))), 0) as Amount
  959 + FROM lq_kd_kdjlb kd
  960 + INNER JOIN lq_khxx kh ON kh.F_Id = kd.kdhy
  961 + WHERE kd.F_IsEffective = 1
  962 + AND DATE_FORMAT(kd.kdrq, '%Y%m') = @statisticsMonth
  963 + GROUP BY kh.F_Id, kh.Khmc
  964 + ORDER BY Amount DESC
  965 + LIMIT 1";
  966 +
  967 + var topBilling = (await _db.Ado.SqlQueryAsync<dynamic>(topBillingSql, new { statisticsMonth })).FirstOrDefault();
  968 +
  969 + var topConsumeSql = @"
  970 + SELECT
  971 + kh.Khmc as MemberName,
  972 + COALESCE(SUM(CAST(xh.xfje AS DECIMAL(18,2))), 0) as Amount
  973 + FROM lq_xh_hyhk xh
  974 + INNER JOIN lq_khxx kh ON kh.F_Id = xh.hy
  975 + WHERE xh.F_IsEffective = 1
  976 + AND DATE_FORMAT(xh.hksj, '%Y%m') = @statisticsMonth
  977 + GROUP BY kh.F_Id, kh.Khmc
  978 + ORDER BY Amount DESC
  979 + LIMIT 1";
  980 +
  981 + var topConsume = (await _db.Ado.SqlQueryAsync<dynamic>(topConsumeSql, new { statisticsMonth })).FirstOrDefault();
  982 +
  983 + var totalMembers = Convert.ToInt32(memberStats?.TotalMembers ?? 0);
  984 + var activeMembers0_3 = Convert.ToInt32(memberStats?.ActiveMembers0_3 ?? 0);
  985 + var activeMembers4_59 = Convert.ToInt32(memberStats?.ActiveMembers4_59 ?? 0);
  986 + var activeMembers = Convert.ToInt32(memberStats?.ActiveMembers30d ?? 0);
  987 + var activeRate = totalMembers > 0 ? Math.Round((double)activeMembers / totalMembers * 100, 2) : 0;
  988 + var totalRemainingAmount = Convert.ToDecimal(memberStats?.TotalRemainingAmount ?? 0m);
  989 + var avgRemainingAmount = totalMembers > 0 ? Math.Round((double)totalRemainingAmount / totalMembers, 2) : 0;
  990 +
  991 + var sleep60_89 = Convert.ToInt32(memberStats?.SleepMembers60_89 ?? 0);
  992 + var sleep90_179 = Convert.ToInt32(memberStats?.SleepMembers90_179 ?? 0);
  993 + var sleep180_359 = Convert.ToInt32(memberStats?.SleepMembers180_359 ?? 0);
  994 + var sleep360Plus = Convert.ToInt32(memberStats?.SleepMembers360Plus ?? 0);
  995 +
893 996 var result = new
894 997 {
895 998 StatisticsMonth = statisticsMonth,
... ... @@ -914,7 +1017,6 @@ namespace NCC.Extend
914 1017 GoldTriangleCount = goldTrianglePerformance.FirstOrDefault()?.GoldTriangleCount ?? 0,
915 1018 TotalPerformance = goldTrianglePerformance.FirstOrDefault()?.TotalPerformance ?? 0,
916 1019 TotalOrderCount = goldTrianglePerformance.FirstOrDefault()?.TotalOrderCount ?? 0,
917   - TotalMemberCount = goldTrianglePerformance.FirstOrDefault()?.TotalMemberCount ?? 0,
918 1020 AvgPerformance = goldTrianglePerformance.FirstOrDefault()?.AvgPerformance ?? 0
919 1021 },
920 1022 ConsumePerformance = new
... ... @@ -924,6 +1026,38 @@ namespace NCC.Extend
924 1026 TotalConsumeQuantity = consumePerformance.FirstOrDefault()?.TotalConsumeQuantity ?? 0,
925 1027 TotalHeadCount = consumePerformance.FirstOrDefault()?.TotalHeadCount ?? 0,
926 1028 TotalPersonCount = consumePerformance.FirstOrDefault()?.TotalPersonCount ?? 0
  1029 + },
  1030 + MemberStatistics = new
  1031 + {
  1032 + TotalMembers = totalMembers,
  1033 + NewMembersThisMonth = Convert.ToInt32(memberStats?.NewMembersThisMonth ?? 0),
  1034 + NewMembersLastMonth = Convert.ToInt32(memberStats?.NewMembersLastMonth ?? 0),
  1035 + ActiveMembers0_3 = activeMembers0_3,
  1036 + ActiveMembers4_59 = activeMembers4_59,
  1037 + ActiveMembers = activeMembers,
  1038 + ActiveRate = activeRate,
  1039 + SleepMembers60_89 = sleep60_89,
  1040 + SleepMembers90_179 = sleep90_179,
  1041 + SleepMembers180_359 = sleep180_359,
  1042 + SleepMembers360Plus = sleep360Plus,
  1043 + TotalSleepMembers = sleep60_89 + sleep90_179 + sleep180_359 + sleep360Plus,
  1044 + TotalRemainingAmount = totalRemainingAmount,
  1045 + AvgRemainingAmount = avgRemainingAmount,
  1046 + TopRemainingMemberName = topRemaining?.MemberName ?? string.Empty,
  1047 + TopRemainingAmount = Convert.ToDecimal(topRemaining?.Amount ?? 0m),
  1048 + TopBillingMemberName = topBilling?.MemberName ?? string.Empty,
  1049 + TopBillingAmount = Convert.ToDecimal(topBilling?.Amount ?? 0m),
  1050 + TopConsumeMemberName = topConsume?.MemberName ?? string.Empty,
  1051 + TopConsumeAmount = Convert.ToDecimal(topConsume?.Amount ?? 0m),
  1052 + BeautyMembers = Convert.ToInt32(memberStats?.BeautyMembers ?? 0),
  1053 + MedicalMembers = Convert.ToInt32(memberStats?.MedicalMembers ?? 0),
  1054 + TechMembers = Convert.ToInt32(memberStats?.TechMembers ?? 0),
  1055 + EducationMembers = Convert.ToInt32(memberStats?.EducationMembers ?? 0),
  1056 + MemberTypeDistribution = memberTypeDistribution.Select(x => new
  1057 + {
  1058 + MemberType = x.MemberType?.ToString() ?? "",
  1059 + Count = Convert.ToInt32(x.Count ?? 0)
  1060 + }).ToList()
927 1061 }
928 1062 };
929 1063  
... ...
sql/排查生美业绩统计差异-简化版.sql
... ... @@ -131,3 +131,4 @@ ORDER BY 生美业绩 DESC;
131 131  
132 132  
133 133  
  134 +
... ...
sql/排查生美业绩统计差异详细.sql
... ... @@ -183,3 +183,4 @@ HAVING COUNT(*) &gt; 1;
183 183  
184 184  
185 185  
  186 +
... ...
sql/检查生美业绩统计差异.sql
... ... @@ -153,3 +153,4 @@ ORDER BY 生美业绩 DESC;
153 153  
154 154  
155 155  
  156 +
... ...
test_goddess_card_members.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试女神卡列表接口(带门店筛选)
  4 +
  5 +echo "=========================================="
  6 +echo "测试女神卡列表接口"
  7 +echo "=========================================="
  8 +
  9 +# 获取token
  10 +echo "1. 获取登录token..."
  11 +TOKEN=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  12 + -H "Content-Type: application/x-www-form-urlencoded" \
  13 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | \
  14 + python3 -c "import sys, json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null)
  15 +
  16 +if [ -z "$TOKEN" ]; then
  17 + echo "❌ 获取token失败"
  18 + exit 1
  19 +fi
  20 +
  21 +echo "✅ Token获取成功"
  22 +echo ""
  23 +
  24 +# 测试1: 不带门店筛选
  25 +echo "2. 测试1: 不带门店筛选..."
  26 +RESPONSE1=$(curl -s -X POST "http://localhost:2011/api/Extend/lqstatistics/GetGoddessCardMembers" \
  27 + -H "Authorization: $TOKEN" \
  28 + -H "Content-Type: application/json" \
  29 + -d '{
  30 + "PageIndex": 1,
  31 + "PageSize": 5
  32 + }')
  33 +
  34 +echo "$RESPONSE1" | python3 -c "
  35 +import sys, json
  36 +try:
  37 + data = json.load(sys.stdin)
  38 + if data.get('code') == 200:
  39 + total = data.get('data', {}).get('pagination', {}).get('total', 0)
  40 + count = len(data.get('data', {}).get('list', []))
  41 + print(f'✅ 成功 - 总记录数: {total}, 当前页记录数: {count}')
  42 + else:
  43 + print(f'❌ 失败 - Code: {data.get(\"code\")}, Msg: {data.get(\"msg\")}')
  44 +except Exception as e:
  45 + print(f'❌ 解析失败: {e}')
  46 + print(sys.stdin.read())
  47 +" 2>/dev/null || echo "$RESPONSE1" | head -10
  48 +echo ""
  49 +
  50 +# 获取一个门店ID用于测试
  51 +echo "3. 获取门店ID..."
  52 +STORE_ID=$(curl -s -X GET "http://localhost:2011/api/Extend/lqmdxx?currentPage=1&pageSize=1" \
  53 + -H "Authorization: $TOKEN" | \
  54 + python3 -c "import sys, json; data = json.load(sys.stdin); print(data.get('data', {}).get('list', [{}])[0].get('id', ''))" 2>/dev/null)
  55 +
  56 +if [ -z "$STORE_ID" ]; then
  57 + echo "❌ 获取门店ID失败"
  58 + exit 1
  59 +fi
  60 +
  61 +echo "✅ 门店ID: $STORE_ID"
  62 +echo ""
  63 +
  64 +# 测试2: 带单个门店筛选
  65 +echo "4. 测试2: 带单个门店筛选 (StoreId)..."
  66 +RESPONSE2=$(curl -s -X POST "http://localhost:2011/api/Extend/lqstatistics/GetGoddessCardMembers" \
  67 + -H "Authorization: $TOKEN" \
  68 + -H "Content-Type: application/json" \
  69 + -d "{
  70 + \"PageIndex\": 1,
  71 + \"PageSize\": 5,
  72 + \"StoreId\": \"$STORE_ID\"
  73 + }")
  74 +
  75 +echo "$RESPONSE2" | python3 -c "
  76 +import sys, json
  77 +try:
  78 + data = json.load(sys.stdin)
  79 + if data.get('code') == 200:
  80 + total = data.get('data', {}).get('pagination', {}).get('total', 0)
  81 + count = len(data.get('data', {}).get('list', []))
  82 + stores = set([m.get('storeName', '') for m in data.get('data', {}).get('list', [])])
  83 + print(f'✅ 成功 - 总记录数: {total}, 当前页记录数: {count}')
  84 + if stores:
  85 + print(f' 门店列表: {stores}')
  86 + else:
  87 + print(f'❌ 失败 - Code: {data.get(\"code\")}, Msg: {data.get(\"msg\")}')
  88 +except Exception as e:
  89 + print(f'❌ 解析失败: {e}')
  90 + print(sys.stdin.read())
  91 +" 2>/dev/null || echo "$RESPONSE2" | head -10
  92 +echo ""
  93 +
  94 +# 测试3: 带多个门店筛选
  95 +echo "5. 测试3: 带多个门店筛选 (StoreIds)..."
  96 +STORE_ID2=$(curl -s -X GET "http://localhost:2011/api/Extend/lqmdxx?currentPage=1&pageSize=2" \
  97 + -H "Authorization: $TOKEN" | \
  98 + python3 -c "import sys, json; data = json.load(sys.stdin); stores = data.get('data', {}).get('list', []); print(stores[1].get('id', '') if len(stores) > 1 else '')" 2>/dev/null)
  99 +
  100 +if [ -n "$STORE_ID2" ]; then
  101 + RESPONSE3=$(curl -s -X POST "http://localhost:2011/api/Extend/lqstatistics/GetGoddessCardMembers" \
  102 + -H "Authorization: $TOKEN" \
  103 + -H "Content-Type: application/json" \
  104 + -d "{
  105 + \"PageIndex\": 1,
  106 + \"PageSize\": 5,
  107 + \"StoreIds\": [\"$STORE_ID\", \"$STORE_ID2\"]
  108 + }")
  109 +
  110 + echo "$RESPONSE3" | python3 -c "
  111 +import sys, json
  112 +try:
  113 + data = json.load(sys.stdin)
  114 + if data.get('code') == 200:
  115 + total = data.get('data', {}).get('pagination', {}).get('total', 0)
  116 + count = len(data.get('data', {}).get('list', []))
  117 + stores = set([m.get('storeName', '') for m in data.get('data', {}).get('list', [])])
  118 + print(f'✅ 成功 - 总记录数: {total}, 当前页记录数: {count}')
  119 + if stores:
  120 + print(f' 门店列表: {stores}')
  121 + else:
  122 + print(f'❌ 失败 - Code: {data.get(\"code\")}, Msg: {data.get(\"msg\")}')
  123 +except Exception as e:
  124 + print(f'❌ 解析失败: {e}')
  125 + print(sys.stdin.read())
  126 +" 2>/dev/null || echo "$RESPONSE3" | head -10
  127 +else
  128 + echo "⚠️ 只有一个门店,跳过多门店筛选测试"
  129 +fi
  130 +echo ""
  131 +
  132 +echo "=========================================="
  133 +echo "测试完成"
  134 +echo "=========================================="
  135 +
... ...
test_tianwang_api.py
... ... @@ -84,3 +84,4 @@ print(f&quot;\n教育一部+教育二部合计 BillingPerformance: {total2}&quot;)
84 84  
85 85  
86 86  
  87 +
... ...
test_update_billing_info.sh 0 → 100755
  1 +#!/bin/bash
  2 +
  3 +# 测试修改开单信息接口
  4 +# 接口地址: PUT /api/Extend/lqkdkdjlb/UpdateBillingInfo
  5 +
  6 +echo "=========================================="
  7 +echo "测试修改开单信息接口"
  8 +echo "=========================================="
  9 +
  10 +# 获取token
  11 +echo "1. 获取登录token..."
  12 +TOKEN=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \
  13 + -H "Content-Type: application/x-www-form-urlencoded" \
  14 + -d "account=admin&password=e10adc3949ba59abbe56e057f20f883e" | \
  15 + python3 -c "import sys, json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null)
  16 +
  17 +if [ -z "$TOKEN" ]; then
  18 + echo "❌ 获取token失败"
  19 + exit 1
  20 +fi
  21 +
  22 +echo "✅ Token获取成功"
  23 +echo ""
  24 +
  25 +# 获取一个有效的开单记录ID
  26 +echo "2. 获取开单记录ID..."
  27 +BILLING_ID=$(curl -s -X GET "http://localhost:2011/api/Extend/lqkdkdjlb?currentPage=1&pageSize=1" \
  28 + -H "Authorization: $TOKEN" | \
  29 + python3 -c "import sys, json; data = json.load(sys.stdin); print(data.get('data', {}).get('list', [{}])[0].get('id', ''))" 2>/dev/null)
  30 +
  31 +if [ -z "$BILLING_ID" ]; then
  32 + echo "❌ 获取开单记录ID失败"
  33 + exit 1
  34 +fi
  35 +
  36 +echo "✅ 开单记录ID: $BILLING_ID"
  37 +echo ""
  38 +
  39 +# 测试1: 只更新备注和简介
  40 +echo "3. 测试1: 只更新备注和简介..."
  41 +RESPONSE1=$(curl -s -X PUT "http://localhost:2011/api/Extend/lqkdkdjlb/UpdateBillingInfo" \
  42 + -H "Authorization: $TOKEN" \
  43 + -H "Content-Type: application/json" \
  44 + -d "{
  45 + \"id\": \"$BILLING_ID\",
  46 + \"Bz\": \"测试备注信息 - $(date +%Y-%m-%d\ %H:%M:%S)\",
  47 + \"Jj\": \"测试简介信息 - $(date +%Y-%m-%d\ %H:%M:%S)\"
  48 + }")
  49 +
  50 +echo "$RESPONSE1" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE1"
  51 +echo ""
  52 +
  53 +# 测试2: 只更新备注
  54 +echo "4. 测试2: 只更新备注..."
  55 +RESPONSE2=$(curl -s -X PUT "http://localhost:2011/api/Extend/lqkdkdjlb/UpdateBillingInfo" \
  56 + -H "Authorization: $TOKEN" \
  57 + -H "Content-Type: application/json" \
  58 + -d "{
  59 + \"id\": \"$BILLING_ID\",
  60 + \"Bz\": \"只更新备注 - $(date +%Y-%m-%d\ %H:%M:%S)\"
  61 + }")
  62 +
  63 +echo "$RESPONSE2" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE2"
  64 +echo ""
  65 +
  66 +# 测试3: 只更新简介
  67 +echo "5. 测试3: 只更新简介..."
  68 +RESPONSE3=$(curl -s -X PUT "http://localhost:2011/api/Extend/lqkdkdjlb/UpdateBillingInfo" \
  69 + -H "Authorization: $TOKEN" \
  70 + -H "Content-Type: application/json" \
  71 + -d "{
  72 + \"id\": \"$BILLING_ID\",
  73 + \"Jj\": \"只更新简介 - $(date +%Y-%m-%d\ %H:%M:%S)\"
  74 + }")
  75 +
  76 +echo "$RESPONSE3" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE3"
  77 +echo ""
  78 +
  79 +echo "=========================================="
  80 +echo "测试完成"
  81 +echo "=========================================="
  82 +
... ...
用户画像数据清单.md 0 → 100644
  1 +# 用户画像数据清单(单个会员视角)
  2 +
  3 +> 目的:快速落地单个会员画像,可直接映射到前端“会员画像”页面。优先复用现有接口与字段,后续补充按需扩展。
  4 +
  5 +## 一、基础档案
  6 +- 会员ID、姓名、手机号
  7 +- 所属门店(gsmd + 门店名)、渠道/来源(khly)
  8 +- 创建时间、首次到店时间、最后到店时间(firstVisitTime, lastVisitTime)
  9 +- 会员类型标识:生美/医美/科技部/教育部(及标记时间)
  10 +- 消费等级(consumeLevel)、消费等级更新时间(consumeLevelUpdateTime)
  11 +- 睡眠天数(sleepDays),睡眠开始时间(sleepStartTime)
  12 +- 拓客人员、推荐人、主/副健康师
  13 +- 备注
  14 +
  15 +## 二、账户资产 / 权益
  16 +- 剩余权益总金额(RemainingRightsAmount)
  17 +- 剩余权益明细(`GetMemberRemainingItems`):
  18 + - 品项ID/名称、单价、来源类型(SourceType)
  19 + - 总购买数量、已消费数量、已退卡数量、储扣数量、剩余数量
  20 + - 剩余价值:单价 * 剩余数量(前端可计算汇总)
  21 +- 权益结构:按来源/品项分类的剩余占比(前端聚合)
  22 +- 未拆明细金额占比(如需):整单实付 - 明细合计(用于提示权益偏大风险)
  23 +
  24 +## 三、消费与开单行为
  25 +- 累计开单金额(totalBillingAmount)
  26 +- 累计消耗金额(totalConsumeAmount)
  27 +- 退卡总金额(totalRefundAmount,如需额外查询)
  28 +- 最近一次开单日期、最近一次消耗日期(可由开单/耗卡记录推导)
  29 +- 开单/消耗频次(如需:最近30/60/90天内次数)
  30 +- 订单类型:首单/升单(已有获取客户订单类型接口)
  31 +
  32 +## 四、活跃度与到店
  33 +- 睡眠分层:≤3天活跃,4-59天常到店,60-89/90-179/180-359/360+天沉睡
  34 +- 到店频次:总到店次数、最近 N 天到店次数(可用消费或预约记录统计)
  35 +- 平均到店间隔(基于首次/最后到店与到店次数估算)
  36 +
  37 +## 五、品项偏好
  38 +- 购买 Top N(按累计购买金额/次数)
  39 +- 剩余 Top N(按剩余价值/剩余次数)
  40 +- 来源类型分布(购买/赠送/活动等)
  41 +
  42 +## 六、门店与人员关联
  43 +- 所属门店的业绩对比:门店消费/开单业绩(可用 store-consume-performance / store-statistics)
  44 +- 服务人员关联:主/副健康师的业绩对比(可用 tech-performance / department-consume-performance 做参考)
  45 +- 拓客/推荐链路:拓客人、推荐人、渠道
  46 +
  47 +## 七、成长与分层
  48 +- 消费等级(0-5)与更新时间
  49 +- 累计消费区间(可映射标签:高净值/中等/低)
  50 +- 成长轨迹:近12个月消费趋势(可用业务统计接口按月聚合)
  51 +
  52 +## 八、风险与预警
  53 +- 高剩余权益 + 长期未到店(remainingRightsAmount 高 & lastVisitTime 久远)
  54 +- 睡眠预警:sleepDays > 阈值
  55 +- 明细缺失预警:实付总额远大于品项权益合计
  56 +- 权益即将过期(如有到期规则,可扩展)
  57 +
  58 +## 九、可直接复用的接口
  59 +- 会员基础/累计字段:`/api/Extend/LqKhxx/get-list`、`/api/Extend/LqKhxx/GetInfo/{id}`
  60 +- 权益明细:`/api/Extend/lqkhxx/GetMemberRemainingItems`
  61 +- 门店/部门/技术业绩(对比参考):
  62 + - `/api/Extend/LqStatistics/get-store-consume-performance-statistics-list`
  63 + - `/api/Extend/LqStatistics/get-department-consume-performance-statistics-list`
  64 + - `/api/Extend/LqStatistics/get-tech-performance-statistics-list`
  65 +- 门店邀约/预约/开单/消耗:`/api/Extend/lqstatistics/get-store-statistics-list`
  66 +- 线索/拓客:`/api/Extend/lqstatistics/get-lead-customer-statistics-list`
  67 +- 全局仪表盘(会员活跃、类型分布等参考):`/api/Extend/LqReport/get-dashboard-data`
  68 +
  69 +## 十、前端呈现建议
  70 +- 页头:基础档案 + 核心指标(剩余权益、消费等级、睡眠天数、最近到店)
  71 +- 权益资产:剩余权益总额 + 明细表 + 剩余结构饼图
  72 +- 行为概览:近 30/60/90 天开单/消耗次数与金额,最近一次开单/消耗时间
  73 +- 活跃/到店:睡眠分层、到店频次、平均间隔
  74 +- 品项偏好:购买/剩余 Top N,来源类型分布
  75 +- 关联与对比:门店均值对比,服务人员业绩对比
  76 +- 风险提示:高余额未到店、明细缺失、沉睡预警
  77 +
... ...