Commit ac14c8dbe991d749b70a5a05dff87bb1dd975c90
1 parent
dc67d3d0
chore: update dashboard modal and docs
Showing
11 changed files
with
1798 additions
and
6 deletions
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
sql/排查生美业绩统计差异详细.sql
sql/检查生美业绩统计差异.sql
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
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 | + | ... | ... |