Commit d2682494cf56ba157cf4587657f777f03b4ffb3e
1 parent
7f0666d6
最新
Showing
7 changed files
with
3263 additions
and
1 deletions
package-lock.json
0 → 100644
| 1 | +{ | |
| 2 | + "name": "lvqianmeiye_ERP", | |
| 3 | + "lockfileVersion": 2, | |
| 4 | + "requires": true, | |
| 5 | + "packages": { | |
| 6 | + "": { | |
| 7 | + "dependencies": { | |
| 8 | + "@qiun/ucharts": "^2.5.0-20230101" | |
| 9 | + } | |
| 10 | + }, | |
| 11 | + "node_modules/@qiun/ucharts": { | |
| 12 | + "version": "2.5.0-20230101", | |
| 13 | + "resolved": "https://registry.npmmirror.com/@qiun/ucharts/-/ucharts-2.5.0-20230101.tgz", | |
| 14 | + "integrity": "sha512-C7ccBgfPuGF6dxTRuMW0NPPMSCf1k/kh3I9zkRVBc5PaivudX/rPL+jd2Wty6gn5ya5L3Ob+YmYe09V5xw66Cw==" | |
| 15 | + } | |
| 16 | + }, | |
| 17 | + "dependencies": { | |
| 18 | + "@qiun/ucharts": { | |
| 19 | + "version": "2.5.0-20230101", | |
| 20 | + "resolved": "https://registry.npmmirror.com/@qiun/ucharts/-/ucharts-2.5.0-20230101.tgz", | |
| 21 | + "integrity": "sha512-C7ccBgfPuGF6dxTRuMW0NPPMSCf1k/kh3I9zkRVBc5PaivudX/rPL+jd2Wty6gn5ya5L3Ob+YmYe09V5xw66Cw==" | |
| 22 | + } | |
| 23 | + } | |
| 24 | +} | ... | ... |
package.json
0 → 100644
绿纤uni-app/apis/modules/store-dashboard.js
0 → 100644
| 1 | +import request from '@/service/request.js' | |
| 2 | +import config from '@/common/config.js' | |
| 3 | + | |
| 4 | +export default { | |
| 5 | + // 获取门店驾驶舱统计数据 | |
| 6 | + getStoreDashboardStatistics(data) { | |
| 7 | + return request.post(`${config.getApiBaseUrl()}/api/Extend/LqStoreDashboard/GetStatistics`, data) | |
| 8 | + }, | |
| 9 | + | |
| 10 | + // 获取门店近12个月业绩趋势 | |
| 11 | + getStoreMonthlyTrend(data) { | |
| 12 | + return request.post(`${config.getApiBaseUrl()}/api/Extend/LqReport/get-store-monthly-trend`, data) | |
| 13 | + }, | |
| 14 | + | |
| 15 | + // 获取门店品项分析(包含品项分类占比) | |
| 16 | + getStoreItemAnalysis(data) { | |
| 17 | + return request.post(`${config.getApiBaseUrl()}/api/Extend/LqReport/get-store-item-analysis`, data) | |
| 18 | + }, | |
| 19 | + | |
| 20 | + // 获取门店健康师业绩排行(Top 10) | |
| 21 | + getStoreHealthCoachAnalysis(data) { | |
| 22 | + return request.post(`${config.getApiBaseUrl()}/api/Extend/LqReport/get-store-health-coach-analysis`, data) | |
| 23 | + }, | |
| 24 | + | |
| 25 | + // 获取门店会员分析 | |
| 26 | + getStoreMemberAnalysis(data) { | |
| 27 | + return request.post(`${config.getApiBaseUrl()}/api/Extend/LqReport/get-store-member-analysis`, data) | |
| 28 | + }, | |
| 29 | + | |
| 30 | + // 获取门店各分类月度业绩 | |
| 31 | + getCategoryMonthlyPerformance(data) { | |
| 32 | + return request.post(`${config.getApiBaseUrl()}/api/Extend/LqStoreDashboard/GetCategoryMonthlyPerformance`, data) | |
| 33 | + }, | |
| 34 | + | |
| 35 | + // 获取门店会员转化漏斗数据 | |
| 36 | + getMemberConversionFunnel(data) { | |
| 37 | + return request.post(`${config.getApiBaseUrl()}/api/Extend/LqStoreDashboard/GetMemberConversionFunnel`, data) | |
| 38 | + }, | |
| 39 | + | |
| 40 | + // 获取门店客单价与项目数关系数据 | |
| 41 | + getCustomerPriceProjectRelation(data) { | |
| 42 | + return request.post(`${config.getApiBaseUrl()}/api/Extend/LqStoreDashboard/GetCustomerPriceProjectRelation`, data) | |
| 43 | + }, | |
| 44 | + | |
| 45 | + // 获取门店排名对比数据 | |
| 46 | + getStoreComparisonAnalysis(data) { | |
| 47 | + return request.post(`${config.getApiBaseUrl()}/api/Extend/LqReport/get-store-comparison-analysis`, data) | |
| 48 | + }, | |
| 49 | + | |
| 50 | + // 获取门店一周运营热力图数据 | |
| 51 | + getWeeklyHeatmap(data) { | |
| 52 | + return request.post(`${config.getApiBaseUrl()}/api/Extend/LqStoreDashboard/GetWeeklyHeatmap`, data) | |
| 53 | + } | |
| 54 | +} | |
| 55 | + | |
| 56 | + | ... | ... |
绿纤uni-app/package-lock.json
| 1 | 1 | { |
| 2 | - "name": "绿纤", | |
| 2 | + "name": "绿纤uni-app", | |
| 3 | 3 | "lockfileVersion": 2, |
| 4 | 4 | "requires": true, |
| 5 | 5 | "packages": { |
| 6 | 6 | "": { |
| 7 | 7 | "dependencies": { |
| 8 | + "@qiun/ucharts": "^2.5.0-20230101", | |
| 8 | 9 | "html2canvas": "^1.4.1" |
| 9 | 10 | } |
| 10 | 11 | }, |
| 12 | + "node_modules/@qiun/ucharts": { | |
| 13 | + "version": "2.5.0-20230101", | |
| 14 | + "resolved": "https://registry.npmmirror.com/@qiun/ucharts/-/ucharts-2.5.0-20230101.tgz", | |
| 15 | + "integrity": "sha512-C7ccBgfPuGF6dxTRuMW0NPPMSCf1k/kh3I9zkRVBc5PaivudX/rPL+jd2Wty6gn5ya5L3Ob+YmYe09V5xw66Cw==" | |
| 16 | + }, | |
| 11 | 17 | "node_modules/base64-arraybuffer": { |
| 12 | 18 | "version": "1.0.2", |
| 13 | 19 | "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", |
| ... | ... | @@ -54,6 +60,11 @@ |
| 54 | 60 | } |
| 55 | 61 | }, |
| 56 | 62 | "dependencies": { |
| 63 | + "@qiun/ucharts": { | |
| 64 | + "version": "2.5.0-20230101", | |
| 65 | + "resolved": "https://registry.npmmirror.com/@qiun/ucharts/-/ucharts-2.5.0-20230101.tgz", | |
| 66 | + "integrity": "sha512-C7ccBgfPuGF6dxTRuMW0NPPMSCf1k/kh3I9zkRVBc5PaivudX/rPL+jd2Wty6gn5ya5L3Ob+YmYe09V5xw66Cw==" | |
| 67 | + }, | |
| 57 | 68 | "base64-arraybuffer": { |
| 58 | 69 | "version": "1.0.2", |
| 59 | 70 | "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", | ... | ... |
绿纤uni-app/package.json
绿纤uni-app/pages.json
绿纤uni-app/pages/store-dashboard/store-dashboard.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <view class="store-dashboard"> | |
| 3 | + <!-- 筛选器 --> | |
| 4 | + <view class="filter-bar"> | |
| 5 | + <view class="filter-item"> | |
| 6 | + <text class="filter-label">选择月份</text> | |
| 7 | + <picker mode="date" fields="month" :value="queryParams.month" @change="onMonthChange"> | |
| 8 | + <view class="picker-view"> | |
| 9 | + <text>{{ queryParams.month || '请选择月份' }}</text> | |
| 10 | + <u-icon name="arrow-down" size="14" color="#909399"></u-icon> | |
| 11 | + </view> | |
| 12 | + </picker> | |
| 13 | + </view> | |
| 14 | + <view class="filter-item"> | |
| 15 | + <text class="filter-label">选择门店</text> | |
| 16 | + <picker mode="selector" :range="storeOptions" range-key="dm" :value="storeIndex" @change="onStoreChange"> | |
| 17 | + <view class="picker-view"> | |
| 18 | + <text>{{ currentStoreName || '请选择门店' }}</text> | |
| 19 | + <u-icon name="arrow-down" size="14" color="#909399"></u-icon> | |
| 20 | + </view> | |
| 21 | + </picker> | |
| 22 | + </view> | |
| 23 | + <view class="filter-actions"> | |
| 24 | + <u-button type="primary" size="mini" @click="handleQuery">查询</u-button> | |
| 25 | + <u-button type="default" size="mini" @click="handleReset">重置</u-button> | |
| 26 | + </view> | |
| 27 | + </view> | |
| 28 | + | |
| 29 | + <!-- 加载状态 --> | |
| 30 | + <view v-if="loading" class="loading-wrapper"> | |
| 31 | + <u-loading-icon mode="spinner" size="40"></u-loading-icon> | |
| 32 | + <text class="loading-text">加载中...</text> | |
| 33 | + </view> | |
| 34 | + | |
| 35 | + <!-- 内容区域 --> | |
| 36 | + <scroll-view v-else scroll-y class="content-scroll" @scrolltolower="onScrollToLower"> | |
| 37 | + <!-- 门店信息卡片 --> | |
| 38 | + <view class="store-info-card" v-if="currentStoreName"> | |
| 39 | + <view class="store-header"> | |
| 40 | + <view class="store-avatar"> | |
| 41 | + <u-icon name="home" size="32" color="#fff"></u-icon> | |
| 42 | + </view> | |
| 43 | + <view class="store-details"> | |
| 44 | + <view class="store-name">{{ currentStoreName }}</view> | |
| 45 | + <view class="store-meta"> | |
| 46 | + <text class="meta-item">{{ currentStoreCode || '-' }}</text> | |
| 47 | + <text class="meta-item">{{ queryParams.month || '当前月份' }}</text> | |
| 48 | + </view> | |
| 49 | + </view> | |
| 50 | + <u-tag text="正常营业" type="success" size="mini"></u-tag> | |
| 51 | + </view> | |
| 52 | + </view> | |
| 53 | + | |
| 54 | + <!-- 核心指标卡片 --> | |
| 55 | + <view class="core-stats-card" v-if="storeData"> | |
| 56 | + <view class="card-title"> | |
| 57 | + <u-icon name="data-line" size="18" color="#409EFF"></u-icon> | |
| 58 | + <text>核心指标</text> | |
| 59 | + </view> | |
| 60 | + <view class="stats-grid"> | |
| 61 | + <view class="stat-item primary"> | |
| 62 | + <view class="stat-label">开单业绩</view> | |
| 63 | + <view class="stat-value">¥{{ formatMoney(storeData.BillingPerformance) }}</view> | |
| 64 | + </view> | |
| 65 | + <view class="stat-item success"> | |
| 66 | + <view class="stat-label">消耗业绩</view> | |
| 67 | + <view class="stat-value">¥{{ formatMoney(storeData.ConsumePerformance) }}</view> | |
| 68 | + </view> | |
| 69 | + <view class="stat-item info"> | |
| 70 | + <view class="stat-label">完成率</view> | |
| 71 | + <view class="stat-value">{{ formatMoney(storeData.CompletionRate, 2) }}%</view> | |
| 72 | + </view> | |
| 73 | + <view class="stat-item warning"> | |
| 74 | + <view class="stat-label">净业绩</view> | |
| 75 | + <view class="stat-value">¥{{ formatMoney(storeData.NetPerformance) }}</view> | |
| 76 | + </view> | |
| 77 | + </view> | |
| 78 | + </view> | |
| 79 | + | |
| 80 | + <!-- 业绩概览 --> | |
| 81 | + <view class="section-card" v-if="storeData"> | |
| 82 | + <view class="card-title"> | |
| 83 | + <u-icon name="list" size="18" color="#409EFF"></u-icon> | |
| 84 | + <text>业绩概览</text> | |
| 85 | + </view> | |
| 86 | + <view class="metrics-list"> | |
| 87 | + <view class="metric-row"> | |
| 88 | + <view class="metric-item"> | |
| 89 | + <text class="metric-label">开单次数</text> | |
| 90 | + <text class="metric-value">{{ storeData.BillingCount || 0 }}</text> | |
| 91 | + </view> | |
| 92 | + <view class="metric-item"> | |
| 93 | + <text class="metric-label">消耗次数</text> | |
| 94 | + <text class="metric-value">{{ storeData.ConsumeCount || 0 }}</text> | |
| 95 | + </view> | |
| 96 | + </view> | |
| 97 | + <view class="metric-row"> | |
| 98 | + <view class="metric-item"> | |
| 99 | + <text class="metric-label">退卡次数</text> | |
| 100 | + <text class="metric-value">{{ storeData.RefundCount || 0 }}</text> | |
| 101 | + </view> | |
| 102 | + <view class="metric-item"> | |
| 103 | + <text class="metric-label">平均开单金额</text> | |
| 104 | + <text class="metric-value">¥{{ formatMoney(storeData.AvgBillingAmount) }}</text> | |
| 105 | + </view> | |
| 106 | + </view> | |
| 107 | + <view class="metric-row"> | |
| 108 | + <view class="metric-item"> | |
| 109 | + <text class="metric-label">平均消耗金额</text> | |
| 110 | + <text class="metric-value">¥{{ formatMoney(storeData.AvgConsumeAmount) }}</text> | |
| 111 | + </view> | |
| 112 | + <view class="metric-item"> | |
| 113 | + <text class="metric-label">剩余权益</text> | |
| 114 | + <text class="metric-value">¥{{ formatMoney(storeData.RemainingRightsAmount) }}</text> | |
| 115 | + </view> | |
| 116 | + </view> | |
| 117 | + <view class="metric-row"> | |
| 118 | + <view class="metric-item"> | |
| 119 | + <text class="metric-label">目标业绩</text> | |
| 120 | + <text class="metric-value">¥{{ formatMoney(storeData.TargetPerformance) }}</text> | |
| 121 | + </view> | |
| 122 | + <view class="metric-item"> | |
| 123 | + <text class="metric-label">退卡金额</text> | |
| 124 | + <text class="metric-value">¥{{ formatMoney(storeData.RefundAmount) }}</text> | |
| 125 | + </view> | |
| 126 | + </view> | |
| 127 | + </view> | |
| 128 | + </view> | |
| 129 | + | |
| 130 | + <!-- 运营指标 --> | |
| 131 | + <view class="section-card" v-if="storeData"> | |
| 132 | + <view class="card-title"> | |
| 133 | + <u-icon name="grid" size="18" color="#409EFF"></u-icon> | |
| 134 | + <text>运营指标</text> | |
| 135 | + </view> | |
| 136 | + <view class="metrics-list"> | |
| 137 | + <view class="metric-row"> | |
| 138 | + <view class="metric-item"> | |
| 139 | + <text class="metric-label">人头数</text> | |
| 140 | + <text class="metric-value">{{ storeData.HeadCount || 0 }}</text> | |
| 141 | + </view> | |
| 142 | + <view class="metric-item"> | |
| 143 | + <text class="metric-label">人次</text> | |
| 144 | + <text class="metric-value">{{ storeData.PersonCount || 0 }}</text> | |
| 145 | + </view> | |
| 146 | + </view> | |
| 147 | + <view class="metric-row"> | |
| 148 | + <view class="metric-item"> | |
| 149 | + <text class="metric-label">项目数</text> | |
| 150 | + <text class="metric-value">{{ storeData.ProjectCount || 0 }}</text> | |
| 151 | + </view> | |
| 152 | + <view class="metric-item"> | |
| 153 | + <text class="metric-label">客单价</text> | |
| 154 | + <text class="metric-value">¥{{ formatMoney(storeData.AvgAmountPerPerson) }}</text> | |
| 155 | + </view> | |
| 156 | + </view> | |
| 157 | + <view class="metric-row"> | |
| 158 | + <view class="metric-item"> | |
| 159 | + <text class="metric-label">项目单价</text> | |
| 160 | + <text class="metric-value">¥{{ formatMoney(storeData.AvgAmountPerProject) }}</text> | |
| 161 | + </view> | |
| 162 | + <view class="metric-item"> | |
| 163 | + <text class="metric-label">人均项目数</text> | |
| 164 | + <text class="metric-value">{{ formatMoney(storeData.AvgProjectPerHead, 2) }}</text> | |
| 165 | + </view> | |
| 166 | + </view> | |
| 167 | + </view> | |
| 168 | + </view> | |
| 169 | + | |
| 170 | + <!-- 会员分析 --> | |
| 171 | + <view class="section-card" v-if="memberData"> | |
| 172 | + <view class="card-title"> | |
| 173 | + <u-icon name="account" size="18" color="#409EFF"></u-icon> | |
| 174 | + <text>会员分析</text> | |
| 175 | + </view> | |
| 176 | + <view class="metrics-list"> | |
| 177 | + <view class="metric-row"> | |
| 178 | + <view class="metric-item"> | |
| 179 | + <text class="metric-label">总会员数</text> | |
| 180 | + <text class="metric-value">{{ formatNumber(memberData.TotalMembers || 0) }}</text> | |
| 181 | + </view> | |
| 182 | + <view class="metric-item"> | |
| 183 | + <text class="metric-label">本月新增</text> | |
| 184 | + <text class="metric-value">{{ formatNumber(memberData.NewMembersThisMonth || 0) }}</text> | |
| 185 | + </view> | |
| 186 | + </view> | |
| 187 | + <view class="metric-row"> | |
| 188 | + <view class="metric-item"> | |
| 189 | + <text class="metric-label">活跃会员</text> | |
| 190 | + <text class="metric-value">{{ formatNumber(memberData.ActiveMembers || 0) }}</text> | |
| 191 | + <text class="metric-rate">{{ formatMoney(memberData.ActiveMemberRate || 0, 1) }}%</text> | |
| 192 | + </view> | |
| 193 | + <view class="metric-item"> | |
| 194 | + <text class="metric-label">沉睡会员</text> | |
| 195 | + <text class="metric-value">{{ formatNumber(memberData.SleepMembers || 0) }}</text> | |
| 196 | + <text class="metric-rate">{{ formatMoney(memberData.SleepMemberRate || 0, 1) }}%</text> | |
| 197 | + </view> | |
| 198 | + </view> | |
| 199 | + <view class="metric-row"> | |
| 200 | + <view class="metric-item"> | |
| 201 | + <text class="metric-label">生美会员</text> | |
| 202 | + <text class="metric-value">{{ formatNumber(memberData.BeautyMembers || 0) }}</text> | |
| 203 | + </view> | |
| 204 | + <view class="metric-item"> | |
| 205 | + <text class="metric-label">医美会员</text> | |
| 206 | + <text class="metric-value">{{ formatNumber(memberData.MedicalMembers || 0) }}</text> | |
| 207 | + </view> | |
| 208 | + </view> | |
| 209 | + <view class="metric-row"> | |
| 210 | + <view class="metric-item"> | |
| 211 | + <text class="metric-label">科美会员</text> | |
| 212 | + <text class="metric-value">{{ formatNumber(memberData.TechMembers || 0) }}</text> | |
| 213 | + </view> | |
| 214 | + <view class="metric-item"> | |
| 215 | + <text class="metric-label">教育会员</text> | |
| 216 | + <text class="metric-value">{{ formatNumber(memberData.EducationMembers || 0) }}</text> | |
| 217 | + </view> | |
| 218 | + </view> | |
| 219 | + </view> | |
| 220 | + </view> | |
| 221 | + | |
| 222 | + <!-- 业绩趋势折线图 --> | |
| 223 | + <view class="section-card" v-if="trendData && trendData.length > 0"> | |
| 224 | + <view class="card-title"> | |
| 225 | + <u-icon name="arrow-up" size="18" color="#409EFF"></u-icon> | |
| 226 | + <text>近12个月业绩趋势</text> | |
| 227 | + </view> | |
| 228 | + <view class="chart-container"> | |
| 229 | + <canvas canvas-id="trendChart" id="trendChart" class="chart-canvas"></canvas> | |
| 230 | + </view> | |
| 231 | + </view> | |
| 232 | + | |
| 233 | + <!-- 业绩对比柱状图 --> | |
| 234 | + <view class="section-card" v-if="trendData && trendData.length > 0"> | |
| 235 | + <view class="card-title"> | |
| 236 | + <u-icon name="bar-chart" size="18" color="#409EFF"></u-icon> | |
| 237 | + <text>业绩对比分析</text> | |
| 238 | + </view> | |
| 239 | + <view class="chart-container"> | |
| 240 | + <canvas canvas-id="compareChart" id="compareChart" class="chart-canvas"></canvas> | |
| 241 | + </view> | |
| 242 | + </view> | |
| 243 | + | |
| 244 | + <!-- 健康师业绩排行 --> | |
| 245 | + <view class="section-card" v-if="healthCoachRanking && healthCoachRanking.length > 0"> | |
| 246 | + <view class="card-title"> | |
| 247 | + <u-icon name="star" size="18" color="#409EFF"></u-icon> | |
| 248 | + <text>健康师业绩排行(Top 10)</text> | |
| 249 | + </view> | |
| 250 | + <view class="ranking-list"> | |
| 251 | + <view class="ranking-item" v-for="(item, index) in healthCoachRanking" :key="index"> | |
| 252 | + <view class="ranking-index" :class="getRankingClass(index)"> | |
| 253 | + {{ index + 1 }} | |
| 254 | + </view> | |
| 255 | + <view class="ranking-info"> | |
| 256 | + <view class="ranking-name">{{ item.HealthCoachName || '未知' }}</view> | |
| 257 | + <view class="ranking-details"> | |
| 258 | + <text class="ranking-detail-item">开单:¥{{ formatMoney(item.BillingPerformance) }}</text> | |
| 259 | + <text class="ranking-detail-item">消耗:¥{{ formatMoney(item.ConsumePerformance) }}</text> | |
| 260 | + <text class="ranking-detail-item">净业绩:¥{{ formatMoney(item.NetPerformance) }}</text> | |
| 261 | + </view> | |
| 262 | + </view> | |
| 263 | + </view> | |
| 264 | + </view> | |
| 265 | + </view> | |
| 266 | + | |
| 267 | + <!-- 品项开单排行 --> | |
| 268 | + <view class="section-card" v-if="topBillingItems && topBillingItems.length > 0"> | |
| 269 | + <view class="card-title"> | |
| 270 | + <u-icon name="shopping-cart" size="18" color="#409EFF"></u-icon> | |
| 271 | + <text>品项开单排行(Top 10)</text> | |
| 272 | + </view> | |
| 273 | + <view class="item-list"> | |
| 274 | + <view class="item-row" v-for="(item, index) in topBillingItems" :key="index"> | |
| 275 | + <view class="item-index" :class="getRankingClass(index)"> | |
| 276 | + {{ index + 1 }} | |
| 277 | + </view> | |
| 278 | + <view class="item-info"> | |
| 279 | + <view class="item-name">{{ item.ItemName || '未知品项' }}</view> | |
| 280 | + <view class="item-details"> | |
| 281 | + <u-tag :text="item.Category || '其他'" :type="getCategoryType(item.Category)" size="mini"></u-tag> | |
| 282 | + <text class="item-amount">¥{{ formatMoney(item.BillingAmount) }}</text> | |
| 283 | + <text class="item-count">{{ item.BillingCount }}次</text> | |
| 284 | + </view> | |
| 285 | + </view> | |
| 286 | + </view> | |
| 287 | + </view> | |
| 288 | + </view> | |
| 289 | + | |
| 290 | + <!-- 门店排名对比 --> | |
| 291 | + <view class="section-card" v-if="comparison && comparison.totalStoreCount > 0"> | |
| 292 | + <view class="card-title"> | |
| 293 | + <u-icon name="trophy" size="18" color="#409EFF"></u-icon> | |
| 294 | + <text>门店排名对比</text> | |
| 295 | + </view> | |
| 296 | + <view class="comparison-content"> | |
| 297 | + <view class="comparison-rank"> | |
| 298 | + <text class="rank-label">业绩排名</text> | |
| 299 | + <view class="rank-value"> | |
| 300 | + <text class="rank-number">{{ comparison.performanceRanking }}</text> | |
| 301 | + <text class="rank-total">/ {{ comparison.totalStoreCount }}</text> | |
| 302 | + </view> | |
| 303 | + <u-tag :text="getRankingText(comparison.performanceRanking, comparison.totalStoreCount)" | |
| 304 | + :type="getRankingTagType(comparison.performanceRanking, comparison.totalStoreCount)" | |
| 305 | + size="mini"></u-tag> | |
| 306 | + </view> | |
| 307 | + <u-line></u-line> | |
| 308 | + <view class="comparison-stats"> | |
| 309 | + <view class="comparison-stat-row"> | |
| 310 | + <text class="stat-label">同类型门店平均业绩</text> | |
| 311 | + <text class="stat-value">¥{{ formatMoney(comparison.avgPerformanceSameType) }}</text> | |
| 312 | + </view> | |
| 313 | + <view class="comparison-stat-row"> | |
| 314 | + <text class="stat-label">同类型门店数</text> | |
| 315 | + <text class="stat-value">{{ comparison.sameTypeStoreCount }}家</text> | |
| 316 | + </view> | |
| 317 | + <view class="comparison-stat-row"> | |
| 318 | + <text class="stat-label">同组织门店平均业绩</text> | |
| 319 | + <text class="stat-value">¥{{ formatMoney(comparison.avgPerformanceSameOrg) }}</text> | |
| 320 | + </view> | |
| 321 | + <view class="comparison-stat-row"> | |
| 322 | + <text class="stat-label">同组织门店数</text> | |
| 323 | + <text class="stat-value">{{ comparison.sameOrgStoreCount }}家</text> | |
| 324 | + </view> | |
| 325 | + </view> | |
| 326 | + </view> | |
| 327 | + </view> | |
| 328 | + | |
| 329 | + <!-- 消耗品项排行 --> | |
| 330 | + <view class="section-card" v-if="topConsumeItems && topConsumeItems.length > 0"> | |
| 331 | + <view class="card-title"> | |
| 332 | + <u-icon name="goods" size="18" color="#409EFF"></u-icon> | |
| 333 | + <text>消耗品项排行(Top 10)</text> | |
| 334 | + </view> | |
| 335 | + <view class="item-list"> | |
| 336 | + <view class="item-row" v-for="(item, index) in topConsumeItems" :key="index"> | |
| 337 | + <view class="item-index" :class="getRankingClass(index)"> | |
| 338 | + {{ index + 1 }} | |
| 339 | + </view> | |
| 340 | + <view class="item-info"> | |
| 341 | + <view class="item-name">{{ item.ItemName || '未知品项' }}</view> | |
| 342 | + <view class="item-details"> | |
| 343 | + <u-tag :text="item.Category || '其他'" :type="getCategoryType(item.Category)" size="mini"></u-tag> | |
| 344 | + <text class="item-amount">¥{{ formatMoney(item.ConsumeAmount) }}</text> | |
| 345 | + </view> | |
| 346 | + </view> | |
| 347 | + </view> | |
| 348 | + </view> | |
| 349 | + </view> | |
| 350 | + | |
| 351 | + <!-- 品项分类占比饼图 --> | |
| 352 | + <view class="section-card" v-if="categoryData && categoryData.length > 0"> | |
| 353 | + <view class="card-title"> | |
| 354 | + <u-icon name="pie-chart" size="18" color="#409EFF"></u-icon> | |
| 355 | + <text>品项分类占比</text> | |
| 356 | + </view> | |
| 357 | + <view class="chart-container"> | |
| 358 | + <canvas canvas-id="categoryChart" id="categoryChart" class="chart-canvas"></canvas> | |
| 359 | + </view> | |
| 360 | + <!-- 图例说明 --> | |
| 361 | + <view class="category-legend"> | |
| 362 | + <view class="legend-item" v-for="(item, index) in categoryData" :key="index"> | |
| 363 | + <view class="legend-color" :style="{ background: getCategoryColor(item.CategoryName) }"></view> | |
| 364 | + <text class="legend-label">{{ item.CategoryName || '其他' }}</text> | |
| 365 | + <text class="legend-value">¥{{ formatMoney(item.ConsumeAmount) }}</text> | |
| 366 | + </view> | |
| 367 | + </view> | |
| 368 | + </view> | |
| 369 | + | |
| 370 | + <!-- 拓客转化漏斗图 --> | |
| 371 | + <view class="section-card" v-if="funnelData"> | |
| 372 | + <view class="card-title"> | |
| 373 | + <u-icon name="sort" size="18" color="#409EFF"></u-icon> | |
| 374 | + <text>拓客转化漏斗</text> | |
| 375 | + </view> | |
| 376 | + <view class="chart-container"> | |
| 377 | + <canvas canvas-id="funnelChart" id="funnelChart" class="chart-canvas"></canvas> | |
| 378 | + </view> | |
| 379 | + </view> | |
| 380 | + | |
| 381 | + <!-- 客单价与项目数关系散点图 --> | |
| 382 | + <view class="section-card" v-if="scatterData && scatterData.length > 0"> | |
| 383 | + <view class="card-title"> | |
| 384 | + <u-icon name="grid" size="18" color="#409EFF"></u-icon> | |
| 385 | + <text>客单价与项目数关系分析</text> | |
| 386 | + </view> | |
| 387 | + <view class="chart-container"> | |
| 388 | + <canvas canvas-id="scatterChart" id="scatterChart" class="chart-canvas"></canvas> | |
| 389 | + </view> | |
| 390 | + </view> | |
| 391 | + | |
| 392 | + <!-- 各分类业绩堆叠对比柱状图 --> | |
| 393 | + <view class="section-card" v-if="categoryMonthlyData && categoryMonthlyData.length > 0"> | |
| 394 | + <view class="card-title"> | |
| 395 | + <u-icon name="data-line" size="18" color="#409EFF"></u-icon> | |
| 396 | + <text>各分类业绩堆叠对比(最近6个月)</text> | |
| 397 | + </view> | |
| 398 | + <view class="chart-container"> | |
| 399 | + <canvas canvas-id="stackedChart" id="stackedChart" class="chart-canvas"></canvas> | |
| 400 | + </view> | |
| 401 | + </view> | |
| 402 | + | |
| 403 | + <!-- 一周运营热力图 --> | |
| 404 | + <view class="section-card" v-if="heatmapData && heatmapData.length > 0"> | |
| 405 | + <view class="card-title"> | |
| 406 | + <u-icon name="grid" size="18" color="#409EFF"></u-icon> | |
| 407 | + <text>一周运营热力图</text> | |
| 408 | + </view> | |
| 409 | + <view class="chart-container chart-container-large"> | |
| 410 | + <canvas canvas-id="heatmapChart" id="heatmapChart" class="chart-canvas"></canvas> | |
| 411 | + </view> | |
| 412 | + </view> | |
| 413 | + | |
| 414 | + <!-- 目标完成度仪表盘 --> | |
| 415 | + <view class="section-card" v-if="storeData"> | |
| 416 | + <view class="card-title"> | |
| 417 | + <u-icon name="flag" size="18" color="#409EFF"></u-icon> | |
| 418 | + <text>目标完成度</text> | |
| 419 | + </view> | |
| 420 | + <view class="chart-container chart-container-gauge"> | |
| 421 | + <canvas canvas-id="gaugeChart" id="gaugeChart" class="chart-canvas"></canvas> | |
| 422 | + </view> | |
| 423 | + <view class="gauge-info"> | |
| 424 | + <view class="gauge-info-item"> | |
| 425 | + <text class="info-label">目标业绩</text> | |
| 426 | + <text class="info-value">¥{{ formatMoney(storeData.TargetPerformance) }}</text> | |
| 427 | + </view> | |
| 428 | + <view class="gauge-info-item"> | |
| 429 | + <text class="info-label">消耗业绩</text> | |
| 430 | + <text class="info-value">¥{{ formatMoney(storeData.ConsumePerformance) }}</text> | |
| 431 | + </view> | |
| 432 | + </view> | |
| 433 | + </view> | |
| 434 | + | |
| 435 | + <!-- 本月经营提示 --> | |
| 436 | + <view class="section-card" v-if="operationTips && operationTips.length > 0"> | |
| 437 | + <view class="card-title"> | |
| 438 | + <u-icon name="warning" size="18" color="#409EFF"></u-icon> | |
| 439 | + <text>本月经营提示</text> | |
| 440 | + </view> | |
| 441 | + <view class="tips-list"> | |
| 442 | + <view class="tip-item" v-for="(tip, index) in operationTips" :key="index" :class="tip.type"> | |
| 443 | + <u-icon :name="getTipIcon(tip.type)" size="16" :color="getTipColor(tip.type)"></u-icon> | |
| 444 | + <text class="tip-text">{{ tip.text }}</text> | |
| 445 | + </view> | |
| 446 | + </view> | |
| 447 | + </view> | |
| 448 | + | |
| 449 | + <!-- 快速数据洞察 --> | |
| 450 | + <view class="section-card" v-if="dataInsights && dataInsights.length > 0"> | |
| 451 | + <view class="card-title"> | |
| 452 | + <u-icon name="data-analysis" size="18" color="#409EFF"></u-icon> | |
| 453 | + <text>快速数据洞察</text> | |
| 454 | + </view> | |
| 455 | + <view class="insight-list"> | |
| 456 | + <view class="insight-item" v-for="(insight, index) in dataInsights" :key="index"> | |
| 457 | + <view class="insight-header"> | |
| 458 | + <text class="insight-title">{{ insight.title }}</text> | |
| 459 | + <u-tag :text="insight.tag" :type="insight.tagType" size="mini"></u-tag> | |
| 460 | + </view> | |
| 461 | + <text class="insight-value">{{ insight.value }}</text> | |
| 462 | + <text class="insight-desc">{{ insight.desc }}</text> | |
| 463 | + </view> | |
| 464 | + </view> | |
| 465 | + </view> | |
| 466 | + | |
| 467 | + <!-- 本月关键指标 --> | |
| 468 | + <view class="section-card" v-if="keyMetrics && keyMetrics.length > 0"> | |
| 469 | + <view class="card-title"> | |
| 470 | + <u-icon name="flag" size="18" color="#409EFF"></u-icon> | |
| 471 | + <text>本月关键指标</text> | |
| 472 | + </view> | |
| 473 | + <view class="key-metrics-list"> | |
| 474 | + <view class="key-metric-item" v-for="(metric, index) in keyMetrics" :key="index"> | |
| 475 | + <view class="metric-header-row"> | |
| 476 | + <text class="metric-label">{{ metric.label }}</text> | |
| 477 | + <text class="metric-percent">{{ metric.value }}%</text> | |
| 478 | + </view> | |
| 479 | + <view class="progress-wrapper"> | |
| 480 | + <view class="progress-bar" :style="{ width: metric.value + '%', background: metric.color }"></view> | |
| 481 | + </view> | |
| 482 | + </view> | |
| 483 | + </view> | |
| 484 | + </view> | |
| 485 | + | |
| 486 | + <!-- 底部占位 --> | |
| 487 | + <view class="bottom-placeholder"></view> | |
| 488 | + </scroll-view> | |
| 489 | + </view> | |
| 490 | +</template> | |
| 491 | + | |
| 492 | +<script> | |
| 493 | + import storeApi from '@/apis/modules/store.js' | |
| 494 | + import storeDashboardApi from '@/apis/modules/store-dashboard.js' | |
| 495 | + import uCharts from '@qiun/ucharts' | |
| 496 | + | |
| 497 | + // uCharts 图表工具类 | |
| 498 | + const ChartUtil = { | |
| 499 | + // 绘制折线图 | |
| 500 | + drawLineChart(vueInstance, canvasId, categories, series, options = {}) { | |
| 501 | + if (!categories || categories.length === 0 || !series || series.length === 0) { | |
| 502 | + return | |
| 503 | + } | |
| 504 | + | |
| 505 | + const chartData = { | |
| 506 | + categories: categories, | |
| 507 | + series: series.map((s, index) => ({ | |
| 508 | + name: s.name || `系列${index + 1}`, | |
| 509 | + data: s.data, | |
| 510 | + color: s.color || ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C'][index] | |
| 511 | + })) | |
| 512 | + } | |
| 513 | + | |
| 514 | + const opts = { | |
| 515 | + type: 'line', | |
| 516 | + fontSize: 11, | |
| 517 | + dataLabel: true, | |
| 518 | + dataPointShape: true, | |
| 519 | + legend: { | |
| 520 | + show: true, | |
| 521 | + position: 'top', | |
| 522 | + lineHeight: 25 | |
| 523 | + }, | |
| 524 | + xAxis: { | |
| 525 | + disableGrid: false, | |
| 526 | + itemCount: categories.length > 12 ? 12 : categories.length | |
| 527 | + }, | |
| 528 | + yAxis: { | |
| 529 | + gridType: 'dash', | |
| 530 | + dashLength: 2, | |
| 531 | + format: (val) => { | |
| 532 | + return val >= 10000 ? (val / 10000).toFixed(1) + '万' : val.toString() | |
| 533 | + } | |
| 534 | + }, | |
| 535 | + extra: { | |
| 536 | + line: { | |
| 537 | + type: 'curve', | |
| 538 | + width: 2 | |
| 539 | + }, | |
| 540 | + tooltip: { | |
| 541 | + showBox: true, | |
| 542 | + showArrow: true, | |
| 543 | + showCategory: true | |
| 544 | + } | |
| 545 | + } | |
| 546 | + } | |
| 547 | + | |
| 548 | + const query = uni.createSelectorQuery().in(vueInstance) | |
| 549 | + query.select(`#${canvasId}`).boundingClientRect((rect) => { | |
| 550 | + if (!rect || !rect.width || !rect.height) { | |
| 551 | + console.warn(`图表容器 ${canvasId} 尺寸获取失败`, rect) | |
| 552 | + // 使用默认尺寸 | |
| 553 | + const ctx = uni.createCanvasContext(canvasId, vueInstance) | |
| 554 | + const chart = new uCharts({ | |
| 555 | + type: 'line', | |
| 556 | + context: ctx, | |
| 557 | + width: options.width || 700, | |
| 558 | + height: options.height || 400, | |
| 559 | + categories: chartData.categories, | |
| 560 | + series: chartData.series, | |
| 561 | + animation: true, | |
| 562 | + background: 'none', | |
| 563 | + color: chartData.series.map(s => s.color), | |
| 564 | + padding: [15, 15, 0, 15], | |
| 565 | + enableScroll: false, | |
| 566 | + legend: opts.legend, | |
| 567 | + xAxis: opts.xAxis, | |
| 568 | + yAxis: opts.yAxis, | |
| 569 | + extra: opts.extra | |
| 570 | + }) | |
| 571 | + return | |
| 572 | + } | |
| 573 | + const ctx = uni.createCanvasContext(canvasId, vueInstance) | |
| 574 | + // 获取系统信息,计算实际像素值 | |
| 575 | + const systemInfo = uni.getSystemInfoSync() | |
| 576 | + const pixelRatio = systemInfo.pixelRatio || 1 | |
| 577 | + const chart = new uCharts({ | |
| 578 | + type: 'line', | |
| 579 | + context: ctx, | |
| 580 | + width: Math.floor(uni.upx2px(rect.width) * pixelRatio) || options.width || 700, | |
| 581 | + height: Math.floor(uni.upx2px(rect.height) * pixelRatio) || options.height || 400, | |
| 582 | + categories: chartData.categories, | |
| 583 | + series: chartData.series, | |
| 584 | + animation: true, | |
| 585 | + background: 'none', | |
| 586 | + color: chartData.series.map(s => s.color), | |
| 587 | + padding: [15, 15, 0, 15], | |
| 588 | + enableScroll: false, | |
| 589 | + legend: opts.legend, | |
| 590 | + xAxis: opts.xAxis, | |
| 591 | + yAxis: opts.yAxis, | |
| 592 | + extra: opts.extra | |
| 593 | + }) | |
| 594 | + }).exec() | |
| 595 | + }, | |
| 596 | + | |
| 597 | + // 绘制柱状图 | |
| 598 | + drawColumnChart(vueInstance, canvasId, categories, series, options = {}) { | |
| 599 | + if (!categories || categories.length === 0 || !series || series.length === 0) { | |
| 600 | + return | |
| 601 | + } | |
| 602 | + | |
| 603 | + const chartData = { | |
| 604 | + categories: categories, | |
| 605 | + series: series.map((s, index) => ({ | |
| 606 | + name: s.name || `系列${index + 1}`, | |
| 607 | + data: s.data, | |
| 608 | + color: s.color || ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C'][index] | |
| 609 | + })) | |
| 610 | + } | |
| 611 | + | |
| 612 | + const opts = { | |
| 613 | + type: 'column', | |
| 614 | + fontSize: 11, | |
| 615 | + dataLabel: true, | |
| 616 | + legend: { | |
| 617 | + show: true, | |
| 618 | + position: 'top', | |
| 619 | + lineHeight: 25 | |
| 620 | + }, | |
| 621 | + xAxis: { | |
| 622 | + disableGrid: false, | |
| 623 | + itemCount: categories.length > 12 ? 12 : categories.length | |
| 624 | + }, | |
| 625 | + yAxis: { | |
| 626 | + gridType: 'dash', | |
| 627 | + dashLength: 2, | |
| 628 | + format: (val) => { | |
| 629 | + return val >= 1 ? val.toFixed(1) + '万' : val.toString() | |
| 630 | + } | |
| 631 | + }, | |
| 632 | + extra: { | |
| 633 | + column: { | |
| 634 | + type: 'group', | |
| 635 | + width: 0.6, | |
| 636 | + activeBgColor: '#000000', | |
| 637 | + activeBgOpacity: 0.08 | |
| 638 | + }, | |
| 639 | + tooltip: { | |
| 640 | + showBox: true, | |
| 641 | + showArrow: true, | |
| 642 | + showCategory: true | |
| 643 | + } | |
| 644 | + } | |
| 645 | + } | |
| 646 | + | |
| 647 | + const query = uni.createSelectorQuery().in(vueInstance) | |
| 648 | + query.select(`#${canvasId}`).boundingClientRect((rect) => { | |
| 649 | + if (!rect || !rect.width || !rect.height) { | |
| 650 | + console.warn(`图表容器 ${canvasId} 尺寸获取失败`, rect) | |
| 651 | + // 使用默认尺寸 | |
| 652 | + const ctx = uni.createCanvasContext(canvasId, vueInstance) | |
| 653 | + const chart = new uCharts({ | |
| 654 | + type: 'column', | |
| 655 | + context: ctx, | |
| 656 | + width: options.width || 700, | |
| 657 | + height: options.height || 400, | |
| 658 | + categories: chartData.categories, | |
| 659 | + series: chartData.series, | |
| 660 | + animation: true, | |
| 661 | + background: 'none', | |
| 662 | + color: chartData.series.map(s => s.color), | |
| 663 | + padding: [15, 15, 0, 15], | |
| 664 | + enableScroll: false, | |
| 665 | + legend: opts.legend, | |
| 666 | + xAxis: opts.xAxis, | |
| 667 | + yAxis: opts.yAxis, | |
| 668 | + extra: opts.extra | |
| 669 | + }) | |
| 670 | + return | |
| 671 | + } | |
| 672 | + const ctx = uni.createCanvasContext(canvasId, vueInstance) | |
| 673 | + const systemInfo = uni.getSystemInfoSync() | |
| 674 | + const pixelRatio = systemInfo.pixelRatio || 1 | |
| 675 | + const chartWidth = Math.floor(uni.upx2px(rect.width) * pixelRatio) || options.width || 700 | |
| 676 | + const chartHeight = Math.floor(uni.upx2px(rect.height) * pixelRatio) || options.height || 400 | |
| 677 | + const chart = new uCharts({ | |
| 678 | + type: 'column', | |
| 679 | + context: ctx, | |
| 680 | + width: chartWidth, | |
| 681 | + height: chartHeight, | |
| 682 | + categories: chartData.categories, | |
| 683 | + series: chartData.series, | |
| 684 | + animation: true, | |
| 685 | + background: 'none', | |
| 686 | + color: chartData.series.map(s => s.color), | |
| 687 | + padding: [15, 15, 0, 15], | |
| 688 | + enableScroll: false, | |
| 689 | + legend: opts.legend, | |
| 690 | + xAxis: opts.xAxis, | |
| 691 | + yAxis: opts.yAxis, | |
| 692 | + extra: opts.extra | |
| 693 | + }) | |
| 694 | + }).exec() | |
| 695 | + }, | |
| 696 | + | |
| 697 | + // 绘制饼图 | |
| 698 | + drawPieChart(vueInstance, canvasId, data, options = {}) { | |
| 699 | + if (!data || data.length === 0) { | |
| 700 | + return | |
| 701 | + } | |
| 702 | + | |
| 703 | + const chartData = { | |
| 704 | + series: [{ | |
| 705 | + data: data.map(item => ({ | |
| 706 | + name: item.name, | |
| 707 | + value: item.value, | |
| 708 | + color: item.color | |
| 709 | + })) | |
| 710 | + }] | |
| 711 | + } | |
| 712 | + | |
| 713 | + const opts = { | |
| 714 | + type: 'pie', | |
| 715 | + fontSize: 11, | |
| 716 | + dataLabel: true, | |
| 717 | + legend: { | |
| 718 | + show: false | |
| 719 | + }, | |
| 720 | + extra: { | |
| 721 | + pie: { | |
| 722 | + activeOpacity: 0.5, | |
| 723 | + activeRadius: 10, | |
| 724 | + offsetAngle: 0, | |
| 725 | + labelWidth: 15, | |
| 726 | + border: true, | |
| 727 | + borderWidth: 3, | |
| 728 | + borderColor: '#FFFFFF' | |
| 729 | + }, | |
| 730 | + tooltip: { | |
| 731 | + showBox: true, | |
| 732 | + showArrow: true | |
| 733 | + } | |
| 734 | + } | |
| 735 | + } | |
| 736 | + | |
| 737 | + const query = uni.createSelectorQuery().in(vueInstance) | |
| 738 | + query.select(`#${canvasId}`).boundingClientRect((rect) => { | |
| 739 | + if (!rect || !rect.width || !rect.height) { | |
| 740 | + console.warn(`图表容器 ${canvasId} 尺寸获取失败`, rect) | |
| 741 | + // 使用默认尺寸 | |
| 742 | + const ctx = uni.createCanvasContext(canvasId, vueInstance) | |
| 743 | + const chart = new uCharts({ | |
| 744 | + type: 'pie', | |
| 745 | + context: ctx, | |
| 746 | + width: options.width || 400, | |
| 747 | + height: options.height || 400, | |
| 748 | + series: chartData.series, | |
| 749 | + animation: true, | |
| 750 | + background: 'none', | |
| 751 | + color: chartData.series[0].data.map(d => d.color), | |
| 752 | + padding: 0, | |
| 753 | + enableScroll: false, | |
| 754 | + legend: opts.legend, | |
| 755 | + extra: opts.extra | |
| 756 | + }) | |
| 757 | + return | |
| 758 | + } | |
| 759 | + const ctx = uni.createCanvasContext(canvasId, vueInstance) | |
| 760 | + const systemInfo = uni.getSystemInfoSync() | |
| 761 | + const pixelRatio = systemInfo.pixelRatio || 1 | |
| 762 | + const chartWidth = Math.floor(uni.upx2px(rect.width) * pixelRatio) || options.width || 400 | |
| 763 | + const chartHeight = Math.floor(uni.upx2px(rect.height) * pixelRatio) || options.height || 400 | |
| 764 | + const chart = new uCharts({ | |
| 765 | + type: 'pie', | |
| 766 | + context: ctx, | |
| 767 | + width: chartWidth, | |
| 768 | + height: chartHeight, | |
| 769 | + series: chartData.series, | |
| 770 | + animation: true, | |
| 771 | + background: 'none', | |
| 772 | + color: chartData.series[0].data.map(d => d.color), | |
| 773 | + padding: 0, | |
| 774 | + enableScroll: false, | |
| 775 | + legend: opts.legend, | |
| 776 | + extra: opts.extra | |
| 777 | + }) | |
| 778 | + }).exec() | |
| 779 | + }, | |
| 780 | + | |
| 781 | + // 绘制漏斗图 | |
| 782 | + drawFunnelChart(vueInstance, canvasId, data, options = {}) { | |
| 783 | + if (!data || data.length === 0) { | |
| 784 | + return | |
| 785 | + } | |
| 786 | + | |
| 787 | + // uCharts 漏斗图数据格式:series 中的 data 应该是对象数组 | |
| 788 | + // 每个对象包含 name、centerText、value 属性 | |
| 789 | + // 数据按从大到小排序,形成漏斗形状 | |
| 790 | + const sortedData = [...data].sort((a, b) => (b.value || 0) - (a.value || 0)) | |
| 791 | + | |
| 792 | + const chartData = { | |
| 793 | + series: [{ | |
| 794 | + data: sortedData.map(item => ({ | |
| 795 | + name: item.name || '', | |
| 796 | + centerText: String(item.value || 0), | |
| 797 | + value: Number(item.value) || 0 | |
| 798 | + })) | |
| 799 | + }] | |
| 800 | + } | |
| 801 | + | |
| 802 | + const opts = { | |
| 803 | + type: 'funnel', | |
| 804 | + fontSize: 11, | |
| 805 | + dataLabel: true, | |
| 806 | + padding: [15, 15, 0, 15], | |
| 807 | + enableScroll: false, | |
| 808 | + color: sortedData.map(item => item.color), | |
| 809 | + extra: { | |
| 810 | + funnel: { | |
| 811 | + activeOpacity: 0.3, | |
| 812 | + activeWidth: 10, | |
| 813 | + border: true, | |
| 814 | + borderWidth: 2, | |
| 815 | + borderColor: '#FFFFFF', | |
| 816 | + fillOpacity: 1, | |
| 817 | + labelAlign: 'right' | |
| 818 | + }, | |
| 819 | + tooltip: { | |
| 820 | + showBox: true, | |
| 821 | + showArrow: true | |
| 822 | + } | |
| 823 | + } | |
| 824 | + } | |
| 825 | + | |
| 826 | + const query = uni.createSelectorQuery().in(vueInstance) | |
| 827 | + query.select(`#${canvasId}`).boundingClientRect((rect) => { | |
| 828 | + if (!rect || !rect.width || !rect.height) { | |
| 829 | + console.warn(`图表容器 ${canvasId} 尺寸获取失败`, rect) | |
| 830 | + const ctx = uni.createCanvasContext(canvasId, vueInstance) | |
| 831 | + const chart = new uCharts({ | |
| 832 | + type: 'funnel', | |
| 833 | + context: ctx, | |
| 834 | + width: options.width || 700, | |
| 835 | + height: options.height || 500, | |
| 836 | + series: chartData.series, | |
| 837 | + animation: true, | |
| 838 | + background: 'none', | |
| 839 | + color: opts.color, | |
| 840 | + padding: opts.padding, | |
| 841 | + enableScroll: opts.enableScroll, | |
| 842 | + extra: opts.extra | |
| 843 | + }) | |
| 844 | + return | |
| 845 | + } | |
| 846 | + const ctx = uni.createCanvasContext(canvasId, vueInstance) | |
| 847 | + const systemInfo = uni.getSystemInfoSync() | |
| 848 | + const pixelRatio = systemInfo.pixelRatio || 1 | |
| 849 | + const chartWidth = Math.floor(uni.upx2px(rect.width) * pixelRatio) || options.width || 700 | |
| 850 | + const chartHeight = Math.floor(uni.upx2px(rect.height) * pixelRatio) || options.height || 500 | |
| 851 | + const chart = new uCharts({ | |
| 852 | + type: 'funnel', | |
| 853 | + context: ctx, | |
| 854 | + width: chartWidth, | |
| 855 | + height: chartHeight, | |
| 856 | + series: chartData.series, | |
| 857 | + animation: true, | |
| 858 | + background: 'none', | |
| 859 | + color: opts.color, | |
| 860 | + padding: opts.padding, | |
| 861 | + enableScroll: opts.enableScroll, | |
| 862 | + extra: opts.extra | |
| 863 | + }) | |
| 864 | + }).exec() | |
| 865 | + }, | |
| 866 | + | |
| 867 | + // 绘制散点图 | |
| 868 | + drawScatterChart(vueInstance, canvasId, data, options = {}) { | |
| 869 | + if (!data || data.length === 0) { | |
| 870 | + return | |
| 871 | + } | |
| 872 | + | |
| 873 | + // 构建散点图数据格式 | |
| 874 | + const scatterData = data.map(item => [ | |
| 875 | + item.AvgAmountPerPerson || 0, | |
| 876 | + item.AvgProjectPerPerson || 0, | |
| 877 | + item.MemberCount || 0 | |
| 878 | + ]) | |
| 879 | + | |
| 880 | + const opts = { | |
| 881 | + type: 'scatter', | |
| 882 | + fontSize: 11, | |
| 883 | + legend: { | |
| 884 | + show: true, | |
| 885 | + position: 'top', | |
| 886 | + lineHeight: 25 | |
| 887 | + }, | |
| 888 | + xAxis: { | |
| 889 | + name: '客单价(元)', | |
| 890 | + nameLocation: 'middle', | |
| 891 | + nameGap: 30, | |
| 892 | + disableGrid: false | |
| 893 | + }, | |
| 894 | + yAxis: { | |
| 895 | + name: '项目数', | |
| 896 | + nameLocation: 'middle', | |
| 897 | + nameGap: 50, | |
| 898 | + gridType: 'dash', | |
| 899 | + dashLength: 2 | |
| 900 | + }, | |
| 901 | + extra: { | |
| 902 | + scatter: { | |
| 903 | + pointShape: 'circle', | |
| 904 | + pointSize: 8, | |
| 905 | + activePointShape: 'circle', | |
| 906 | + activePointSize: 12 | |
| 907 | + }, | |
| 908 | + tooltip: { | |
| 909 | + showBox: true, | |
| 910 | + showArrow: true | |
| 911 | + } | |
| 912 | + } | |
| 913 | + } | |
| 914 | + | |
| 915 | + const query = uni.createSelectorQuery().in(vueInstance) | |
| 916 | + query.select(`#${canvasId}`).boundingClientRect((rect) => { | |
| 917 | + if (!rect || !rect.width || !rect.height) { | |
| 918 | + console.warn(`图表容器 ${canvasId} 尺寸获取失败`, rect) | |
| 919 | + const ctx = uni.createCanvasContext(canvasId, vueInstance) | |
| 920 | + const chart = new uCharts({ | |
| 921 | + type: 'scatter', | |
| 922 | + context: ctx, | |
| 923 | + width: options.width || 700, | |
| 924 | + height: options.height || 500, | |
| 925 | + categories: [], | |
| 926 | + series: [{ | |
| 927 | + name: '会员分布', | |
| 928 | + data: scatterData | |
| 929 | + }], | |
| 930 | + animation: true, | |
| 931 | + background: 'none', | |
| 932 | + color: ['#409EFF'], | |
| 933 | + padding: [15, 15, 0, 15], | |
| 934 | + enableScroll: false, | |
| 935 | + legend: opts.legend, | |
| 936 | + xAxis: opts.xAxis, | |
| 937 | + yAxis: opts.yAxis, | |
| 938 | + extra: opts.extra | |
| 939 | + }) | |
| 940 | + return | |
| 941 | + } | |
| 942 | + const ctx = uni.createCanvasContext(canvasId, vueInstance) | |
| 943 | + const systemInfo = uni.getSystemInfoSync() | |
| 944 | + const pixelRatio = systemInfo.pixelRatio || 1 | |
| 945 | + const chartWidth = Math.floor(uni.upx2px(rect.width) * pixelRatio) || options.width || 700 | |
| 946 | + const chartHeight = Math.floor(uni.upx2px(rect.height) * pixelRatio) || options.height || 500 | |
| 947 | + const chart = new uCharts({ | |
| 948 | + type: 'scatter', | |
| 949 | + context: ctx, | |
| 950 | + width: chartWidth, | |
| 951 | + height: chartHeight, | |
| 952 | + categories: [], | |
| 953 | + series: [{ | |
| 954 | + name: '会员分布', | |
| 955 | + data: scatterData | |
| 956 | + }], | |
| 957 | + animation: true, | |
| 958 | + background: 'none', | |
| 959 | + color: ['#409EFF'], | |
| 960 | + padding: [15, 15, 0, 15], | |
| 961 | + enableScroll: false, | |
| 962 | + legend: opts.legend, | |
| 963 | + xAxis: opts.xAxis, | |
| 964 | + yAxis: opts.yAxis, | |
| 965 | + extra: opts.extra | |
| 966 | + }) | |
| 967 | + }).exec() | |
| 968 | + }, | |
| 969 | + | |
| 970 | + // 绘制热力图 | |
| 971 | + drawHeatmapChart(vueInstance, canvasId, data, options = {}) { | |
| 972 | + if (!data || data.length === 0) { | |
| 973 | + return | |
| 974 | + } | |
| 975 | + | |
| 976 | + // 构建热力图数据:按星期和时间段组织 | |
| 977 | + const days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] | |
| 978 | + const times = ['09:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00', '17:00', '18:00', '19:00', '20:00', '21:00'] | |
| 979 | + | |
| 980 | + // 构建热力图矩阵数据 | |
| 981 | + const heatmapMatrix = [] | |
| 982 | + days.forEach((day, dayIndex) => { | |
| 983 | + times.forEach((time, timeIndex) => { | |
| 984 | + const item = data.find(d => { | |
| 985 | + const dayLabel = vueInstance.getDayLabel(d.DayOfWeek) | |
| 986 | + return dayLabel === day && d.TimeSlot === time | |
| 987 | + }) | |
| 988 | + heatmapMatrix.push([ | |
| 989 | + timeIndex, | |
| 990 | + dayIndex, | |
| 991 | + item ? (item.CustomerFlow || 0) : 0 | |
| 992 | + ]) | |
| 993 | + }) | |
| 994 | + }) | |
| 995 | + | |
| 996 | + const opts = { | |
| 997 | + type: 'heatmap', | |
| 998 | + fontSize: 10, | |
| 999 | + extra: { | |
| 1000 | + heatmap: { | |
| 1001 | + activeRadius: 5 | |
| 1002 | + }, | |
| 1003 | + tooltip: { | |
| 1004 | + showBox: true, | |
| 1005 | + showArrow: true | |
| 1006 | + } | |
| 1007 | + } | |
| 1008 | + } | |
| 1009 | + | |
| 1010 | + const query = uni.createSelectorQuery().in(vueInstance) | |
| 1011 | + query.select(`#${canvasId}`).boundingClientRect((rect) => { | |
| 1012 | + if (!rect || !rect.width || !rect.height) { | |
| 1013 | + console.warn(`图表容器 ${canvasId} 尺寸获取失败`, rect) | |
| 1014 | + const ctx = uni.createCanvasContext(canvasId, vueInstance) | |
| 1015 | + const chart = new uCharts({ | |
| 1016 | + type: 'heatmap', | |
| 1017 | + context: ctx, | |
| 1018 | + width: options.width || 700, | |
| 1019 | + height: options.height || 600, | |
| 1020 | + categories: times, | |
| 1021 | + series: [{ | |
| 1022 | + name: '客流量', | |
| 1023 | + data: heatmapMatrix | |
| 1024 | + }], | |
| 1025 | + animation: true, | |
| 1026 | + background: 'none', | |
| 1027 | + color: ['#e0f3ff', '#409EFF', '#1d4ed8'], | |
| 1028 | + padding: [15, 15, 0, 15], | |
| 1029 | + enableScroll: false, | |
| 1030 | + extra: opts.extra | |
| 1031 | + }) | |
| 1032 | + return | |
| 1033 | + } | |
| 1034 | + const ctx = uni.createCanvasContext(canvasId, vueInstance) | |
| 1035 | + const systemInfo = uni.getSystemInfoSync() | |
| 1036 | + const pixelRatio = systemInfo.pixelRatio || 1 | |
| 1037 | + const chartWidth = Math.floor(uni.upx2px(rect.width) * pixelRatio) || options.width || 700 | |
| 1038 | + const chartHeight = Math.floor(uni.upx2px(rect.height) * pixelRatio) || options.height || 600 | |
| 1039 | + const chart = new uCharts({ | |
| 1040 | + type: 'heatmap', | |
| 1041 | + context: ctx, | |
| 1042 | + width: chartWidth, | |
| 1043 | + height: chartHeight, | |
| 1044 | + categories: times, | |
| 1045 | + series: [{ | |
| 1046 | + name: '客流量', | |
| 1047 | + data: heatmapMatrix | |
| 1048 | + }], | |
| 1049 | + animation: true, | |
| 1050 | + background: 'none', | |
| 1051 | + color: ['#e0f3ff', '#409EFF', '#1d4ed8'], | |
| 1052 | + padding: [15, 15, 0, 15], | |
| 1053 | + enableScroll: false, | |
| 1054 | + extra: opts.extra | |
| 1055 | + }) | |
| 1056 | + }).exec() | |
| 1057 | + }, | |
| 1058 | + | |
| 1059 | + // 绘制仪表盘 | |
| 1060 | + drawGaugeChart(vueInstance, canvasId, value, options = {}) { | |
| 1061 | + // uCharts 仪表盘需要 categories(刻度标签数组)和 series(数据值数组) | |
| 1062 | + const chartValue = Math.min(100, Math.max(0, value || 0)) | |
| 1063 | + const chartData = { | |
| 1064 | + categories: ['0', '20', '40', '60', '80', '100'], | |
| 1065 | + series: [{ | |
| 1066 | + name: '完成率', | |
| 1067 | + data: [chartValue] | |
| 1068 | + }] | |
| 1069 | + } | |
| 1070 | + | |
| 1071 | + const opts = { | |
| 1072 | + type: 'gauge', | |
| 1073 | + fontSize: 11, | |
| 1074 | + dataLabel: true, | |
| 1075 | + extra: { | |
| 1076 | + gauge: { | |
| 1077 | + type: 'default', | |
| 1078 | + width: 2, | |
| 1079 | + labelColor: '#666666', | |
| 1080 | + labelShow: true, | |
| 1081 | + startAngle: 0.75, | |
| 1082 | + endAngle: 0.25, | |
| 1083 | + splitLine: { | |
| 1084 | + show: true, | |
| 1085 | + lineLength: 15, | |
| 1086 | + lineColor: '#FFFFFF', | |
| 1087 | + lineWidth: 2 | |
| 1088 | + }, | |
| 1089 | + pointer: { | |
| 1090 | + show: true, | |
| 1091 | + width: 5, | |
| 1092 | + length: 80 | |
| 1093 | + }, | |
| 1094 | + labelFormat: (val) => { | |
| 1095 | + return val.toFixed(1) + '%' | |
| 1096 | + } | |
| 1097 | + }, | |
| 1098 | + tooltip: { | |
| 1099 | + showBox: true, | |
| 1100 | + showArrow: true | |
| 1101 | + } | |
| 1102 | + } | |
| 1103 | + } | |
| 1104 | + | |
| 1105 | + const query = uni.createSelectorQuery().in(vueInstance) | |
| 1106 | + query.select(`#${canvasId}`).boundingClientRect((rect) => { | |
| 1107 | + if (!rect || !rect.width || !rect.height) { | |
| 1108 | + console.warn(`图表容器 ${canvasId} 尺寸获取失败`, rect) | |
| 1109 | + const ctx = uni.createCanvasContext(canvasId, vueInstance) | |
| 1110 | + const chart = new uCharts({ | |
| 1111 | + type: 'gauge', | |
| 1112 | + context: ctx, | |
| 1113 | + width: options.width || 400, | |
| 1114 | + height: options.height || 400, | |
| 1115 | + categories: chartData.categories, | |
| 1116 | + series: chartData.series, | |
| 1117 | + animation: true, | |
| 1118 | + background: 'none', | |
| 1119 | + color: ['#67C23A', '#409EFF', '#F56C6C'], | |
| 1120 | + padding: 0, | |
| 1121 | + enableScroll: false, | |
| 1122 | + extra: opts.extra | |
| 1123 | + }) | |
| 1124 | + return | |
| 1125 | + } | |
| 1126 | + const ctx = uni.createCanvasContext(canvasId, vueInstance) | |
| 1127 | + const systemInfo = uni.getSystemInfoSync() | |
| 1128 | + const pixelRatio = systemInfo.pixelRatio || 1 | |
| 1129 | + const chartWidth = Math.floor(uni.upx2px(rect.width) * pixelRatio) || options.width || 400 | |
| 1130 | + const chartHeight = Math.floor(uni.upx2px(rect.height) * pixelRatio) || options.height || 400 | |
| 1131 | + const chart = new uCharts({ | |
| 1132 | + type: 'gauge', | |
| 1133 | + context: ctx, | |
| 1134 | + width: chartWidth, | |
| 1135 | + height: chartHeight, | |
| 1136 | + categories: chartData.categories, | |
| 1137 | + series: chartData.series, | |
| 1138 | + animation: true, | |
| 1139 | + background: 'none', | |
| 1140 | + color: ['#67C23A', '#409EFF', '#F56C6C'], | |
| 1141 | + padding: 0, | |
| 1142 | + enableScroll: false, | |
| 1143 | + extra: opts.extra | |
| 1144 | + }) | |
| 1145 | + }).exec() | |
| 1146 | + } | |
| 1147 | + } | |
| 1148 | + | |
| 1149 | + export default { | |
| 1150 | + data() { | |
| 1151 | + return { | |
| 1152 | + loading: false, | |
| 1153 | + queryParams: { | |
| 1154 | + month: '', // 格式:YYYY-MM | |
| 1155 | + storeId: '' | |
| 1156 | + }, | |
| 1157 | + storeOptions: [], | |
| 1158 | + storeIndex: -1, | |
| 1159 | + currentStoreName: '', | |
| 1160 | + currentStoreCode: '', | |
| 1161 | + currentStoreAddress: '', | |
| 1162 | + storeData: null, | |
| 1163 | + memberData: null, | |
| 1164 | + trendData: [], | |
| 1165 | + healthCoachRanking: [], | |
| 1166 | + topBillingItems: [], | |
| 1167 | + topConsumeItems: [], | |
| 1168 | + categoryData: [], | |
| 1169 | + categoryMonthlyData: [], | |
| 1170 | + funnelData: null, | |
| 1171 | + scatterData: [], | |
| 1172 | + heatmapData: [], | |
| 1173 | + operationTips: [], | |
| 1174 | + dataInsights: [], | |
| 1175 | + keyMetrics: [], | |
| 1176 | + comparison: { | |
| 1177 | + performanceRanking: 0, | |
| 1178 | + totalStoreCount: 0, | |
| 1179 | + avgPerformanceSameType: 0, | |
| 1180 | + sameTypeStoreCount: 0, | |
| 1181 | + avgPerformanceSameOrg: 0, | |
| 1182 | + sameOrgStoreCount: 0 | |
| 1183 | + } | |
| 1184 | + } | |
| 1185 | + }, | |
| 1186 | + computed: { | |
| 1187 | + // 显示最近6个月的趋势数据 | |
| 1188 | + displayTrendData() { | |
| 1189 | + if (!this.trendData || this.trendData.length === 0) return [] | |
| 1190 | + return this.trendData.slice(-6).reverse() // 取最后6个月,倒序显示 | |
| 1191 | + }, | |
| 1192 | + // 显示最近6个月的分类业绩数据 | |
| 1193 | + displayCategoryMonthlyData() { | |
| 1194 | + if (!this.categoryMonthlyData || this.categoryMonthlyData.length === 0) return [] | |
| 1195 | + return this.categoryMonthlyData.slice(-6).reverse().map(item => ({ | |
| 1196 | + ...item, | |
| 1197 | + TotalPerformance: (item.BeautyPerformance || 0) + (item.MedicalPerformance || 0) + (item.TechPerformance || 0) + (item.ProductPerformance || 0) | |
| 1198 | + })) | |
| 1199 | + }, | |
| 1200 | + // 转化漏斗步骤 | |
| 1201 | + funnelSteps() { | |
| 1202 | + if (!this.funnelData) return [] | |
| 1203 | + const maxValue = Math.max( | |
| 1204 | + this.funnelData.ExpansionCount || 0, | |
| 1205 | + this.funnelData.InviteCount || 0, | |
| 1206 | + this.funnelData.AppointmentCount || 0, | |
| 1207 | + this.funnelData.BillingCount || 0 | |
| 1208 | + ) || 1 | |
| 1209 | + const steps = [ | |
| 1210 | + { label: '拓客', value: this.funnelData.ExpansionCount || 0, color: '#409EFF' }, | |
| 1211 | + { label: '邀约', value: this.funnelData.InviteCount || 0, color: '#67C23A' }, | |
| 1212 | + { label: '预约', value: this.funnelData.AppointmentCount || 0, color: '#E6A23C' }, | |
| 1213 | + { label: '开单', value: this.funnelData.BillingCount || 0, color: '#909399' } | |
| 1214 | + ] | |
| 1215 | + return steps.map(step => ({ | |
| 1216 | + ...step, | |
| 1217 | + percent: maxValue > 0 ? Math.round((step.value / maxValue) * 100) : 0 | |
| 1218 | + })) | |
| 1219 | + } | |
| 1220 | + }, | |
| 1221 | + onLoad() { | |
| 1222 | + this.initQueryParams() | |
| 1223 | + this.loadStoreOptions() | |
| 1224 | + }, | |
| 1225 | + onPullDownRefresh() { | |
| 1226 | + // 下拉刷新 | |
| 1227 | + if (this.queryParams.storeId && this.queryParams.month) { | |
| 1228 | + this.loadDashboardData().finally(() => { | |
| 1229 | + uni.stopPullDownRefresh() | |
| 1230 | + }) | |
| 1231 | + } else { | |
| 1232 | + uni.stopPullDownRefresh() | |
| 1233 | + } | |
| 1234 | + }, | |
| 1235 | + methods: { | |
| 1236 | + // 初始化查询参数 | |
| 1237 | + initQueryParams() { | |
| 1238 | + const now = new Date() | |
| 1239 | + const year = now.getFullYear() | |
| 1240 | + const month = String(now.getMonth() + 1).padStart(2, '0') | |
| 1241 | + this.queryParams.month = `${year}-${month}` | |
| 1242 | + }, | |
| 1243 | + // 加载门店选项 | |
| 1244 | + async loadStoreOptions() { | |
| 1245 | + try { | |
| 1246 | + uni.showLoading({ | |
| 1247 | + title: '加载门店列表...' | |
| 1248 | + }) | |
| 1249 | + const response = await storeApi.getStoreList({ | |
| 1250 | + pageSize: 1000, | |
| 1251 | + currentPage: 1 | |
| 1252 | + }) | |
| 1253 | + uni.hideLoading() | |
| 1254 | + | |
| 1255 | + console.log('门店列表响应:', response) | |
| 1256 | + | |
| 1257 | + if (response.code === 200 && response.data) { | |
| 1258 | + // 处理不同的数据结构 | |
| 1259 | + let storeList = [] | |
| 1260 | + if (Array.isArray(response.data)) { | |
| 1261 | + storeList = response.data | |
| 1262 | + } else if (response.data.list && Array.isArray(response.data.list)) { | |
| 1263 | + storeList = response.data.list | |
| 1264 | + } | |
| 1265 | + | |
| 1266 | + // 确保门店数据包含必要的字段 | |
| 1267 | + this.storeOptions = storeList.map(item => ({ | |
| 1268 | + id: item.id || item.F_Id || '', | |
| 1269 | + dm: item.dm || item.DM || item.fullName || '未知门店', | |
| 1270 | + bm: item.bm || item.BM || item.enCode || '', | |
| 1271 | + address: item.dz || item.DZ || item.address || '', | |
| 1272 | + ...item // 保留其他字段 | |
| 1273 | + })) | |
| 1274 | + | |
| 1275 | + console.log('处理后的门店列表:', this.storeOptions) | |
| 1276 | + | |
| 1277 | + // 如果只有一个门店,默认选中 | |
| 1278 | + if (this.storeOptions.length === 1) { | |
| 1279 | + this.storeIndex = 0 | |
| 1280 | + this.queryParams.storeId = this.storeOptions[0].id | |
| 1281 | + this.currentStoreName = this.storeOptions[0].dm || '' | |
| 1282 | + this.currentStoreCode = this.storeOptions[0].bm || '' | |
| 1283 | + this.currentStoreAddress = this.storeOptions[0].address || '' | |
| 1284 | + this.loadDashboardData() | |
| 1285 | + } else if (this.storeOptions.length === 0) { | |
| 1286 | + uni.showToast({ | |
| 1287 | + title: '暂无门店数据', | |
| 1288 | + icon: 'none' | |
| 1289 | + }) | |
| 1290 | + } | |
| 1291 | + } else { | |
| 1292 | + console.error('门店列表响应错误:', response) | |
| 1293 | + uni.showToast({ | |
| 1294 | + title: response.msg || '获取门店列表失败', | |
| 1295 | + icon: 'none' | |
| 1296 | + }) | |
| 1297 | + } | |
| 1298 | + } catch (error) { | |
| 1299 | + uni.hideLoading() | |
| 1300 | + console.error('获取门店列表失败:', error) | |
| 1301 | + uni.showToast({ | |
| 1302 | + title: '获取门店列表失败:' + (error.message || '未知错误'), | |
| 1303 | + icon: 'none', | |
| 1304 | + duration: 3000 | |
| 1305 | + }) | |
| 1306 | + } | |
| 1307 | + }, | |
| 1308 | + // 月份选择变化 | |
| 1309 | + onMonthChange(e) { | |
| 1310 | + const value = e.detail.value | |
| 1311 | + // 处理不同平台的日期格式 | |
| 1312 | + if (value && value.length >= 7) { | |
| 1313 | + this.queryParams.month = value.substring(0, 7) // 只取年月部分 YYYY-MM | |
| 1314 | + } else if (value && value.length === 6) { | |
| 1315 | + // 如果是 YYYYMM 格式,转换为 YYYY-MM | |
| 1316 | + this.queryParams.month = value.substring(0, 4) + '-' + value.substring(4, 6) | |
| 1317 | + } | |
| 1318 | + }, | |
| 1319 | + // 门店选择变化 | |
| 1320 | + onStoreChange(e) { | |
| 1321 | + const index = e.detail.value | |
| 1322 | + console.log('门店选择索引:', index, '门店列表:', this.storeOptions) | |
| 1323 | + this.storeIndex = index | |
| 1324 | + if (this.storeOptions[index]) { | |
| 1325 | + const store = this.storeOptions[index] | |
| 1326 | + this.queryParams.storeId = store.id | |
| 1327 | + this.currentStoreName = store.dm || store.DM || store.fullName || '未知门店' | |
| 1328 | + this.currentStoreCode = store.bm || store.BM || store.enCode || '' | |
| 1329 | + this.currentStoreAddress = store.address || store.dz || store.DZ || '' | |
| 1330 | + console.log('选中的门店:', { | |
| 1331 | + id: this.queryParams.storeId, | |
| 1332 | + name: this.currentStoreName, | |
| 1333 | + code: this.currentStoreCode | |
| 1334 | + }) | |
| 1335 | + } else { | |
| 1336 | + console.error('门店索引超出范围:', index, '门店列表长度:', this.storeOptions.length) | |
| 1337 | + } | |
| 1338 | + }, | |
| 1339 | + // 查询 | |
| 1340 | + handleQuery() { | |
| 1341 | + console.log('查询参数:', this.queryParams) | |
| 1342 | + if (!this.queryParams.storeId) { | |
| 1343 | + uni.showToast({ | |
| 1344 | + title: '请选择门店', | |
| 1345 | + icon: 'none' | |
| 1346 | + }) | |
| 1347 | + return | |
| 1348 | + } | |
| 1349 | + if (!this.queryParams.month) { | |
| 1350 | + uni.showToast({ | |
| 1351 | + title: '请选择月份', | |
| 1352 | + icon: 'none' | |
| 1353 | + }) | |
| 1354 | + return | |
| 1355 | + } | |
| 1356 | + console.log('开始加载数据,门店ID:', this.queryParams.storeId, '月份:', this.queryParams.month) | |
| 1357 | + this.loadDashboardData() | |
| 1358 | + }, | |
| 1359 | + // 重置 | |
| 1360 | + handleReset() { | |
| 1361 | + this.initQueryParams() | |
| 1362 | + this.storeIndex = -1 | |
| 1363 | + this.queryParams.storeId = '' | |
| 1364 | + this.currentStoreName = '' | |
| 1365 | + this.currentStoreCode = '' | |
| 1366 | + this.currentStoreAddress = '' | |
| 1367 | + this.storeData = null | |
| 1368 | + this.memberData = null | |
| 1369 | + this.trendData = [] | |
| 1370 | + this.healthCoachRanking = [] | |
| 1371 | + this.topBillingItems = [] | |
| 1372 | + this.topConsumeItems = [] | |
| 1373 | + this.categoryData = [] | |
| 1374 | + this.categoryMonthlyData = [] | |
| 1375 | + this.funnelData = null | |
| 1376 | + this.scatterData = [] | |
| 1377 | + this.heatmapData = [] | |
| 1378 | + this.operationTips = [] | |
| 1379 | + this.dataInsights = [] | |
| 1380 | + this.keyMetrics = [] | |
| 1381 | + this.comparison = { | |
| 1382 | + performanceRanking: 0, | |
| 1383 | + totalStoreCount: 0, | |
| 1384 | + avgPerformanceSameType: 0, | |
| 1385 | + sameTypeStoreCount: 0, | |
| 1386 | + avgPerformanceSameOrg: 0, | |
| 1387 | + sameOrgStoreCount: 0 | |
| 1388 | + } | |
| 1389 | + }, | |
| 1390 | + // 加载驾驶舱数据 | |
| 1391 | + async loadDashboardData() { | |
| 1392 | + if (!this.queryParams.storeId || !this.queryParams.month) { | |
| 1393 | + console.warn('缺少必要参数,门店ID:', this.queryParams.storeId, '月份:', this.queryParams.month) | |
| 1394 | + return | |
| 1395 | + } | |
| 1396 | + | |
| 1397 | + this.loading = true | |
| 1398 | + try { | |
| 1399 | + // 将月份格式从 YYYY-MM 转换为 YYYYMM | |
| 1400 | + const statisticsMonth = this.queryParams.month.replace('-', '') | |
| 1401 | + console.log('加载数据参数:', { | |
| 1402 | + storeId: this.queryParams.storeId, | |
| 1403 | + statisticsMonth: statisticsMonth | |
| 1404 | + }) | |
| 1405 | + | |
| 1406 | + // 并行加载所有数据 | |
| 1407 | + const [ | |
| 1408 | + statisticsRes, | |
| 1409 | + trendRes, | |
| 1410 | + memberRes, | |
| 1411 | + itemRes, | |
| 1412 | + healthCoachRes, | |
| 1413 | + comparisonRes, | |
| 1414 | + categoryMonthlyRes, | |
| 1415 | + funnelRes, | |
| 1416 | + scatterRes, | |
| 1417 | + heatmapRes | |
| 1418 | + ] = await Promise.all([ | |
| 1419 | + storeDashboardApi.getStoreDashboardStatistics({ | |
| 1420 | + storeId: this.queryParams.storeId, | |
| 1421 | + statisticsMonth: statisticsMonth | |
| 1422 | + }), | |
| 1423 | + storeDashboardApi.getStoreMonthlyTrend({ | |
| 1424 | + storeId: this.queryParams.storeId, | |
| 1425 | + statisticsMonth: statisticsMonth | |
| 1426 | + }), | |
| 1427 | + storeDashboardApi.getStoreMemberAnalysis({ | |
| 1428 | + storeId: this.queryParams.storeId, | |
| 1429 | + statisticsMonth: statisticsMonth | |
| 1430 | + }), | |
| 1431 | + storeDashboardApi.getStoreItemAnalysis({ | |
| 1432 | + storeId: this.queryParams.storeId, | |
| 1433 | + statisticsMonth: statisticsMonth | |
| 1434 | + }), | |
| 1435 | + storeDashboardApi.getStoreHealthCoachAnalysis({ | |
| 1436 | + storeId: this.queryParams.storeId, | |
| 1437 | + statisticsMonth: statisticsMonth | |
| 1438 | + }), | |
| 1439 | + storeDashboardApi.getStoreComparisonAnalysis({ | |
| 1440 | + storeId: this.queryParams.storeId, | |
| 1441 | + statisticsMonth: statisticsMonth | |
| 1442 | + }), | |
| 1443 | + storeDashboardApi.getCategoryMonthlyPerformance({ | |
| 1444 | + storeId: this.queryParams.storeId, | |
| 1445 | + statisticsMonth: statisticsMonth | |
| 1446 | + }), | |
| 1447 | + storeDashboardApi.getMemberConversionFunnel({ | |
| 1448 | + storeId: this.queryParams.storeId, | |
| 1449 | + statisticsMonth: statisticsMonth | |
| 1450 | + }), | |
| 1451 | + storeDashboardApi.getCustomerPriceProjectRelation({ | |
| 1452 | + storeId: this.queryParams.storeId, | |
| 1453 | + statisticsMonth: statisticsMonth | |
| 1454 | + }), | |
| 1455 | + storeDashboardApi.getWeeklyHeatmap({ | |
| 1456 | + storeId: this.queryParams.storeId, | |
| 1457 | + statisticsMonth: statisticsMonth | |
| 1458 | + }) | |
| 1459 | + ]) | |
| 1460 | + | |
| 1461 | + // 处理统计数据 | |
| 1462 | + if (statisticsRes.code === 200 && statisticsRes.data) { | |
| 1463 | + this.storeData = statisticsRes.data | |
| 1464 | + } | |
| 1465 | + | |
| 1466 | + // 处理趋势数据 | |
| 1467 | + if (trendRes.code === 200 && trendRes.data && trendRes.data.length > 0) { | |
| 1468 | + this.trendData = trendRes.data | |
| 1469 | + } | |
| 1470 | + | |
| 1471 | + // 处理会员数据 | |
| 1472 | + if (memberRes.code === 200 && memberRes.data) { | |
| 1473 | + this.memberData = memberRes.data | |
| 1474 | + } | |
| 1475 | + | |
| 1476 | + // 处理品项数据 | |
| 1477 | + if (itemRes.code === 200 && itemRes.data) { | |
| 1478 | + if (itemRes.data.TopBillingItems) { | |
| 1479 | + this.topBillingItems = itemRes.data.TopBillingItems.slice(0, 10) | |
| 1480 | + } | |
| 1481 | + if (itemRes.data.TopConsumeItems) { | |
| 1482 | + this.topConsumeItems = itemRes.data.TopConsumeItems.slice(0, 10) | |
| 1483 | + } | |
| 1484 | + if (itemRes.data.CategoryRatios) { | |
| 1485 | + this.categoryData = itemRes.data.CategoryRatios | |
| 1486 | + } | |
| 1487 | + } | |
| 1488 | + | |
| 1489 | + // 处理健康师排行 | |
| 1490 | + if (healthCoachRes.code === 200 && healthCoachRes.data && healthCoachRes.data.length > 0) { | |
| 1491 | + this.healthCoachRanking = healthCoachRes.data.slice(0, 10) | |
| 1492 | + } | |
| 1493 | + | |
| 1494 | + // 处理排名对比 | |
| 1495 | + if (comparisonRes.code === 200 && comparisonRes.data) { | |
| 1496 | + this.comparison = { | |
| 1497 | + performanceRanking: comparisonRes.data.PerformanceRanking || 0, | |
| 1498 | + totalStoreCount: comparisonRes.data.TotalStoreCount || 0, | |
| 1499 | + avgPerformanceSameType: comparisonRes.data.AvgPerformanceSameType || 0, | |
| 1500 | + sameTypeStoreCount: comparisonRes.data.SameTypeStoreCount || 0, | |
| 1501 | + avgPerformanceSameOrg: comparisonRes.data.AvgPerformanceSameOrg || 0, | |
| 1502 | + sameOrgStoreCount: comparisonRes.data.SameOrgStoreCount || 0 | |
| 1503 | + } | |
| 1504 | + } | |
| 1505 | + | |
| 1506 | + // 处理各分类月度业绩 | |
| 1507 | + if (categoryMonthlyRes.code === 200 && categoryMonthlyRes.data && categoryMonthlyRes.data.length > 0) { | |
| 1508 | + this.categoryMonthlyData = categoryMonthlyRes.data | |
| 1509 | + } | |
| 1510 | + | |
| 1511 | + // 处理转化漏斗 | |
| 1512 | + if (funnelRes.code === 200 && funnelRes.data) { | |
| 1513 | + this.funnelData = funnelRes.data | |
| 1514 | + } | |
| 1515 | + | |
| 1516 | + // 处理客单价与项目数关系 | |
| 1517 | + if (scatterRes.code === 200 && scatterRes.data && scatterRes.data.length > 0) { | |
| 1518 | + this.scatterData = scatterRes.data | |
| 1519 | + } | |
| 1520 | + | |
| 1521 | + // 处理热力图 | |
| 1522 | + if (heatmapRes.code === 200 && heatmapRes.data && heatmapRes.data.length > 0) { | |
| 1523 | + this.heatmapData = heatmapRes.data | |
| 1524 | + } | |
| 1525 | + | |
| 1526 | + // 更新数据洞察、关键指标、经营提示 | |
| 1527 | + this.updateDataInsights() | |
| 1528 | + this.updateKeyMetrics() | |
| 1529 | + this.updateOperationTips() | |
| 1530 | + | |
| 1531 | + // 绘制图表 | |
| 1532 | + this.$nextTick(() => { | |
| 1533 | + this.drawCharts() | |
| 1534 | + }) | |
| 1535 | + } catch (error) { | |
| 1536 | + console.error('加载数据失败:', error) | |
| 1537 | + uni.showToast({ | |
| 1538 | + title: '加载数据失败', | |
| 1539 | + icon: 'none' | |
| 1540 | + }) | |
| 1541 | + } finally { | |
| 1542 | + this.loading = false | |
| 1543 | + } | |
| 1544 | + }, | |
| 1545 | + // 滚动到底部 | |
| 1546 | + onScrollToLower() { | |
| 1547 | + // 可以在这里加载更多数据 | |
| 1548 | + }, | |
| 1549 | + // 格式化金额 | |
| 1550 | + formatMoney(value, decimals = 2) { | |
| 1551 | + if (value === null || value === undefined) return '0.00' | |
| 1552 | + const num = Number(value) | |
| 1553 | + if (isNaN(num)) return '0.00' | |
| 1554 | + return num.toLocaleString('zh-CN', { | |
| 1555 | + minimumFractionDigits: decimals, | |
| 1556 | + maximumFractionDigits: decimals | |
| 1557 | + }) | |
| 1558 | + }, | |
| 1559 | + // 格式化数字 | |
| 1560 | + formatNumber(value) { | |
| 1561 | + if (value === null || value === undefined) return '0' | |
| 1562 | + const num = Number(value) | |
| 1563 | + if (isNaN(num)) return '0' | |
| 1564 | + return num.toLocaleString('zh-CN') | |
| 1565 | + }, | |
| 1566 | + // 格式化月份显示 | |
| 1567 | + formatMonth(month) { | |
| 1568 | + if (!month) return '' | |
| 1569 | + if (month.length === 6) { | |
| 1570 | + return month.substring(0, 4) + '年' + parseInt(month.substring(4, 6)) + '月' | |
| 1571 | + } | |
| 1572 | + return month | |
| 1573 | + }, | |
| 1574 | + // 获取排名样式类 | |
| 1575 | + getRankingClass(index) { | |
| 1576 | + if (index === 0) return 'rank-first' | |
| 1577 | + if (index === 1) return 'rank-second' | |
| 1578 | + if (index === 2) return 'rank-third' | |
| 1579 | + return 'rank-normal' | |
| 1580 | + }, | |
| 1581 | + // 获取分类标签类型 | |
| 1582 | + getCategoryType(category) { | |
| 1583 | + const typeMap = { | |
| 1584 | + '生美': 'primary', | |
| 1585 | + '医美': 'success', | |
| 1586 | + '科美': 'warning', | |
| 1587 | + '产品': 'info' | |
| 1588 | + } | |
| 1589 | + return typeMap[category] || 'default' | |
| 1590 | + }, | |
| 1591 | + // 获取排名文本 | |
| 1592 | + getRankingText(rank, total) { | |
| 1593 | + if (total === 0) return '暂无' | |
| 1594 | + const percentage = rank / total | |
| 1595 | + if (percentage <= 0.2) return '优秀' | |
| 1596 | + if (percentage <= 0.5) return '良好' | |
| 1597 | + return '一般' | |
| 1598 | + }, | |
| 1599 | + // 获取排名标签类型 | |
| 1600 | + getRankingTagType(rank, total) { | |
| 1601 | + if (total === 0) return 'info' | |
| 1602 | + const percentage = rank / total | |
| 1603 | + if (percentage <= 0.2) return 'success' | |
| 1604 | + if (percentage <= 0.5) return 'warning' | |
| 1605 | + return 'info' | |
| 1606 | + }, | |
| 1607 | + // 计算分类占比百分比 | |
| 1608 | + getCategoryPercent(amount) { | |
| 1609 | + if (!this.categoryData || this.categoryData.length === 0) return 0 | |
| 1610 | + const total = this.categoryData.reduce((sum, item) => sum + (item.ConsumeAmount || 0), 0) | |
| 1611 | + if (total === 0) return 0 | |
| 1612 | + return Math.round((amount / total) * 100) | |
| 1613 | + }, | |
| 1614 | + // 获取分类颜色 | |
| 1615 | + getCategoryColor(categoryName) { | |
| 1616 | + const colorMap = { | |
| 1617 | + '生美': '#A8D5E2', | |
| 1618 | + '医美': '#B8E6B8', | |
| 1619 | + '科美': '#FFD4A3', | |
| 1620 | + '产品': '#E6C1E6', | |
| 1621 | + '教育': '#F5DEB3', | |
| 1622 | + '其他': '#DDA0DD' | |
| 1623 | + } | |
| 1624 | + return colorMap[categoryName] || '#DDA0DD' | |
| 1625 | + }, | |
| 1626 | + // 获取完成度颜色 | |
| 1627 | + getCompletionColor(rate) { | |
| 1628 | + if (rate >= 100) return '#67C23A' | |
| 1629 | + if (rate >= 80) return '#409EFF' | |
| 1630 | + if (rate >= 60) return '#E6A23C' | |
| 1631 | + return '#F56C6C' | |
| 1632 | + }, | |
| 1633 | + // 获取提示图标 | |
| 1634 | + getTipIcon(type) { | |
| 1635 | + const iconMap = { | |
| 1636 | + 'success': 'checkmark-circle', | |
| 1637 | + 'warning': 'alert-circle', | |
| 1638 | + 'danger': 'close-circle', | |
| 1639 | + 'info': 'info-circle' | |
| 1640 | + } | |
| 1641 | + return iconMap[type] || 'info-circle' | |
| 1642 | + }, | |
| 1643 | + // 获取提示颜色 | |
| 1644 | + getTipColor(type) { | |
| 1645 | + const colorMap = { | |
| 1646 | + 'success': '#67C23A', | |
| 1647 | + 'warning': '#E6A23C', | |
| 1648 | + 'danger': '#F56C6C', | |
| 1649 | + 'info': '#909399' | |
| 1650 | + } | |
| 1651 | + return colorMap[type] || '#909399' | |
| 1652 | + }, | |
| 1653 | + // 获取星期标签 | |
| 1654 | + getDayLabel(dayOfWeek) { | |
| 1655 | + const dayMap = { | |
| 1656 | + 0: '周一', | |
| 1657 | + 1: '周二', | |
| 1658 | + 2: '周三', | |
| 1659 | + 3: '周四', | |
| 1660 | + 4: '周五', | |
| 1661 | + 5: '周六', | |
| 1662 | + 6: '周日' | |
| 1663 | + } | |
| 1664 | + // MySQL的DAYOFWEEK返回1=周日,2=周一...,转换为0=周一,6=周日 | |
| 1665 | + let index = dayOfWeek === 0 ? 6 : dayOfWeek - 1 | |
| 1666 | + if (index < 0 || index > 6) index = 0 | |
| 1667 | + return dayMap[index] || '未知' | |
| 1668 | + }, | |
| 1669 | + // 获取热力图颜色 | |
| 1670 | + getHeatmapColor(value) { | |
| 1671 | + if (value === 0) return '#f0f0f0' | |
| 1672 | + if (value <= 5) return '#e0f3ff' | |
| 1673 | + if (value <= 10) return '#409EFF' | |
| 1674 | + return '#1d4ed8' | |
| 1675 | + }, | |
| 1676 | + // 计算分类柱状图百分比 | |
| 1677 | + getCategoryBarPercent(value, total) { | |
| 1678 | + if (!total || total === 0) return 0 | |
| 1679 | + return Math.round((value / total) * 100) | |
| 1680 | + }, | |
| 1681 | + // 更新快速数据洞察 | |
| 1682 | + updateDataInsights() { | |
| 1683 | + if (!this.storeData || !this.heatmapData || this.heatmapData.length === 0) { | |
| 1684 | + this.dataInsights = [ | |
| 1685 | + { title: '最佳营业时段', tag: '暂无', tagType: 'info', value: '暂无数据', desc: '暂无数据' }, | |
| 1686 | + { title: '高价值会员', tag: '暂无', tagType: 'info', value: '0人', desc: '暂无数据' }, | |
| 1687 | + { title: '项目转化率', tag: '暂无', tagType: 'info', value: '0%', desc: '暂无数据' }, | |
| 1688 | + { title: '复购周期', tag: '暂无', tagType: 'info', value: '暂无', desc: '暂无数据' } | |
| 1689 | + ] | |
| 1690 | + return | |
| 1691 | + } | |
| 1692 | + | |
| 1693 | + // 1. 最佳营业时段(从热力图数据中找) | |
| 1694 | + let bestHour = 0 | |
| 1695 | + let maxPersonCount = 0 | |
| 1696 | + this.heatmapData.forEach(item => { | |
| 1697 | + if ((item.CustomerFlow || item.PersonCount || 0) > maxPersonCount) { | |
| 1698 | + maxPersonCount = item.CustomerFlow || item.PersonCount || 0 | |
| 1699 | + bestHour = item.Hour || 0 | |
| 1700 | + } | |
| 1701 | + }) | |
| 1702 | + const bestTimeRange = bestHour >= 0 && maxPersonCount > 0 | |
| 1703 | + ? `${String(bestHour).padStart(2, '0')}:00-${String(bestHour + 1).padStart(2, '0')}:00` | |
| 1704 | + : '暂无数据' | |
| 1705 | + | |
| 1706 | + // 2. 高价值会员(客单价超过1000的会员数,这里用估算) | |
| 1707 | + const avgAmountPerPerson = this.storeData.AvgAmountPerPerson || 0 | |
| 1708 | + const headCount = this.storeData.HeadCount || 0 | |
| 1709 | + const highValueMemberCount = avgAmountPerPerson > 1000 | |
| 1710 | + ? Math.round(headCount * 0.3) // 估算30%为高价值会员 | |
| 1711 | + : 0 | |
| 1712 | + | |
| 1713 | + // 3. 项目转化率(拓客转化为开单的比例) | |
| 1714 | + const conversionRate = this.funnelData && this.funnelData.ExpansionCount > 0 | |
| 1715 | + ? ((this.funnelData.BillingCount / this.funnelData.ExpansionCount) * 100).toFixed(1) | |
| 1716 | + : '0.0' | |
| 1717 | + | |
| 1718 | + // 4. 复购周期(估算,基于平均项目数和人均项目数) | |
| 1719 | + const avgProjectPerHead = this.storeData.AvgProjectPerHead || 0 | |
| 1720 | + const repurchaseCycle = avgProjectPerHead > 0 | |
| 1721 | + ? Math.round(30 / avgProjectPerHead) + '天' | |
| 1722 | + : '暂无' | |
| 1723 | + | |
| 1724 | + this.dataInsights = [ | |
| 1725 | + { | |
| 1726 | + title: '最佳营业时段', | |
| 1727 | + tag: maxPersonCount > 0 ? '热门' : '暂无', | |
| 1728 | + tagType: maxPersonCount > 0 ? 'danger' : 'info', | |
| 1729 | + value: bestTimeRange, | |
| 1730 | + desc: maxPersonCount > 0 ? `此时段客流量最高(${maxPersonCount}人次),建议配置更多人手` : '暂无数据' | |
| 1731 | + }, | |
| 1732 | + { | |
| 1733 | + title: '高价值会员', | |
| 1734 | + tag: highValueMemberCount > 0 ? '重点' : '暂无', | |
| 1735 | + tagType: highValueMemberCount > 0 ? 'warning' : 'info', | |
| 1736 | + value: highValueMemberCount > 0 ? `${highValueMemberCount}人` : '0人', | |
| 1737 | + desc: highValueMemberCount > 0 ? `单次消费超过¥1000,需重点维护` : '暂无数据' | |
| 1738 | + }, | |
| 1739 | + { | |
| 1740 | + title: '项目转化率', | |
| 1741 | + tag: parseFloat(conversionRate) > 50 ? '优秀' : parseFloat(conversionRate) > 30 ? '良好' : '待提升', | |
| 1742 | + tagType: parseFloat(conversionRate) > 50 ? 'success' : parseFloat(conversionRate) > 30 ? 'warning' : 'info', | |
| 1743 | + value: conversionRate + '%', | |
| 1744 | + desc: `拓客转化为开单的比例` | |
| 1745 | + }, | |
| 1746 | + { | |
| 1747 | + title: '复购周期', | |
| 1748 | + tag: repurchaseCycle !== '暂无' ? '正常' : '暂无', | |
| 1749 | + tagType: repurchaseCycle !== '暂无' ? 'info' : 'info', | |
| 1750 | + value: repurchaseCycle, | |
| 1751 | + desc: repurchaseCycle !== '暂无' ? `会员平均复购间隔,保持稳定` : '暂无数据' | |
| 1752 | + } | |
| 1753 | + ] | |
| 1754 | + }, | |
| 1755 | + // 更新本月关键指标 | |
| 1756 | + updateKeyMetrics() { | |
| 1757 | + if (!this.storeData) { | |
| 1758 | + this.keyMetrics = [ | |
| 1759 | + { label: '目标完成度', value: 0, color: '#67C23A' }, | |
| 1760 | + { label: '会员活跃度', value: 0, color: '#409EFF' }, | |
| 1761 | + { label: '项目满意度', value: 0, color: '#E6A23C' }, | |
| 1762 | + { label: '员工效率', value: 0, color: '#F56C6C' } | |
| 1763 | + ] | |
| 1764 | + return | |
| 1765 | + } | |
| 1766 | + | |
| 1767 | + // 1. 目标完成度(消耗业绩/目标业绩) | |
| 1768 | + const targetPerformance = this.storeData.TargetPerformance || 0 | |
| 1769 | + const consumePerformance = this.storeData.ConsumePerformance || 0 | |
| 1770 | + const completionRate = targetPerformance > 0 | |
| 1771 | + ? Math.min(100, parseFloat((consumePerformance / targetPerformance * 100).toFixed(1))) | |
| 1772 | + : 0 | |
| 1773 | + | |
| 1774 | + // 2. 会员活跃度(活跃会员/总会员数) | |
| 1775 | + const activeMemberRate = this.memberData && this.memberData.TotalMembers > 0 | |
| 1776 | + ? parseFloat((this.memberData.ActiveMemberRate || 0).toFixed(1)) | |
| 1777 | + : 0 | |
| 1778 | + | |
| 1779 | + // 3. 项目满意度(估算,基于退卡率,退卡率越低满意度越高) | |
| 1780 | + const billingPerformance = this.storeData.BillingPerformance || 0 | |
| 1781 | + const refundAmount = this.storeData.RefundAmount || 0 | |
| 1782 | + const refundRate = billingPerformance > 0 | |
| 1783 | + ? (refundAmount / billingPerformance * 100) | |
| 1784 | + : 0 | |
| 1785 | + const satisfactionRate = Math.max(0, Math.min(100, parseFloat((100 - refundRate * 10).toFixed(1)))) | |
| 1786 | + | |
| 1787 | + // 4. 员工效率(基于人均项目数,估算) | |
| 1788 | + const avgProjectPerHead = this.storeData.AvgProjectPerHead || 0 | |
| 1789 | + const efficiencyRate = Math.min(100, parseFloat((avgProjectPerHead * 10).toFixed(1))) | |
| 1790 | + | |
| 1791 | + this.keyMetrics = [ | |
| 1792 | + { label: '目标完成度', value: completionRate, color: completionRate >= 100 ? '#67C23A' : completionRate >= 80 ? '#E6A23C' : '#F56C6C' }, | |
| 1793 | + { label: '会员活跃度', value: activeMemberRate, color: activeMemberRate >= 60 ? '#67C23A' : activeMemberRate >= 40 ? '#409EFF' : '#909399' }, | |
| 1794 | + { label: '项目满意度', value: satisfactionRate, color: satisfactionRate >= 90 ? '#67C23A' : satisfactionRate >= 70 ? '#E6A23C' : '#F56C6C' }, | |
| 1795 | + { label: '员工效率', value: efficiencyRate, color: efficiencyRate >= 80 ? '#67C23A' : efficiencyRate >= 60 ? '#409EFF' : '#F56C6C' } | |
| 1796 | + ] | |
| 1797 | + }, | |
| 1798 | + // 更新本月经营提示 | |
| 1799 | + updateOperationTips() { | |
| 1800 | + if (!this.storeData) { | |
| 1801 | + this.operationTips = [] | |
| 1802 | + return | |
| 1803 | + } | |
| 1804 | + | |
| 1805 | + const tips = [] | |
| 1806 | + | |
| 1807 | + // 1. 目标完成度提示 | |
| 1808 | + const targetPerformance = this.storeData.TargetPerformance || 0 | |
| 1809 | + const consumePerformance = this.storeData.ConsumePerformance || 0 | |
| 1810 | + const completionRate = targetPerformance > 0 | |
| 1811 | + ? (consumePerformance / targetPerformance * 100) | |
| 1812 | + : 0 | |
| 1813 | + if (completionRate >= 100) { | |
| 1814 | + tips.push({ type: 'success', text: `本月业绩完成度${completionRate.toFixed(1)}%,超额完成目标,继续保持` }) | |
| 1815 | + } else if (completionRate >= 80) { | |
| 1816 | + tips.push({ type: 'success', text: `本月业绩完成度${completionRate.toFixed(1)}%,保持当前节奏` }) | |
| 1817 | + } else if (completionRate >= 60) { | |
| 1818 | + tips.push({ type: 'warning', text: `本月业绩完成度${completionRate.toFixed(1)}%,需加快进度` }) | |
| 1819 | + } else { | |
| 1820 | + tips.push({ type: 'danger', text: `本月业绩完成度${completionRate.toFixed(1)}%,严重滞后,需立即采取措施` }) | |
| 1821 | + } | |
| 1822 | + | |
| 1823 | + // 2. 沉睡会员提示 | |
| 1824 | + if (this.memberData) { | |
| 1825 | + const sleepMemberRate = this.memberData.SleepMemberRate || 0 | |
| 1826 | + if (sleepMemberRate > 20) { | |
| 1827 | + tips.push({ type: 'warning', text: `沉睡会员占比${sleepMemberRate.toFixed(1)}%,建议加强会员唤醒` }) | |
| 1828 | + } | |
| 1829 | + } | |
| 1830 | + | |
| 1831 | + // 3. 客单价提示 | |
| 1832 | + const avgAmountPerPerson = this.storeData.AvgAmountPerPerson || 0 | |
| 1833 | + if (avgAmountPerPerson > 0) { | |
| 1834 | + if (avgAmountPerPerson < 300) { | |
| 1835 | + tips.push({ type: 'info', text: `客单价¥${avgAmountPerPerson.toFixed(0)},可通过项目组合提升` }) | |
| 1836 | + } else if (avgAmountPerPerson > 800) { | |
| 1837 | + tips.push({ type: 'success', text: `客单价¥${avgAmountPerPerson.toFixed(0)},表现优秀` }) | |
| 1838 | + } | |
| 1839 | + } | |
| 1840 | + | |
| 1841 | + // 4. 退卡金额提示 | |
| 1842 | + const billingPerformance = this.storeData.BillingPerformance || 0 | |
| 1843 | + const refundAmount = this.storeData.RefundAmount || 0 | |
| 1844 | + if (refundAmount > 0 && billingPerformance > 0) { | |
| 1845 | + const refundRate = (refundAmount / billingPerformance * 100) | |
| 1846 | + if (refundRate > 5) { | |
| 1847 | + tips.push({ type: 'warning', text: `退卡金额${this.formatMoney(refundAmount)},退卡率${refundRate.toFixed(1)}%,需关注服务质量` }) | |
| 1848 | + } | |
| 1849 | + } | |
| 1850 | + | |
| 1851 | + // 5. 如果提示少于4条,补充一些通用提示 | |
| 1852 | + if (tips.length < 4) { | |
| 1853 | + const headCount = this.storeData.HeadCount || 0 | |
| 1854 | + const personCount = this.storeData.PersonCount || 0 | |
| 1855 | + if (headCount > 0) { | |
| 1856 | + tips.push({ type: 'info', text: `本月服务${headCount}位会员,${personCount}人次` }) | |
| 1857 | + } | |
| 1858 | + } | |
| 1859 | + | |
| 1860 | + this.operationTips = tips.slice(0, 4) // 最多显示4条 | |
| 1861 | + }, | |
| 1862 | + // 绘制所有图表 | |
| 1863 | + drawCharts() { | |
| 1864 | + // 使用 nextTick 确保 DOM 渲染完成 | |
| 1865 | + this.$nextTick(() => { | |
| 1866 | + setTimeout(() => { | |
| 1867 | + // 绘制业绩趋势折线图 | |
| 1868 | + if (this.trendData && this.trendData.length > 0) { | |
| 1869 | + const categories = this.trendData.map(item => { | |
| 1870 | + const month = item.Month || '' | |
| 1871 | + if (month.length === 6) { | |
| 1872 | + return month.substring(0, 4) + '-' + month.substring(4, 6) | |
| 1873 | + } | |
| 1874 | + return month | |
| 1875 | + }) | |
| 1876 | + const series = [ | |
| 1877 | + { | |
| 1878 | + name: '开单业绩', | |
| 1879 | + data: this.trendData.map(item => item.BillingPerformance || 0), | |
| 1880 | + color: '#409EFF' | |
| 1881 | + }, | |
| 1882 | + { | |
| 1883 | + name: '消耗业绩', | |
| 1884 | + data: this.trendData.map(item => item.ConsumePerformance || 0), | |
| 1885 | + color: '#67C23A' | |
| 1886 | + }, | |
| 1887 | + { | |
| 1888 | + name: '净业绩', | |
| 1889 | + data: this.trendData.map(item => item.NetPerformance || 0), | |
| 1890 | + color: '#E6A23C' | |
| 1891 | + } | |
| 1892 | + ] | |
| 1893 | + ChartUtil.drawLineChart(this, 'trendChart', categories, series) | |
| 1894 | + } | |
| 1895 | + | |
| 1896 | + // 绘制业绩对比柱状图 | |
| 1897 | + if (this.trendData && this.trendData.length > 0) { | |
| 1898 | + const categories = this.trendData.map(item => { | |
| 1899 | + const month = item.Month || '' | |
| 1900 | + if (month.length === 6) { | |
| 1901 | + const monthNum = parseInt(month.substring(4, 6)) | |
| 1902 | + return monthNum + '月' | |
| 1903 | + } | |
| 1904 | + return month | |
| 1905 | + }) | |
| 1906 | + const series = [ | |
| 1907 | + { | |
| 1908 | + name: '开单业绩', | |
| 1909 | + data: this.trendData.map(item => (item.BillingPerformance || 0) / 10000), | |
| 1910 | + color: '#409EFF' | |
| 1911 | + }, | |
| 1912 | + { | |
| 1913 | + name: '消耗业绩', | |
| 1914 | + data: this.trendData.map(item => (item.ConsumePerformance || 0) / 10000), | |
| 1915 | + color: '#67C23A' | |
| 1916 | + } | |
| 1917 | + ] | |
| 1918 | + ChartUtil.drawColumnChart(this, 'compareChart', categories, series) | |
| 1919 | + } | |
| 1920 | + | |
| 1921 | + // 绘制品项分类占比饼图 | |
| 1922 | + if (this.categoryData && this.categoryData.length > 0) { | |
| 1923 | + const pieData = this.categoryData.map((item, index) => ({ | |
| 1924 | + name: item.CategoryName || '其他', | |
| 1925 | + value: item.ConsumeAmount || 0, | |
| 1926 | + color: this.getCategoryColor(item.CategoryName) | |
| 1927 | + })) | |
| 1928 | + ChartUtil.drawPieChart(this, 'categoryChart', pieData) | |
| 1929 | + } | |
| 1930 | + | |
| 1931 | + // 绘制各分类业绩堆叠柱状图 | |
| 1932 | + if (this.categoryMonthlyData && this.categoryMonthlyData.length > 0) { | |
| 1933 | + const displayData = this.displayCategoryMonthlyData | |
| 1934 | + const categories = displayData.map(item => { | |
| 1935 | + const month = item.Month || '' | |
| 1936 | + if (month.length === 6) { | |
| 1937 | + const monthNum = parseInt(month.substring(4, 6)) | |
| 1938 | + return monthNum + '月' | |
| 1939 | + } | |
| 1940 | + return month | |
| 1941 | + }) | |
| 1942 | + const series = [ | |
| 1943 | + { | |
| 1944 | + name: '生美', | |
| 1945 | + data: displayData.map(item => (item.BeautyPerformance || 0) / 10000), | |
| 1946 | + color: '#A8D5E2' | |
| 1947 | + }, | |
| 1948 | + { | |
| 1949 | + name: '医美', | |
| 1950 | + data: displayData.map(item => (item.MedicalPerformance || 0) / 10000), | |
| 1951 | + color: '#B8E6B8' | |
| 1952 | + }, | |
| 1953 | + { | |
| 1954 | + name: '科美', | |
| 1955 | + data: displayData.map(item => (item.TechPerformance || 0) / 10000), | |
| 1956 | + color: '#FFD4A3' | |
| 1957 | + }, | |
| 1958 | + { | |
| 1959 | + name: '产品', | |
| 1960 | + data: displayData.map(item => (item.ProductPerformance || 0) / 10000), | |
| 1961 | + color: '#E6C1E6' | |
| 1962 | + } | |
| 1963 | + ] | |
| 1964 | + ChartUtil.drawColumnChart(this, 'stackedChart', categories, series) | |
| 1965 | + } | |
| 1966 | + | |
| 1967 | + // 绘制拓客转化漏斗图 | |
| 1968 | + if (this.funnelData) { | |
| 1969 | + const funnelChartData = [ | |
| 1970 | + { name: '拓客', value: this.funnelData.ExpansionCount || 0, color: '#409EFF' }, | |
| 1971 | + { name: '邀约', value: this.funnelData.InviteCount || 0, color: '#67C23A' }, | |
| 1972 | + { name: '预约', value: this.funnelData.AppointmentCount || 0, color: '#E6A23C' }, | |
| 1973 | + { name: '开单', value: this.funnelData.BillingCount || 0, color: '#909399' } | |
| 1974 | + ] | |
| 1975 | + ChartUtil.drawFunnelChart(this, 'funnelChart', funnelChartData) | |
| 1976 | + } | |
| 1977 | + | |
| 1978 | + // 绘制客单价与项目数关系散点图 | |
| 1979 | + if (this.scatterData && this.scatterData.length > 0) { | |
| 1980 | + ChartUtil.drawScatterChart(this, 'scatterChart', this.scatterData.slice(0, 20)) | |
| 1981 | + } | |
| 1982 | + | |
| 1983 | + // 绘制一周运营热力图 | |
| 1984 | + if (this.heatmapData && this.heatmapData.length > 0) { | |
| 1985 | + ChartUtil.drawHeatmapChart(this, 'heatmapChart', this.heatmapData) | |
| 1986 | + } | |
| 1987 | + | |
| 1988 | + // 绘制目标完成度仪表盘 | |
| 1989 | + if (this.storeData) { | |
| 1990 | + const completionRate = this.storeData.CompletionRate || 0 | |
| 1991 | + ChartUtil.drawGaugeChart(this, 'gaugeChart', completionRate) | |
| 1992 | + } | |
| 1993 | + }, 500) | |
| 1994 | + }) | |
| 1995 | + } | |
| 1996 | + } | |
| 1997 | + } | |
| 1998 | +</script> | |
| 1999 | + | |
| 2000 | +<style lang="scss" scoped> | |
| 2001 | + .store-dashboard { | |
| 2002 | + min-height: 100vh; | |
| 2003 | + background: #f5f7fa; | |
| 2004 | + } | |
| 2005 | + | |
| 2006 | + .filter-bar { | |
| 2007 | + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| 2008 | + padding: 32rpx 24rpx; | |
| 2009 | + margin-bottom: 20rpx; | |
| 2010 | + box-shadow: 0 4rpx 20rpx rgba(102, 126, 234, 0.3); | |
| 2011 | + | |
| 2012 | + .filter-item { | |
| 2013 | + margin-bottom: 24rpx; | |
| 2014 | + | |
| 2015 | + &:last-of-type { | |
| 2016 | + margin-bottom: 0; | |
| 2017 | + } | |
| 2018 | + | |
| 2019 | + .filter-label { | |
| 2020 | + display: block; | |
| 2021 | + font-size: 26rpx; | |
| 2022 | + color: rgba(255, 255, 255, 0.9); | |
| 2023 | + margin-bottom: 12rpx; | |
| 2024 | + font-weight: 500; | |
| 2025 | + } | |
| 2026 | + | |
| 2027 | + .picker-view { | |
| 2028 | + display: flex; | |
| 2029 | + align-items: center; | |
| 2030 | + justify-content: space-between; | |
| 2031 | + padding: 24rpx; | |
| 2032 | + background: rgba(255, 255, 255, 0.95); | |
| 2033 | + border-radius: 16rpx; | |
| 2034 | + font-size: 28rpx; | |
| 2035 | + color: #303133; | |
| 2036 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); | |
| 2037 | + transition: all 0.3s; | |
| 2038 | + | |
| 2039 | + &:active { | |
| 2040 | + background: rgba(255, 255, 255, 1); | |
| 2041 | + transform: scale(0.98); | |
| 2042 | + } | |
| 2043 | + | |
| 2044 | + text { | |
| 2045 | + flex: 1; | |
| 2046 | + } | |
| 2047 | + } | |
| 2048 | + } | |
| 2049 | + | |
| 2050 | + .filter-actions { | |
| 2051 | + display: flex; | |
| 2052 | + gap: 20rpx; | |
| 2053 | + margin-top: 24rpx; | |
| 2054 | + | |
| 2055 | + ::v-deep .u-btn { | |
| 2056 | + flex: 1; | |
| 2057 | + height: 80rpx; | |
| 2058 | + border-radius: 16rpx; | |
| 2059 | + font-size: 28rpx; | |
| 2060 | + font-weight: 600; | |
| 2061 | + } | |
| 2062 | + } | |
| 2063 | + } | |
| 2064 | + | |
| 2065 | + .loading-wrapper { | |
| 2066 | + display: flex; | |
| 2067 | + flex-direction: column; | |
| 2068 | + align-items: center; | |
| 2069 | + justify-content: center; | |
| 2070 | + padding: 200rpx 0; | |
| 2071 | + min-height: 60vh; | |
| 2072 | + | |
| 2073 | + .loading-text { | |
| 2074 | + margin-top: 24rpx; | |
| 2075 | + font-size: 28rpx; | |
| 2076 | + color: #909399; | |
| 2077 | + font-weight: 500; | |
| 2078 | + } | |
| 2079 | + } | |
| 2080 | + | |
| 2081 | + .content-scroll { | |
| 2082 | + height: calc(100vh - 200rpx); | |
| 2083 | + } | |
| 2084 | + | |
| 2085 | + .store-info-card { | |
| 2086 | + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); | |
| 2087 | + margin: 0 24rpx 20rpx; | |
| 2088 | + border-radius: 24rpx; | |
| 2089 | + padding: 32rpx; | |
| 2090 | + box-shadow: 0 8rpx 24rpx rgba(67, 233, 123, 0.25); | |
| 2091 | + position: relative; | |
| 2092 | + overflow: hidden; | |
| 2093 | + | |
| 2094 | + &::before { | |
| 2095 | + content: ''; | |
| 2096 | + position: absolute; | |
| 2097 | + top: -50%; | |
| 2098 | + right: -50%; | |
| 2099 | + width: 200%; | |
| 2100 | + height: 200%; | |
| 2101 | + background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%); | |
| 2102 | + pointer-events: none; | |
| 2103 | + } | |
| 2104 | + | |
| 2105 | + .store-header { | |
| 2106 | + display: flex; | |
| 2107 | + align-items: center; | |
| 2108 | + gap: 24rpx; | |
| 2109 | + position: relative; | |
| 2110 | + z-index: 1; | |
| 2111 | + | |
| 2112 | + .store-avatar { | |
| 2113 | + width: 120rpx; | |
| 2114 | + height: 120rpx; | |
| 2115 | + border-radius: 24rpx; | |
| 2116 | + background: rgba(255, 255, 255, 0.25); | |
| 2117 | + backdrop-filter: blur(10rpx); | |
| 2118 | + display: flex; | |
| 2119 | + align-items: center; | |
| 2120 | + justify-content: center; | |
| 2121 | + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); | |
| 2122 | + } | |
| 2123 | + | |
| 2124 | + .store-details { | |
| 2125 | + flex: 1; | |
| 2126 | + | |
| 2127 | + .store-name { | |
| 2128 | + font-size: 40rpx; | |
| 2129 | + font-weight: 700; | |
| 2130 | + color: #fff; | |
| 2131 | + margin-bottom: 16rpx; | |
| 2132 | + text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); | |
| 2133 | + } | |
| 2134 | + | |
| 2135 | + .store-meta { | |
| 2136 | + display: flex; | |
| 2137 | + flex-wrap: wrap; | |
| 2138 | + gap: 20rpx; | |
| 2139 | + font-size: 24rpx; | |
| 2140 | + color: rgba(255, 255, 255, 0.9); | |
| 2141 | + | |
| 2142 | + .meta-item { | |
| 2143 | + display: flex; | |
| 2144 | + align-items: center; | |
| 2145 | + background: rgba(255, 255, 255, 0.2); | |
| 2146 | + padding: 8rpx 16rpx; | |
| 2147 | + border-radius: 20rpx; | |
| 2148 | + backdrop-filter: blur(10rpx); | |
| 2149 | + } | |
| 2150 | + } | |
| 2151 | + } | |
| 2152 | + } | |
| 2153 | + } | |
| 2154 | + | |
| 2155 | + .core-stats-card, | |
| 2156 | + .section-card { | |
| 2157 | + background: #fff; | |
| 2158 | + border-radius: 24rpx; | |
| 2159 | + margin: 0 24rpx 24rpx; | |
| 2160 | + padding: 32rpx; | |
| 2161 | + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06); | |
| 2162 | + transition: all 0.3s; | |
| 2163 | + | |
| 2164 | + &:active { | |
| 2165 | + transform: translateY(-2rpx); | |
| 2166 | + box-shadow: 0 6rpx 24rpx rgba(0, 0, 0, 0.1); | |
| 2167 | + } | |
| 2168 | + | |
| 2169 | + .card-title { | |
| 2170 | + display: flex; | |
| 2171 | + align-items: center; | |
| 2172 | + gap: 12rpx; | |
| 2173 | + font-size: 32rpx; | |
| 2174 | + font-weight: 700; | |
| 2175 | + color: #303133; | |
| 2176 | + margin-bottom: 28rpx; | |
| 2177 | + padding-bottom: 20rpx; | |
| 2178 | + border-bottom: 2rpx solid #f0f2f5; | |
| 2179 | + | |
| 2180 | + i { | |
| 2181 | + color: #409EFF; | |
| 2182 | + } | |
| 2183 | + } | |
| 2184 | + } | |
| 2185 | + | |
| 2186 | + .stats-grid { | |
| 2187 | + display: grid; | |
| 2188 | + grid-template-columns: repeat(2, 1fr); | |
| 2189 | + gap: 20rpx; | |
| 2190 | + | |
| 2191 | + .stat-item { | |
| 2192 | + padding: 28rpx 24rpx; | |
| 2193 | + border-radius: 20rpx; | |
| 2194 | + text-align: center; | |
| 2195 | + position: relative; | |
| 2196 | + overflow: hidden; | |
| 2197 | + transition: all 0.3s; | |
| 2198 | + | |
| 2199 | + &::before { | |
| 2200 | + content: ''; | |
| 2201 | + position: absolute; | |
| 2202 | + top: 0; | |
| 2203 | + left: 0; | |
| 2204 | + right: 0; | |
| 2205 | + height: 6rpx; | |
| 2206 | + background: currentColor; | |
| 2207 | + opacity: 0.3; | |
| 2208 | + } | |
| 2209 | + | |
| 2210 | + &:active { | |
| 2211 | + transform: scale(0.98); | |
| 2212 | + } | |
| 2213 | + | |
| 2214 | + &.primary { | |
| 2215 | + background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%); | |
| 2216 | + border-left: 6rpx solid #409EFF; | |
| 2217 | + color: #409EFF; | |
| 2218 | + } | |
| 2219 | + | |
| 2220 | + &.success { | |
| 2221 | + background: linear-gradient(135deg, #f0f9ff 0%, #e1f3ff 100%); | |
| 2222 | + border-left: 6rpx solid #67C23A; | |
| 2223 | + color: #67C23A; | |
| 2224 | + } | |
| 2225 | + | |
| 2226 | + &.info { | |
| 2227 | + background: linear-gradient(135deg, #f4f4f5 0%, #e9e9eb 100%); | |
| 2228 | + border-left: 6rpx solid #909399; | |
| 2229 | + color: #909399; | |
| 2230 | + } | |
| 2231 | + | |
| 2232 | + &.warning { | |
| 2233 | + background: linear-gradient(135deg, #fdf6ec 0%, #fae6d3 100%); | |
| 2234 | + border-left: 6rpx solid #E6A23C; | |
| 2235 | + color: #E6A23C; | |
| 2236 | + } | |
| 2237 | + | |
| 2238 | + .stat-label { | |
| 2239 | + font-size: 24rpx; | |
| 2240 | + color: #606266; | |
| 2241 | + margin-bottom: 16rpx; | |
| 2242 | + font-weight: 500; | |
| 2243 | + } | |
| 2244 | + | |
| 2245 | + .stat-value { | |
| 2246 | + font-size: 36rpx; | |
| 2247 | + font-weight: 700; | |
| 2248 | + color: #303133; | |
| 2249 | + word-break: break-all; | |
| 2250 | + } | |
| 2251 | + } | |
| 2252 | + } | |
| 2253 | + | |
| 2254 | + .metrics-list { | |
| 2255 | + .metric-row { | |
| 2256 | + display: flex; | |
| 2257 | + gap: 20rpx; | |
| 2258 | + margin-bottom: 20rpx; | |
| 2259 | + | |
| 2260 | + &:last-child { | |
| 2261 | + margin-bottom: 0; | |
| 2262 | + } | |
| 2263 | + | |
| 2264 | + .metric-item { | |
| 2265 | + flex: 1; | |
| 2266 | + padding: 24rpx; | |
| 2267 | + background: linear-gradient(135deg, #f8f9fa 0%, #f0f2f5 100%); | |
| 2268 | + border-radius: 16rpx; | |
| 2269 | + border: 1rpx solid #e9ecef; | |
| 2270 | + transition: all 0.3s; | |
| 2271 | + | |
| 2272 | + &:active { | |
| 2273 | + background: linear-gradient(135deg, #f0f2f5 0%, #e9ecef 100%); | |
| 2274 | + transform: translateY(-2rpx); | |
| 2275 | + } | |
| 2276 | + | |
| 2277 | + .metric-label { | |
| 2278 | + display: block; | |
| 2279 | + font-size: 24rpx; | |
| 2280 | + color: #909399; | |
| 2281 | + margin-bottom: 12rpx; | |
| 2282 | + font-weight: 500; | |
| 2283 | + } | |
| 2284 | + | |
| 2285 | + .metric-value { | |
| 2286 | + display: block; | |
| 2287 | + font-size: 30rpx; | |
| 2288 | + font-weight: 700; | |
| 2289 | + color: #303133; | |
| 2290 | + margin-bottom: 4rpx; | |
| 2291 | + } | |
| 2292 | + | |
| 2293 | + .metric-rate { | |
| 2294 | + display: block; | |
| 2295 | + font-size: 22rpx; | |
| 2296 | + color: #67C23A; | |
| 2297 | + margin-top: 6rpx; | |
| 2298 | + font-weight: 500; | |
| 2299 | + } | |
| 2300 | + } | |
| 2301 | + } | |
| 2302 | + } | |
| 2303 | + | |
| 2304 | + .trend-list { | |
| 2305 | + .trend-item { | |
| 2306 | + padding: 28rpx; | |
| 2307 | + margin-bottom: 20rpx; | |
| 2308 | + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); | |
| 2309 | + border-radius: 20rpx; | |
| 2310 | + border: 1rpx solid #e9ecef; | |
| 2311 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); | |
| 2312 | + transition: all 0.3s; | |
| 2313 | + | |
| 2314 | + &:last-child { | |
| 2315 | + margin-bottom: 0; | |
| 2316 | + } | |
| 2317 | + | |
| 2318 | + &:active { | |
| 2319 | + transform: translateY(-2rpx); | |
| 2320 | + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); | |
| 2321 | + } | |
| 2322 | + | |
| 2323 | + .trend-month { | |
| 2324 | + font-size: 30rpx; | |
| 2325 | + font-weight: 700; | |
| 2326 | + color: #303133; | |
| 2327 | + margin-bottom: 20rpx; | |
| 2328 | + padding-bottom: 16rpx; | |
| 2329 | + border-bottom: 2rpx dashed #e9ecef; | |
| 2330 | + } | |
| 2331 | + | |
| 2332 | + .trend-values { | |
| 2333 | + display: flex; | |
| 2334 | + flex-direction: column; | |
| 2335 | + gap: 16rpx; | |
| 2336 | + | |
| 2337 | + .trend-value-item { | |
| 2338 | + display: flex; | |
| 2339 | + justify-content: space-between; | |
| 2340 | + align-items: center; | |
| 2341 | + padding: 12rpx 0; | |
| 2342 | + | |
| 2343 | + .trend-label { | |
| 2344 | + font-size: 26rpx; | |
| 2345 | + color: #606266; | |
| 2346 | + font-weight: 500; | |
| 2347 | + } | |
| 2348 | + | |
| 2349 | + .trend-value { | |
| 2350 | + font-size: 30rpx; | |
| 2351 | + font-weight: 700; | |
| 2352 | + | |
| 2353 | + &.primary { | |
| 2354 | + color: #409EFF; | |
| 2355 | + } | |
| 2356 | + | |
| 2357 | + &.success { | |
| 2358 | + color: #67C23A; | |
| 2359 | + } | |
| 2360 | + | |
| 2361 | + &.warning { | |
| 2362 | + color: #E6A23C; | |
| 2363 | + } | |
| 2364 | + } | |
| 2365 | + } | |
| 2366 | + } | |
| 2367 | + } | |
| 2368 | + } | |
| 2369 | + | |
| 2370 | + .ranking-list { | |
| 2371 | + .ranking-item { | |
| 2372 | + display: flex; | |
| 2373 | + align-items: center; | |
| 2374 | + padding: 24rpx; | |
| 2375 | + margin-bottom: 20rpx; | |
| 2376 | + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); | |
| 2377 | + border-radius: 20rpx; | |
| 2378 | + border: 1rpx solid #e9ecef; | |
| 2379 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); | |
| 2380 | + transition: all 0.3s; | |
| 2381 | + | |
| 2382 | + &:last-child { | |
| 2383 | + margin-bottom: 0; | |
| 2384 | + } | |
| 2385 | + | |
| 2386 | + &:active { | |
| 2387 | + transform: translateX(4rpx); | |
| 2388 | + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); | |
| 2389 | + } | |
| 2390 | + | |
| 2391 | + .ranking-index { | |
| 2392 | + width: 72rpx; | |
| 2393 | + height: 72rpx; | |
| 2394 | + border-radius: 16rpx; | |
| 2395 | + display: flex; | |
| 2396 | + align-items: center; | |
| 2397 | + justify-content: center; | |
| 2398 | + font-size: 32rpx; | |
| 2399 | + font-weight: 700; | |
| 2400 | + margin-right: 24rpx; | |
| 2401 | + flex-shrink: 0; | |
| 2402 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); | |
| 2403 | + | |
| 2404 | + &.rank-first { | |
| 2405 | + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| 2406 | + color: #fff; | |
| 2407 | + } | |
| 2408 | + | |
| 2409 | + &.rank-second { | |
| 2410 | + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); | |
| 2411 | + color: #fff; | |
| 2412 | + } | |
| 2413 | + | |
| 2414 | + &.rank-third { | |
| 2415 | + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); | |
| 2416 | + color: #fff; | |
| 2417 | + } | |
| 2418 | + | |
| 2419 | + &.rank-normal { | |
| 2420 | + background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%); | |
| 2421 | + color: #606266; | |
| 2422 | + } | |
| 2423 | + } | |
| 2424 | + | |
| 2425 | + .ranking-info { | |
| 2426 | + flex: 1; | |
| 2427 | + min-width: 0; | |
| 2428 | + | |
| 2429 | + .ranking-name { | |
| 2430 | + font-size: 30rpx; | |
| 2431 | + font-weight: 700; | |
| 2432 | + color: #303133; | |
| 2433 | + margin-bottom: 12rpx; | |
| 2434 | + } | |
| 2435 | + | |
| 2436 | + .ranking-details { | |
| 2437 | + display: flex; | |
| 2438 | + flex-wrap: wrap; | |
| 2439 | + gap: 20rpx; | |
| 2440 | + | |
| 2441 | + .ranking-detail-item { | |
| 2442 | + font-size: 24rpx; | |
| 2443 | + color: #606266; | |
| 2444 | + background: #f0f2f5; | |
| 2445 | + padding: 6rpx 12rpx; | |
| 2446 | + border-radius: 8rpx; | |
| 2447 | + } | |
| 2448 | + } | |
| 2449 | + } | |
| 2450 | + } | |
| 2451 | + } | |
| 2452 | + | |
| 2453 | + .item-list { | |
| 2454 | + .item-row { | |
| 2455 | + display: flex; | |
| 2456 | + align-items: center; | |
| 2457 | + padding: 24rpx; | |
| 2458 | + margin-bottom: 20rpx; | |
| 2459 | + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); | |
| 2460 | + border-radius: 20rpx; | |
| 2461 | + border: 1rpx solid #e9ecef; | |
| 2462 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); | |
| 2463 | + transition: all 0.3s; | |
| 2464 | + | |
| 2465 | + &:last-child { | |
| 2466 | + margin-bottom: 0; | |
| 2467 | + } | |
| 2468 | + | |
| 2469 | + &:active { | |
| 2470 | + transform: translateX(4rpx); | |
| 2471 | + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); | |
| 2472 | + } | |
| 2473 | + | |
| 2474 | + .item-index { | |
| 2475 | + width: 72rpx; | |
| 2476 | + height: 72rpx; | |
| 2477 | + border-radius: 16rpx; | |
| 2478 | + display: flex; | |
| 2479 | + align-items: center; | |
| 2480 | + justify-content: center; | |
| 2481 | + font-size: 32rpx; | |
| 2482 | + font-weight: 700; | |
| 2483 | + margin-right: 24rpx; | |
| 2484 | + flex-shrink: 0; | |
| 2485 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); | |
| 2486 | + | |
| 2487 | + &.rank-first { | |
| 2488 | + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| 2489 | + color: #fff; | |
| 2490 | + } | |
| 2491 | + | |
| 2492 | + &.rank-second { | |
| 2493 | + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); | |
| 2494 | + color: #fff; | |
| 2495 | + } | |
| 2496 | + | |
| 2497 | + &.rank-third { | |
| 2498 | + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); | |
| 2499 | + color: #fff; | |
| 2500 | + } | |
| 2501 | + | |
| 2502 | + &.rank-normal { | |
| 2503 | + background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%); | |
| 2504 | + color: #606266; | |
| 2505 | + } | |
| 2506 | + } | |
| 2507 | + | |
| 2508 | + .item-info { | |
| 2509 | + flex: 1; | |
| 2510 | + min-width: 0; | |
| 2511 | + | |
| 2512 | + .item-name { | |
| 2513 | + font-size: 30rpx; | |
| 2514 | + font-weight: 700; | |
| 2515 | + color: #303133; | |
| 2516 | + margin-bottom: 12rpx; | |
| 2517 | + } | |
| 2518 | + | |
| 2519 | + .item-details { | |
| 2520 | + display: flex; | |
| 2521 | + align-items: center; | |
| 2522 | + gap: 16rpx; | |
| 2523 | + flex-wrap: wrap; | |
| 2524 | + | |
| 2525 | + .item-amount { | |
| 2526 | + font-size: 28rpx; | |
| 2527 | + font-weight: 700; | |
| 2528 | + color: #67C23A; | |
| 2529 | + background: #f0f9ff; | |
| 2530 | + padding: 6rpx 12rpx; | |
| 2531 | + border-radius: 8rpx; | |
| 2532 | + } | |
| 2533 | + | |
| 2534 | + .item-count { | |
| 2535 | + font-size: 24rpx; | |
| 2536 | + color: #606266; | |
| 2537 | + background: #f8f9fa; | |
| 2538 | + padding: 6rpx 12rpx; | |
| 2539 | + border-radius: 8rpx; | |
| 2540 | + } | |
| 2541 | + } | |
| 2542 | + } | |
| 2543 | + } | |
| 2544 | + } | |
| 2545 | + | |
| 2546 | + .comparison-content { | |
| 2547 | + .comparison-rank { | |
| 2548 | + text-align: center; | |
| 2549 | + padding: 40rpx 0; | |
| 2550 | + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); | |
| 2551 | + border-radius: 20rpx; | |
| 2552 | + margin-bottom: 24rpx; | |
| 2553 | + | |
| 2554 | + .rank-label { | |
| 2555 | + display: block; | |
| 2556 | + font-size: 26rpx; | |
| 2557 | + color: #909399; | |
| 2558 | + margin-bottom: 20rpx; | |
| 2559 | + font-weight: 500; | |
| 2560 | + } | |
| 2561 | + | |
| 2562 | + .rank-value { | |
| 2563 | + margin-bottom: 20rpx; | |
| 2564 | + | |
| 2565 | + .rank-number { | |
| 2566 | + font-size: 72rpx; | |
| 2567 | + font-weight: 700; | |
| 2568 | + color: #409EFF; | |
| 2569 | + text-shadow: 0 2rpx 4rpx rgba(64, 158, 255, 0.2); | |
| 2570 | + } | |
| 2571 | + | |
| 2572 | + .rank-total { | |
| 2573 | + font-size: 36rpx; | |
| 2574 | + color: #909399; | |
| 2575 | + margin-left: 8rpx; | |
| 2576 | + font-weight: 500; | |
| 2577 | + } | |
| 2578 | + } | |
| 2579 | + } | |
| 2580 | + | |
| 2581 | + .comparison-stats { | |
| 2582 | + padding-top: 24rpx; | |
| 2583 | + | |
| 2584 | + .comparison-stat-row { | |
| 2585 | + display: flex; | |
| 2586 | + justify-content: space-between; | |
| 2587 | + align-items: center; | |
| 2588 | + padding: 24rpx 0; | |
| 2589 | + border-bottom: 2rpx dashed #ebeef5; | |
| 2590 | + transition: all 0.3s; | |
| 2591 | + | |
| 2592 | + &:last-child { | |
| 2593 | + border-bottom: none; | |
| 2594 | + } | |
| 2595 | + | |
| 2596 | + &:active { | |
| 2597 | + background: #f8f9fa; | |
| 2598 | + margin: 0 -32rpx; | |
| 2599 | + padding-left: 32rpx; | |
| 2600 | + padding-right: 32rpx; | |
| 2601 | + border-radius: 12rpx; | |
| 2602 | + } | |
| 2603 | + | |
| 2604 | + .stat-label { | |
| 2605 | + font-size: 26rpx; | |
| 2606 | + color: #606266; | |
| 2607 | + font-weight: 500; | |
| 2608 | + } | |
| 2609 | + | |
| 2610 | + .stat-value { | |
| 2611 | + font-size: 30rpx; | |
| 2612 | + font-weight: 700; | |
| 2613 | + color: #303133; | |
| 2614 | + } | |
| 2615 | + } | |
| 2616 | + } | |
| 2617 | + } | |
| 2618 | + | |
| 2619 | + // 品项分类占比 | |
| 2620 | + .category-list { | |
| 2621 | + .category-item { | |
| 2622 | + margin-bottom: 24rpx; | |
| 2623 | + | |
| 2624 | + &:last-child { | |
| 2625 | + margin-bottom: 0; | |
| 2626 | + } | |
| 2627 | + | |
| 2628 | + .category-header { | |
| 2629 | + display: flex; | |
| 2630 | + justify-content: space-between; | |
| 2631 | + align-items: center; | |
| 2632 | + margin-bottom: 12rpx; | |
| 2633 | + | |
| 2634 | + .category-name-row { | |
| 2635 | + display: flex; | |
| 2636 | + align-items: center; | |
| 2637 | + gap: 12rpx; | |
| 2638 | + flex: 1; | |
| 2639 | + | |
| 2640 | + .category-amount { | |
| 2641 | + font-size: 28rpx; | |
| 2642 | + font-weight: 700; | |
| 2643 | + color: #303133; | |
| 2644 | + } | |
| 2645 | + } | |
| 2646 | + | |
| 2647 | + .category-percent { | |
| 2648 | + font-size: 28rpx; | |
| 2649 | + font-weight: 700; | |
| 2650 | + color: #409EFF; | |
| 2651 | + } | |
| 2652 | + } | |
| 2653 | + | |
| 2654 | + .category-progress { | |
| 2655 | + height: 12rpx; | |
| 2656 | + background: #f0f2f5; | |
| 2657 | + border-radius: 6rpx; | |
| 2658 | + overflow: hidden; | |
| 2659 | + | |
| 2660 | + .progress-bar { | |
| 2661 | + height: 100%; | |
| 2662 | + border-radius: 6rpx; | |
| 2663 | + transition: width 0.3s; | |
| 2664 | + } | |
| 2665 | + } | |
| 2666 | + } | |
| 2667 | + } | |
| 2668 | + | |
| 2669 | + // 转化漏斗 | |
| 2670 | + .funnel-list { | |
| 2671 | + .funnel-item { | |
| 2672 | + margin-bottom: 24rpx; | |
| 2673 | + | |
| 2674 | + &:last-child { | |
| 2675 | + margin-bottom: 0; | |
| 2676 | + } | |
| 2677 | + | |
| 2678 | + .funnel-label-row { | |
| 2679 | + display: flex; | |
| 2680 | + justify-content: space-between; | |
| 2681 | + align-items: center; | |
| 2682 | + margin-bottom: 12rpx; | |
| 2683 | + | |
| 2684 | + .funnel-label { | |
| 2685 | + font-size: 28rpx; | |
| 2686 | + font-weight: 600; | |
| 2687 | + color: #303133; | |
| 2688 | + } | |
| 2689 | + | |
| 2690 | + .funnel-value { | |
| 2691 | + font-size: 28rpx; | |
| 2692 | + font-weight: 700; | |
| 2693 | + color: #409EFF; | |
| 2694 | + } | |
| 2695 | + } | |
| 2696 | + | |
| 2697 | + .funnel-bar-wrapper { | |
| 2698 | + position: relative; | |
| 2699 | + height: 40rpx; | |
| 2700 | + background: #f0f2f5; | |
| 2701 | + border-radius: 20rpx; | |
| 2702 | + overflow: hidden; | |
| 2703 | + | |
| 2704 | + .funnel-bar { | |
| 2705 | + height: 100%; | |
| 2706 | + border-radius: 20rpx; | |
| 2707 | + transition: width 0.3s; | |
| 2708 | + } | |
| 2709 | + | |
| 2710 | + .funnel-percent { | |
| 2711 | + position: absolute; | |
| 2712 | + right: 16rpx; | |
| 2713 | + top: 50%; | |
| 2714 | + transform: translateY(-50%); | |
| 2715 | + font-size: 24rpx; | |
| 2716 | + font-weight: 600; | |
| 2717 | + color: #606266; | |
| 2718 | + } | |
| 2719 | + } | |
| 2720 | + } | |
| 2721 | + } | |
| 2722 | + | |
| 2723 | + // 客单价与项目数关系 | |
| 2724 | + .scatter-list { | |
| 2725 | + .scatter-item { | |
| 2726 | + padding: 24rpx; | |
| 2727 | + margin-bottom: 16rpx; | |
| 2728 | + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); | |
| 2729 | + border-radius: 16rpx; | |
| 2730 | + border: 1rpx solid #e9ecef; | |
| 2731 | + | |
| 2732 | + &:last-child { | |
| 2733 | + margin-bottom: 0; | |
| 2734 | + } | |
| 2735 | + | |
| 2736 | + .scatter-header { | |
| 2737 | + display: flex; | |
| 2738 | + justify-content: space-between; | |
| 2739 | + align-items: center; | |
| 2740 | + margin-bottom: 12rpx; | |
| 2741 | + | |
| 2742 | + .scatter-label { | |
| 2743 | + font-size: 26rpx; | |
| 2744 | + font-weight: 600; | |
| 2745 | + color: #303133; | |
| 2746 | + } | |
| 2747 | + | |
| 2748 | + .scatter-count { | |
| 2749 | + font-size: 24rpx; | |
| 2750 | + color: #909399; | |
| 2751 | + background: #f0f2f5; | |
| 2752 | + padding: 4rpx 12rpx; | |
| 2753 | + border-radius: 12rpx; | |
| 2754 | + } | |
| 2755 | + } | |
| 2756 | + | |
| 2757 | + .scatter-details { | |
| 2758 | + display: flex; | |
| 2759 | + gap: 24rpx; | |
| 2760 | + | |
| 2761 | + .scatter-detail-item { | |
| 2762 | + flex: 1; | |
| 2763 | + display: flex; | |
| 2764 | + flex-direction: column; | |
| 2765 | + gap: 8rpx; | |
| 2766 | + | |
| 2767 | + .detail-label { | |
| 2768 | + font-size: 24rpx; | |
| 2769 | + color: #909399; | |
| 2770 | + } | |
| 2771 | + | |
| 2772 | + .detail-value { | |
| 2773 | + font-size: 28rpx; | |
| 2774 | + font-weight: 700; | |
| 2775 | + | |
| 2776 | + &.primary { | |
| 2777 | + color: #409EFF; | |
| 2778 | + } | |
| 2779 | + | |
| 2780 | + &.success { | |
| 2781 | + color: #67C23A; | |
| 2782 | + } | |
| 2783 | + } | |
| 2784 | + } | |
| 2785 | + } | |
| 2786 | + } | |
| 2787 | + } | |
| 2788 | + | |
| 2789 | + // 各分类业绩堆叠对比 | |
| 2790 | + .category-monthly-list { | |
| 2791 | + .category-monthly-item { | |
| 2792 | + padding: 24rpx; | |
| 2793 | + margin-bottom: 24rpx; | |
| 2794 | + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); | |
| 2795 | + border-radius: 16rpx; | |
| 2796 | + border: 1rpx solid #e9ecef; | |
| 2797 | + | |
| 2798 | + &:last-child { | |
| 2799 | + margin-bottom: 0; | |
| 2800 | + } | |
| 2801 | + | |
| 2802 | + .month-header { | |
| 2803 | + display: flex; | |
| 2804 | + justify-content: space-between; | |
| 2805 | + align-items: center; | |
| 2806 | + margin-bottom: 20rpx; | |
| 2807 | + padding-bottom: 16rpx; | |
| 2808 | + border-bottom: 2rpx dashed #e9ecef; | |
| 2809 | + | |
| 2810 | + .month-label { | |
| 2811 | + font-size: 30rpx; | |
| 2812 | + font-weight: 700; | |
| 2813 | + color: #303133; | |
| 2814 | + } | |
| 2815 | + | |
| 2816 | + .month-total { | |
| 2817 | + font-size: 26rpx; | |
| 2818 | + font-weight: 600; | |
| 2819 | + color: #409EFF; | |
| 2820 | + } | |
| 2821 | + } | |
| 2822 | + | |
| 2823 | + .category-bars { | |
| 2824 | + display: flex; | |
| 2825 | + flex-direction: column; | |
| 2826 | + gap: 16rpx; | |
| 2827 | + | |
| 2828 | + .category-bar-item { | |
| 2829 | + .bar-label-row { | |
| 2830 | + display: flex; | |
| 2831 | + justify-content: space-between; | |
| 2832 | + align-items: center; | |
| 2833 | + margin-bottom: 8rpx; | |
| 2834 | + | |
| 2835 | + .bar-label { | |
| 2836 | + font-size: 24rpx; | |
| 2837 | + color: #606266; | |
| 2838 | + } | |
| 2839 | + | |
| 2840 | + .bar-value { | |
| 2841 | + font-size: 26rpx; | |
| 2842 | + font-weight: 600; | |
| 2843 | + color: #303133; | |
| 2844 | + } | |
| 2845 | + } | |
| 2846 | + | |
| 2847 | + .bar-wrapper { | |
| 2848 | + height: 24rpx; | |
| 2849 | + background: #f0f2f5; | |
| 2850 | + border-radius: 12rpx; | |
| 2851 | + overflow: hidden; | |
| 2852 | + position: relative; | |
| 2853 | + | |
| 2854 | + .bar-fill { | |
| 2855 | + height: 100%; | |
| 2856 | + border-radius: 12rpx; | |
| 2857 | + transition: width 0.3s; | |
| 2858 | + } | |
| 2859 | + } | |
| 2860 | + } | |
| 2861 | + } | |
| 2862 | + } | |
| 2863 | + } | |
| 2864 | + | |
| 2865 | + // 一周运营热力图 | |
| 2866 | + .heatmap-content { | |
| 2867 | + .heatmap-grid { | |
| 2868 | + display: grid; | |
| 2869 | + grid-template-columns: repeat(7, 1fr); | |
| 2870 | + gap: 8rpx; | |
| 2871 | + | |
| 2872 | + .heatmap-cell { | |
| 2873 | + padding: 16rpx 8rpx; | |
| 2874 | + border-radius: 8rpx; | |
| 2875 | + text-align: center; | |
| 2876 | + display: flex; | |
| 2877 | + flex-direction: column; | |
| 2878 | + align-items: center; | |
| 2879 | + justify-content: center; | |
| 2880 | + min-height: 80rpx; | |
| 2881 | + transition: all 0.3s; | |
| 2882 | + | |
| 2883 | + &:active { | |
| 2884 | + transform: scale(1.05); | |
| 2885 | + } | |
| 2886 | + | |
| 2887 | + .cell-value { | |
| 2888 | + font-size: 24rpx; | |
| 2889 | + font-weight: 700; | |
| 2890 | + color: #303133; | |
| 2891 | + margin-bottom: 4rpx; | |
| 2892 | + } | |
| 2893 | + | |
| 2894 | + .cell-label { | |
| 2895 | + font-size: 20rpx; | |
| 2896 | + color: rgba(0, 0, 0, 0.6); | |
| 2897 | + } | |
| 2898 | + } | |
| 2899 | + } | |
| 2900 | + } | |
| 2901 | + | |
| 2902 | + // 目标完成度 | |
| 2903 | + .gauge-content { | |
| 2904 | + display: flex; | |
| 2905 | + flex-direction: column; | |
| 2906 | + align-items: center; | |
| 2907 | + gap: 32rpx; | |
| 2908 | + | |
| 2909 | + .gauge-circle { | |
| 2910 | + width: 200rpx; | |
| 2911 | + height: 200rpx; | |
| 2912 | + border-radius: 50%; | |
| 2913 | + display: flex; | |
| 2914 | + flex-direction: column; | |
| 2915 | + align-items: center; | |
| 2916 | + justify-content: center; | |
| 2917 | + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); | |
| 2918 | + | |
| 2919 | + .gauge-value { | |
| 2920 | + font-size: 48rpx; | |
| 2921 | + font-weight: 700; | |
| 2922 | + color: #fff; | |
| 2923 | + margin-bottom: 8rpx; | |
| 2924 | + } | |
| 2925 | + | |
| 2926 | + .gauge-label { | |
| 2927 | + font-size: 24rpx; | |
| 2928 | + color: rgba(255, 255, 255, 0.9); | |
| 2929 | + } | |
| 2930 | + } | |
| 2931 | + | |
| 2932 | + .gauge-info { | |
| 2933 | + width: 100%; | |
| 2934 | + display: flex; | |
| 2935 | + justify-content: space-around; | |
| 2936 | + | |
| 2937 | + .gauge-info-item { | |
| 2938 | + display: flex; | |
| 2939 | + flex-direction: column; | |
| 2940 | + align-items: center; | |
| 2941 | + gap: 8rpx; | |
| 2942 | + | |
| 2943 | + .info-label { | |
| 2944 | + font-size: 24rpx; | |
| 2945 | + color: #909399; | |
| 2946 | + } | |
| 2947 | + | |
| 2948 | + .info-value { | |
| 2949 | + font-size: 28rpx; | |
| 2950 | + font-weight: 700; | |
| 2951 | + color: #303133; | |
| 2952 | + } | |
| 2953 | + } | |
| 2954 | + } | |
| 2955 | + } | |
| 2956 | + | |
| 2957 | + // 本月经营提示 | |
| 2958 | + .tips-list { | |
| 2959 | + .tip-item { | |
| 2960 | + display: flex; | |
| 2961 | + align-items: flex-start; | |
| 2962 | + padding: 20rpx; | |
| 2963 | + margin-bottom: 16rpx; | |
| 2964 | + border-radius: 12rpx; | |
| 2965 | + border-left: 4rpx solid; | |
| 2966 | + transition: all 0.3s; | |
| 2967 | + | |
| 2968 | + &:last-child { | |
| 2969 | + margin-bottom: 0; | |
| 2970 | + } | |
| 2971 | + | |
| 2972 | + &:active { | |
| 2973 | + transform: translateX(4rpx); | |
| 2974 | + } | |
| 2975 | + | |
| 2976 | + &.success { | |
| 2977 | + background: #f0f9ff; | |
| 2978 | + border-left-color: #67C23A; | |
| 2979 | + } | |
| 2980 | + | |
| 2981 | + &.warning { | |
| 2982 | + background: #fdf6ec; | |
| 2983 | + border-left-color: #E6A23C; | |
| 2984 | + } | |
| 2985 | + | |
| 2986 | + &.danger { | |
| 2987 | + background: #fef0f0; | |
| 2988 | + border-left-color: #F56C6C; | |
| 2989 | + } | |
| 2990 | + | |
| 2991 | + &.info { | |
| 2992 | + background: #f4f4f5; | |
| 2993 | + border-left-color: #909399; | |
| 2994 | + } | |
| 2995 | + | |
| 2996 | + .tip-text { | |
| 2997 | + flex: 1; | |
| 2998 | + font-size: 26rpx; | |
| 2999 | + line-height: 1.6; | |
| 3000 | + margin-left: 12rpx; | |
| 3001 | + } | |
| 3002 | + } | |
| 3003 | + } | |
| 3004 | + | |
| 3005 | + // 快速数据洞察 | |
| 3006 | + .insight-list { | |
| 3007 | + .insight-item { | |
| 3008 | + padding: 24rpx; | |
| 3009 | + margin-bottom: 20rpx; | |
| 3010 | + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); | |
| 3011 | + border-radius: 16rpx; | |
| 3012 | + border-left: 4rpx solid #409EFF; | |
| 3013 | + transition: all 0.3s; | |
| 3014 | + | |
| 3015 | + &:last-child { | |
| 3016 | + margin-bottom: 0; | |
| 3017 | + } | |
| 3018 | + | |
| 3019 | + &:active { | |
| 3020 | + transform: translateX(4rpx); | |
| 3021 | + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); | |
| 3022 | + } | |
| 3023 | + | |
| 3024 | + .insight-header { | |
| 3025 | + display: flex; | |
| 3026 | + justify-content: space-between; | |
| 3027 | + align-items: center; | |
| 3028 | + margin-bottom: 12rpx; | |
| 3029 | + | |
| 3030 | + .insight-title { | |
| 3031 | + font-size: 28rpx; | |
| 3032 | + font-weight: 600; | |
| 3033 | + color: #303133; | |
| 3034 | + } | |
| 3035 | + } | |
| 3036 | + | |
| 3037 | + .insight-value { | |
| 3038 | + display: block; | |
| 3039 | + font-size: 36rpx; | |
| 3040 | + font-weight: 700; | |
| 3041 | + color: #409EFF; | |
| 3042 | + margin-bottom: 8rpx; | |
| 3043 | + } | |
| 3044 | + | |
| 3045 | + .insight-desc { | |
| 3046 | + display: block; | |
| 3047 | + font-size: 24rpx; | |
| 3048 | + color: #909399; | |
| 3049 | + line-height: 1.5; | |
| 3050 | + } | |
| 3051 | + } | |
| 3052 | + } | |
| 3053 | + | |
| 3054 | + // 本月关键指标 | |
| 3055 | + .key-metrics-list { | |
| 3056 | + .key-metric-item { | |
| 3057 | + margin-bottom: 28rpx; | |
| 3058 | + | |
| 3059 | + &:last-child { | |
| 3060 | + margin-bottom: 0; | |
| 3061 | + } | |
| 3062 | + | |
| 3063 | + .metric-header-row { | |
| 3064 | + display: flex; | |
| 3065 | + justify-content: space-between; | |
| 3066 | + align-items: center; | |
| 3067 | + margin-bottom: 12rpx; | |
| 3068 | + | |
| 3069 | + .metric-label { | |
| 3070 | + font-size: 26rpx; | |
| 3071 | + color: #606266; | |
| 3072 | + font-weight: 500; | |
| 3073 | + } | |
| 3074 | + | |
| 3075 | + .metric-percent { | |
| 3076 | + font-size: 28rpx; | |
| 3077 | + font-weight: 700; | |
| 3078 | + color: #303133; | |
| 3079 | + } | |
| 3080 | + } | |
| 3081 | + | |
| 3082 | + .progress-wrapper { | |
| 3083 | + height: 16rpx; | |
| 3084 | + background: #f0f2f5; | |
| 3085 | + border-radius: 8rpx; | |
| 3086 | + overflow: hidden; | |
| 3087 | + | |
| 3088 | + .progress-bar { | |
| 3089 | + height: 100%; | |
| 3090 | + border-radius: 8rpx; | |
| 3091 | + transition: width 0.3s; | |
| 3092 | + } | |
| 3093 | + } | |
| 3094 | + } | |
| 3095 | + } | |
| 3096 | + | |
| 3097 | + .bottom-placeholder { | |
| 3098 | + height: 40rpx; | |
| 3099 | + } | |
| 3100 | + | |
| 3101 | + // 图表容器 | |
| 3102 | + .chart-container { | |
| 3103 | + width: 100%; | |
| 3104 | + height: 500rpx; | |
| 3105 | + display: flex; | |
| 3106 | + justify-content: center; | |
| 3107 | + align-items: center; | |
| 3108 | + background: #ffffff; | |
| 3109 | + border-radius: 16rpx; | |
| 3110 | + padding: 20rpx; | |
| 3111 | + margin-top: 20rpx; | |
| 3112 | + position: relative; | |
| 3113 | + box-sizing: border-box; | |
| 3114 | + | |
| 3115 | + .chart-canvas { | |
| 3116 | + width: calc(100% - 40rpx); | |
| 3117 | + height: calc(100% - 40rpx); | |
| 3118 | + display: block; | |
| 3119 | + } | |
| 3120 | + } | |
| 3121 | + | |
| 3122 | + // 饼图图例 | |
| 3123 | + .category-legend { | |
| 3124 | + display: flex; | |
| 3125 | + flex-wrap: wrap; | |
| 3126 | + gap: 20rpx; | |
| 3127 | + margin-top: 20rpx; | |
| 3128 | + padding: 20rpx; | |
| 3129 | + background: #f8f9fa; | |
| 3130 | + border-radius: 12rpx; | |
| 3131 | + | |
| 3132 | + .legend-item { | |
| 3133 | + display: flex; | |
| 3134 | + align-items: center; | |
| 3135 | + gap: 8rpx; | |
| 3136 | + flex: 1; | |
| 3137 | + min-width: 200rpx; | |
| 3138 | + | |
| 3139 | + .legend-color { | |
| 3140 | + width: 24rpx; | |
| 3141 | + height: 24rpx; | |
| 3142 | + border-radius: 4rpx; | |
| 3143 | + } | |
| 3144 | + | |
| 3145 | + .legend-label { | |
| 3146 | + font-size: 24rpx; | |
| 3147 | + color: #606266; | |
| 3148 | + } | |
| 3149 | + | |
| 3150 | + .legend-value { | |
| 3151 | + font-size: 24rpx; | |
| 3152 | + font-weight: 600; | |
| 3153 | + color: #303133; | |
| 3154 | + margin-left: auto; | |
| 3155 | + } | |
| 3156 | + } | |
| 3157 | + } | |
| 3158 | +</style> | |
| 3159 | + | ... | ... |