Commit d2682494cf56ba157cf4587657f777f03b4ffb3e

Authored by 李宇
1 parent 7f0666d6

最新

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
  1 +{
  2 + "dependencies": {
  3 + "@qiun/ucharts": "^2.5.0-20230101"
  4 + }
  5 +}
... ...
绿纤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
1 1 {
2 2 "dependencies": {
  3 + "@qiun/ucharts": "^2.5.0-20230101",
3 4 "html2canvas": "^1.4.1"
4 5 }
5 6 }
... ...
绿纤uni-app/pages.json
... ... @@ -339,6 +339,12 @@
339 339 "style": {
340 340 "navigationStyle": "custom"
341 341 }
  342 + },
  343 + {
  344 + "path": "pages/store-dashboard/store-dashboard",
  345 + "style": {
  346 + "navigationBarTitleText": "门店数据报表"
  347 + }
342 348 }
343 349 ],
344 350 "globalStyle": {
... ...
绿纤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 +
... ...