Преглед изворни кода

生命周期数据大屏模块+系统导航重构

Zhu Jiaqi пре 4 месеци
родитељ
комит
baa99a436b

+ 1 - 1
.env.development

@@ -1,5 +1,5 @@
 # 页面标题
-VUE_APP_TITLE = 若依管理系统
+VUE_APP_TITLE = 冬塔米CSO系统
 
 # 开发环境配置
 ENV = 'development'

+ 1 - 1
.env.production

@@ -1,5 +1,5 @@
 # 页面标题
-VUE_APP_TITLE = 若依管理系统
+VUE_APP_TITLE = 冬塔米CSO系统
 
 # 生产环境配置
 ENV = 'production'

+ 1 - 1
.env.staging

@@ -1,5 +1,5 @@
 # 页面标题
-VUE_APP_TITLE = 若依管理系统
+VUE_APP_TITLE = 冬塔米CSO系统
 
 BABEL_ENV = production
 

+ 42 - 0
src/api/lifecycle.js

@@ -0,0 +1,42 @@
+import request from '@/utils/request'
+
+// 上传文件并分析
+export function uploadAndAnalyze(file) {
+  const formData = new FormData()
+  formData.append('file', file)
+  return request({
+    url: '/statistics/lifecycle/upload',
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    },
+    timeout: 300000 // 5分钟超时,因为分析可能需要较长时间
+  })
+}
+
+// 获取生命周期分析结果
+export function getLifecycleResults() {
+  return request({
+    url: '/statistics/lifecycle/results',
+    method: 'get'
+  })
+}
+
+// 获取生命周期分析总览数据
+export function getLifecycleOverview(params) {
+  return request({
+    url: '/statistics/lifecycle/overview',
+    method: 'get',
+    params: params
+  })
+}
+
+// 获取指定SKU的生命周期详情
+export function getLifecycleDetail(sku) {
+  return request({
+    url: '/lifecycle/detail/' + sku,
+    method: 'get'
+  })
+}
+

+ 3 - 2
src/settings.js

@@ -2,7 +2,8 @@ module.exports = {
   /**
    * 网页标题
    */
-  title: process.env.VUE_APP_TITLE,
+  // title: process.env.VUE_APP_TITLE,
+  title: '冬塔米CSO系统',
 
   /**
    * 侧边栏主题 深色主题theme-dark,浅色主题theme-light
@@ -23,7 +24,7 @@ module.exports = {
    * 是否显示 tagsView
    */
   tagsView: true,
-  
+
   /**
    * 显示页签图标
    */

+ 3 - 3
src/views/index.vue

@@ -4,7 +4,7 @@
     <div class="module-grid-container">
       <div class="page-header">
         <h2><i class="el-icon-menu"></i> 功能导航</h2>
-        <p>选择您需要的功能模块,开始您的工作</p>
+        <!-- <p>选择您需要的功能模块,开始您的工作</p> -->
       </div>
 
       <div class="module-grid">
@@ -14,9 +14,9 @@
           class="module-card"
           @click="handleModuleClick(module)"
         >
-          <div class="module-badge" v-if="module.children && module.children.length">
+          <!-- <div class="module-badge" v-if="module.children && module.children.length">
             <span>{{ module.children.length }}</span>
-          </div>
+          </div> -->
           <div class="module-icon">
             <svg-icon v-if="module.meta && module.meta.icon" :icon-class="module.meta.icon" class="icon" />
             <i v-else class="el-icon-menu icon"></i>

+ 0 - 98
src/views/index_v1.vue

@@ -1,98 +0,0 @@
-<template>
-  <div class="dashboard-editor-container">
-
-    <panel-group @handleSetLineChartData="handleSetLineChartData" />
-
-    <el-row style="background:#fff;padding:16px 16px 0;margin-bottom:32px;">
-      <line-chart :chart-data="lineChartData" />
-    </el-row>
-
-    <el-row :gutter="32">
-      <el-col :xs="24" :sm="24" :lg="8">
-        <div class="chart-wrapper">
-          <raddar-chart />
-        </div>
-      </el-col>
-      <el-col :xs="24" :sm="24" :lg="8">
-        <div class="chart-wrapper">
-          <pie-chart />
-        </div>
-      </el-col>
-      <el-col :xs="24" :sm="24" :lg="8">
-        <div class="chart-wrapper">
-          <bar-chart />
-        </div>
-      </el-col>
-    </el-row>
-
-    
-  </div>
-</template>
-
-<script>
-import PanelGroup from './dashboard/PanelGroup'
-import LineChart from './dashboard/LineChart'
-import RaddarChart from './dashboard/RaddarChart'
-import PieChart from './dashboard/PieChart'
-import BarChart from './dashboard/BarChart'
-
-const lineChartData = {
-  newVisitis: {
-    expectedData: [100, 120, 161, 134, 105, 160, 165],
-    actualData: [120, 82, 91, 154, 162, 140, 145]
-  },
-  messages: {
-    expectedData: [200, 192, 120, 144, 160, 130, 140],
-    actualData: [180, 160, 151, 106, 145, 150, 130]
-  },
-  purchases: {
-    expectedData: [80, 100, 121, 104, 105, 90, 100],
-    actualData: [120, 90, 100, 138, 142, 130, 130]
-  },
-  shoppings: {
-    expectedData: [130, 140, 141, 142, 145, 150, 160],
-    actualData: [120, 82, 91, 154, 162, 140, 130]
-  }
-}
-
-export default {
-  name: 'Index',
-  components: {
-    PanelGroup,
-    LineChart,
-    RaddarChart,
-    PieChart,
-    BarChart
-  },
-  data() {
-    return {
-      lineChartData: lineChartData.newVisitis
-    }
-  },
-  methods: {
-    handleSetLineChartData(type) {
-      this.lineChartData = lineChartData[type]
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.dashboard-editor-container {
-  padding: 32px;
-  background-color: rgb(240, 242, 245);
-  position: relative;
-
-  .chart-wrapper {
-    background: #fff;
-    padding: 16px 16px 0;
-    margin-bottom: 32px;
-  }
-}
-
-@media (max-width:1024px) {
-  .chart-wrapper {
-    padding: 8px;
-  }
-}
-</style>

+ 819 - 0
src/views/lifecycle/overview/index.vue

@@ -0,0 +1,819 @@
+<template>
+  <div class="app-container">
+    <!-- 页面标题 -->
+    <div class="page-header">
+      <h2><i class="el-icon-data-analysis"></i> SKU生命周期整体看板</h2>
+      <p class="page-desc">全局SKU生命周期分析概览,包含关键指标和趋势分析</p>
+    </div>
+
+    <!-- 文件上传区域 -->
+    <el-card class="mb-20">
+      <div slot="header">
+        <span><i class="el-icon-upload"></i> 数据文件上传</span>
+      </div>
+      <el-upload
+        ref="upload"
+        :limit="1"
+        accept=".xlsx,.xls,.csv"
+        :http-request="customUpload"
+        :disabled="upload.isUploading"
+        :on-progress="handleFileUploadProgress"
+        :before-upload="beforeUpload"
+        :auto-upload="false"
+        drag
+      >
+        <i class="el-icon-upload"></i>
+        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+        <div class="el-upload__tip" slot="tip">
+          <el-checkbox v-model="upload.updateSupport" /> 是否覆盖已上传的文件
+          <div>只能上传xlsx/xls/csv文件,且不超过20MB</div>
+        </div>
+      </el-upload>
+      <div style="margin-top: 15px">
+        <el-button :loading="upload.isUploading" type="primary" @click="submitUpload">立即上传并分析</el-button>
+        <el-button @click="resetUpload">重置</el-button>
+      </div>
+    </el-card>
+
+    <!-- 关键指标卡片 -->
+    <el-row :gutter="20" class="mb-20">
+      <el-col :xs="24" :sm="12" :md="8" :lg="4">
+        <el-card class="stat-card">
+          <div class="stat-content">
+            <div class="stat-info">
+              <p class="stat-label">订单总数(估)</p>
+              <p class="stat-value">{{ orderCount }}</p>
+              <p class="stat-desc">按销量近似</p>
+            </div>
+            <div class="stat-icon stat-icon-purple">
+              <i class="el-icon-s-order"></i>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :sm="12" :md="8" :lg="4">
+        <el-card class="stat-card">
+          <div class="stat-content">
+            <div class="stat-info">
+              <p class="stat-label">SKU总数</p>
+              <p class="stat-value">{{ skuCount }}</p>
+              <p class="stat-desc stat-desc-success">分析后</p>
+            </div>
+            <div class="stat-icon stat-icon-blue">
+              <i class="el-icon-box"></i>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :sm="12" :md="8" :lg="4">
+        <el-card class="stat-card">
+          <div class="stat-content">
+            <div class="stat-info">
+              <p class="stat-label">商品总数</p>
+              <p class="stat-value">{{ productCount }}</p>
+              <p class="stat-desc">按详情名称去重</p>
+            </div>
+            <div class="stat-icon stat-icon-teal">
+              <i class="el-icon-s-goods"></i>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :sm="12" :md="8" :lg="4">
+        <el-card class="stat-card">
+          <div class="stat-content">
+            <div class="stat-info">
+              <p class="stat-label">完整生命周期SKU</p>
+              <p class="stat-value">{{ completeCount }}</p>
+              <p class="stat-desc stat-desc-success">完整占比 {{ completeRatio }}%</p>
+            </div>
+            <div class="stat-icon stat-icon-green">
+              <i class="el-icon-success"></i>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :sm="12" :md="8" :lg="4">
+        <el-card class="stat-card">
+          <div class="stat-content">
+            <div class="stat-info">
+              <p class="stat-label">平均生命周期时长(天)</p>
+              <p class="stat-value">{{ avgLifecycleDays }}</p>
+              <p class="stat-desc">仅计算完整SKU</p>
+            </div>
+            <div class="stat-icon stat-icon-yellow">
+              <i class="el-icon-time"></i>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :sm="12" :md="8" :lg="4">
+        <el-card class="stat-card">
+          <div class="stat-content">
+            <div class="stat-info">
+              <p class="stat-label">数据不足SKU</p>
+              <p class="stat-value">{{ insufficientCount }}</p>
+              <p class="stat-desc">天数/字段不足</p>
+            </div>
+            <div class="stat-icon stat-icon-red">
+              <i class="el-icon-warning"></i>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 阶段分布与关键指标图 -->
+    <el-row :gutter="20" class="mb-20">
+      <el-col :xs="24" :sm="24" :md="12" :lg="12">
+        <el-card>
+          <div slot="header">
+            <span><i class="el-icon-pie-chart"></i> SKU生命周期阶段分布</span>
+            <span class="header-desc">按数量占比</span>
+          </div>
+          <div ref="stageDistributionChart" style="height: 400px"></div>
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :sm="24" :md="12" :lg="12">
+        <el-card>
+          <div slot="header">
+            <span><i class="el-icon-data-line"></i> 各阶段关键指标对比</span>
+            <span class="header-desc">销售额/销量(估)</span>
+          </div>
+          <div ref="stageMetricsChart" style="height: 400px"></div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 阶段平均时长与转化漏斗 -->
+    <el-row :gutter="20" class="mb-20">
+      <el-col :xs="24" :sm="24" :md="12" :lg="12">
+        <el-card>
+          <div slot="header">
+            <span><i class="el-icon-s-marketing"></i> 阶段平均时长(天)</span>
+            <span class="header-desc">完整SKU统计</span>
+          </div>
+          <div ref="avgDurationChart" style="height: 400px"></div>
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :sm="24" :md="12" :lg="12">
+        <el-card>
+          <div slot="header">
+            <span><i class="el-icon-s-data"></i> 阶段转化漏斗</span>
+            <span class="header-desc">引入→成长→成熟→衰退</span>
+          </div>
+          <div ref="funnelChart" style="height: 400px"></div>
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { uploadAndAnalyze, getLifecycleResults } from '@/api/lifecycle'
+import { getToken } from '@/utils/auth'
+import * as echarts from 'echarts'
+require('echarts/theme/macarons')
+
+export default {
+  name: 'LifecycleOverview',
+  data() {
+    return {
+      // 图表实例
+      stageDistributionChart: null,
+      stageMetricsChart: null,
+      avgDurationChart: null,
+      funnelChart: null,
+      // 数据
+      results: {},
+      // 计算属性数据
+      orderCount: 0,
+      skuCount: 0,
+      productCount: 0,
+      completeCount: 0,
+      completeRatio: 0,
+      avgLifecycleDays: 0,
+      insufficientCount: 0,
+      // 文件上传相关
+      upload: {
+        // 是否显示弹出层
+        open: false,
+        // 弹出层标题
+        title: '',
+        // 是否禁用上传
+        isUploading: false,
+        // 是否更新已经存在的文件
+        updateSupport: 0,
+        // 设置上传的请求头部
+        headers: { Authorization: 'Bearer ' + getToken() },
+        // 上传的地址
+        url: process.env.VUE_APP_BASE_API + '/statistics/lifecycle/upload'
+      }
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initCharts()
+    })
+    // 监听窗口大小变化
+    window.addEventListener('resize', this.handleResize)
+  },
+  beforeDestroy() {
+    // 销毁图表实例
+    if (this.stageDistributionChart) {
+      this.stageDistributionChart.dispose()
+    }
+    if (this.stageMetricsChart) {
+      this.stageMetricsChart.dispose()
+    }
+    if (this.avgDurationChart) {
+      this.avgDurationChart.dispose()
+    }
+    if (this.funnelChart) {
+      this.funnelChart.dispose()
+    }
+    window.removeEventListener('resize', this.handleResize)
+  },
+  methods: {
+    /** 文件上传前的校验 */
+    beforeUpload(file) {
+      const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
+                      file.type === 'application/vnd.ms-excel' ||
+                      file.type === 'text/csv' ||
+                      file.name.endsWith('.xlsx') ||
+                      file.name.endsWith('.xls') ||
+                      file.name.endsWith('.csv')
+      const isLt20M = file.size / 1024 / 1024 < 20
+
+      if (!isExcel) {
+        this.$modal.msgError('上传文件只能是 xlsx/xls/csv 格式!')
+        return false
+      }
+      if (!isLt20M) {
+        this.$modal.msgError('上传文件大小不能超过 20MB!')
+        return false
+      }
+      return true
+    },
+    /** 文件上传中处理 */
+    handleFileUploadProgress(event, file, fileList) {
+      this.upload.isUploading = true
+    },
+    /** 自定义上传方法 */
+    customUpload(options) {
+      const file = options.file
+      this.upload.isUploading = true
+      uploadAndAnalyze(file).then(response => {
+        this.upload.isUploading = false
+        if (response.code === 200) {
+          this.$modal.msgSuccess('文件上传并分析成功')
+          // response.data 就是分析结果,格式为 { sku1: {...}, sku2: {...}, ... }
+          this.results = response.data || {}
+          this.calculateMetrics()
+          this.$nextTick(() => {
+            this.renderCharts()
+          })
+          options.onSuccess(response)
+        } else {
+          this.$modal.msgError(response.msg || '分析失败')
+          options.onError(new Error(response.msg || '分析失败'))
+        }
+        // 重置上传组件
+        this.$refs.upload.clearFiles()
+      }).catch(error => {
+        this.upload.isUploading = false
+        const errorMsg = error.response?.data?.msg || error.message || '文件上传失败,请重试'
+        this.$modal.msgError(errorMsg)
+        options.onError(error)
+      })
+    },
+    /** 提交上传文件 */
+    submitUpload() {
+      const fileList = this.$refs.upload.uploadFiles
+      if (!fileList || fileList.length === 0) {
+        this.$modal.msgError('请选择要上传的文件')
+        return
+      }
+      this.$refs.upload.submit()
+    },
+    /** 重置上传 */
+    resetUpload() {
+      this.$refs.upload.clearFiles()
+    },
+    /** 获取生命周期分析结果 */
+    getList() {
+      getLifecycleResults().then(response => {
+        if (response.code === 200 && response.data) {
+          // response.data 就是分析结果,格式为 { sku1: {...}, sku2: {...}, ... }
+          this.results = response.data || {}
+          this.calculateMetrics()
+          this.$nextTick(() => {
+            this.renderCharts()
+          })
+        }
+      }).catch(() => {
+        // 如果没有数据,不显示错误,只是不显示图表
+        this.results = {}
+      })
+    },
+    /** 计算关键指标 */
+    calculateMetrics() {
+      // 仅取真实SKU结果,排除聚合概要 _analysis_summary_
+      const entries = Object.entries(this.results || {})
+      const resultsArr = entries.filter(([k]) => k !== '_analysis_summary_').map(([, v]) => v)
+
+      this.skuCount = resultsArr.length
+
+      // 商品总数(按详情名称去重)
+      const names = new Set()
+      resultsArr.forEach(r => {
+        if (r?.details) names.add(r.details)
+      })
+      this.productCount = names.size
+
+      // 完整生命周期SKU
+      this.completeCount = resultsArr.filter(r => !!r?.is_complete).length
+      this.completeRatio = this.skuCount ? ((this.completeCount / this.skuCount) * 100).toFixed(1) : 0
+
+      // 平均生命周期时长(仅计算完整SKU)
+      const completeList = resultsArr.filter(r => !!r?.is_complete)
+      if (completeList.length > 0) {
+        const total = completeList.reduce((s, r) => s + this.getLifecycleDays(r), 0)
+        this.avgLifecycleDays = Math.round(total / completeList.length)
+      } else {
+        this.avgLifecycleDays = 0
+      }
+
+      // 数据不足SKU
+      this.insufficientCount = resultsArr.filter(r => this.isInsufficient(r)).length
+
+      // 订单总数(按销量近似)
+      this.orderCount = resultsArr.reduce((s, r) => s + this.sumArray(r?.quantity_series || []), 0)
+    },
+    /** 初始化图表 */
+    initCharts() {
+      if (this.$refs.stageDistributionChart) {
+        this.stageDistributionChart = echarts.init(this.$refs.stageDistributionChart, 'macarons')
+      }
+      if (this.$refs.stageMetricsChart) {
+        this.stageMetricsChart = echarts.init(this.$refs.stageMetricsChart, 'macarons')
+      }
+      if (this.$refs.avgDurationChart) {
+        this.avgDurationChart = echarts.init(this.$refs.avgDurationChart, 'macarons')
+      }
+      if (this.$refs.funnelChart) {
+        this.funnelChart = echarts.init(this.$refs.funnelChart, 'macarons')
+      }
+    },
+    /** 渲染所有图表 */
+    renderCharts() {
+      const entries = Object.entries(this.results || {})
+      const resultsArr = entries.filter(([k]) => k !== '_analysis_summary_').map(([, v]) => v)
+
+      // 1. SKU生命周期阶段分布(饼图)
+      this.renderStageDistribution(resultsArr)
+
+      // 2. 各阶段关键指标对比(柱状图)
+      this.renderStageMetrics(resultsArr)
+
+      // 3. 阶段平均时长(柱状图)
+      this.renderAvgDuration(resultsArr)
+
+      // 4. 阶段转化漏斗(横向条形图)
+      this.renderFunnel(resultsArr)
+    },
+    /** 渲染阶段分布饼图 */
+    renderStageDistribution(list) {
+      const dist = { 引入期: 0, 成长期: 0, 成熟期: 0, 衰退期: 0 }
+      list.forEach(r => {
+        const cur = this.normalizeStage(r?.current_stage)
+        if (cur && dist[cur] != null) dist[cur]++
+      })
+
+      const data = Object.keys(dist).map(key => ({
+        value: dist[key],
+        name: key
+      }))
+
+      const option = {
+        tooltip: {
+          trigger: 'item',
+          formatter: '{a} <br/>{b}: {c} ({d}%)'
+        },
+        legend: {
+          orient: 'vertical',
+          left: 'left',
+          data: Object.keys(dist)
+        },
+        series: [
+          {
+            name: '阶段分布',
+            type: 'pie',
+            radius: ['40%', '70%'],
+            avoidLabelOverlap: false,
+            itemStyle: {
+              borderRadius: 10,
+              borderColor: '#fff',
+              borderWidth: 2
+            },
+            label: {
+              show: true,
+              formatter: '{b}: {c}\n({d}%)'
+            },
+            emphasis: {
+              label: {
+                show: true,
+                fontSize: 16,
+                fontWeight: 'bold'
+              }
+            },
+            data: data,
+            color: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444']
+          }
+        ]
+      }
+
+      if (this.stageDistributionChart) {
+        this.stageDistributionChart.setOption(option)
+      }
+    },
+    /** 渲染各阶段关键指标对比 */
+    renderStageMetrics(list) {
+      const agg = this.aggregateStageMetrics(list)
+      const metricLabels = Object.keys(agg)
+      const revenueAgg = metricLabels.map(k => agg[k].totalRevenue)
+      const qtyAgg = metricLabels.map(k => agg[k].totalQuantity)
+
+      const option = {
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            type: 'shadow'
+          }
+        },
+        legend: {
+          data: ['销售额(总计)', '销量(总计)']
+        },
+        grid: {
+          left: '3%',
+          right: '4%',
+          bottom: '3%',
+          containLabel: true
+        },
+        xAxis: {
+          type: 'category',
+          data: metricLabels
+        },
+        yAxis: [
+          {
+            type: 'value',
+            name: '销售额',
+            position: 'left'
+          },
+          {
+            type: 'value',
+            name: '销量',
+            position: 'right'
+          }
+        ],
+        series: [
+          {
+            name: '销售额(总计)',
+            type: 'bar',
+            data: revenueAgg,
+            itemStyle: {
+              color: 'rgba(59,130,246,0.7)'
+            }
+          },
+          {
+            name: '销量(总计)',
+            type: 'bar',
+            yAxisIndex: 1,
+            data: qtyAgg,
+            itemStyle: {
+              color: 'rgba(100,116,139,0.7)'
+            }
+          }
+        ]
+      }
+
+      if (this.stageMetricsChart) {
+        this.stageMetricsChart.setOption(option)
+      }
+    },
+    /** 渲染阶段平均时长 */
+    renderAvgDuration(list) {
+      const avgDur = this.averageStageDuration(list)
+      const durLabels = Object.keys(avgDur)
+      const durValues = Object.values(avgDur)
+
+      const option = {
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            type: 'shadow'
+          }
+        },
+        grid: {
+          left: '3%',
+          right: '4%',
+          bottom: '3%',
+          containLabel: true
+        },
+        xAxis: {
+          type: 'category',
+          data: durLabels
+        },
+        yAxis: {
+          type: 'value',
+          name: '天数'
+        },
+        series: [
+          {
+            name: '平均持续天数',
+            type: 'bar',
+            data: durValues,
+            itemStyle: {
+              color: 'rgba(245,158,11,0.7)'
+            },
+            label: {
+              show: true,
+              position: 'top'
+            }
+          }
+        ]
+      }
+
+      if (this.avgDurationChart) {
+        this.avgDurationChart.setOption(option)
+      }
+    },
+    /** 渲染阶段转化漏斗 */
+    renderFunnel(list) {
+      const funnelData = this.stageFunnel(list)
+      const funnelLabels = funnelData.labels
+      const funnelValues = funnelData.values
+
+      const option = {
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            type: 'shadow'
+          }
+        },
+        grid: {
+          left: '3%',
+          right: '4%',
+          bottom: '3%',
+          containLabel: true
+        },
+        xAxis: {
+          type: 'value',
+          name: '转化率(%)'
+        },
+        yAxis: {
+          type: 'category',
+          data: funnelLabels
+        },
+        series: [
+          {
+            name: '转化率',
+            type: 'bar',
+            data: funnelValues,
+            itemStyle: {
+              color: 'rgba(59,130,246,0.7)'
+            },
+            label: {
+              show: true,
+              position: 'right',
+              formatter: '{c}%'
+            }
+          }
+        ]
+      }
+
+      if (this.funnelChart) {
+        this.funnelChart.setOption(option)
+      }
+    },
+    /** 窗口大小变化处理 */
+    handleResize() {
+      if (this.stageDistributionChart) {
+        this.stageDistributionChart.resize()
+      }
+      if (this.stageMetricsChart) {
+        this.stageMetricsChart.resize()
+      }
+      if (this.avgDurationChart) {
+        this.avgDurationChart.resize()
+      }
+      if (this.funnelChart) {
+        this.funnelChart.resize()
+      }
+    },
+    /** 工具函数 */
+    sumArray(arr) {
+      return (arr || []).reduce((s, v) => s + (Number(v) || 0), 0)
+    },
+    getLifecycleDays(r) {
+      const stats = r?.stage_statistics || {}
+      return Object.values(stats).reduce((s, v) => s + (v.durationDays || 0), 0)
+    },
+    isInsufficient(r) {
+      const days = (r?.date_series || []).length
+      const stats = r?.stage_statistics || {}
+      return days < 120 || Object.keys(stats).length === 0
+    },
+    normalizeStage(s) {
+      if (!s) return ''
+      if (s.includes('导入') || s.includes('引入')) return '引入期'
+      if (s.includes('成长')) return '成长期'
+      if (s.includes('成熟')) return '成熟期'
+      if (s.includes('衰退')) return '衰退期'
+      return s
+    },
+    averageStageDuration(list) {
+      // 仅统计完整生命周期SKU
+      const filtered = list.filter(r => !!r?.is_complete)
+      const agg = { 引入期: [], 成长期: [], 成熟期: [], 衰退期: [] }
+      filtered.forEach(r => {
+        const stats = r?.stage_statistics || {}
+        Object.entries(stats).forEach(([stage, v]) => {
+          const key = this.normalizeStage(stage)
+          if (agg[key]) agg[key].push(v?.durationDays || 0)
+        })
+      })
+      const res = {}
+      Object.entries(agg).forEach(([k, arr]) => {
+        res[k] = arr.length ? Math.round(arr.reduce((s, x) => s + (Number(x) || 0), 0) / arr.length) : 0
+      })
+      return res
+    },
+    aggregateStageMetrics(list) {
+      const agg = {
+        引入期: { totalRevenue: 0, totalQuantity: 0 },
+        成长期: { totalRevenue: 0, totalQuantity: 0 },
+        成熟期: { totalRevenue: 0, totalQuantity: 0 },
+        衰退期: { totalRevenue: 0, totalQuantity: 0 }
+      }
+      list.forEach(r => {
+        const stats = r?.stage_statistics || {}
+        Object.entries(stats).forEach(([stage, v]) => {
+          const key = this.normalizeStage(stage)
+          if (agg[key]) {
+            agg[key].totalRevenue += Number(v?.totalRevenue || 0)
+            agg[key].totalQuantity += Number(v?.totalQuantity || 0)
+          }
+        })
+      })
+      return agg
+    },
+    stageFunnel(list) {
+      // 顺序转化:引入→成长→成熟→衰退
+      let intro = 0, growth = 0, maturity = 0, decline = 0
+      list.forEach(r => {
+        const stats = r?.stage_statistics || {}
+        const names = Object.keys(stats).map(this.normalizeStage)
+        const hasIntro = names.includes('引入期')
+        const hasGrowth = names.includes('成长期')
+        const hasMaturity = names.includes('成熟期')
+        const hasDecline = names.includes('衰退期')
+        if (hasIntro) intro++
+        if (hasIntro && hasGrowth) growth++
+        if (hasIntro && hasGrowth && hasMaturity) maturity++
+        if (hasIntro && hasGrowth && hasMaturity && hasDecline) decline++
+      })
+      const labels = ['引入期', '成长期', '成熟期', '衰退期']
+      const counts = [intro, growth, maturity, decline]
+      const values = counts.map((c, i) => {
+        if (i === 0) return intro ? 100 : 0
+        const prev = counts[i - 1]
+        return prev ? Number(((c / prev) * 100).toFixed(1)) : 0
+      })
+      return { labels, values }
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.app-container {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 20px;
+  
+  h2 {
+    font-size: 24px;
+    font-weight: 600;
+    color: #303133;
+    margin-bottom: 8px;
+    
+    i {
+      margin-right: 8px;
+      color: #409EFF;
+    }
+  }
+  
+  .page-desc {
+    color: #909399;
+    font-size: 14px;
+    margin: 0;
+  }
+}
+
+.mb-20 {
+  margin-bottom: 20px;
+}
+
+.stat-card {
+  .stat-content {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    
+    .stat-info {
+      flex: 1;
+      
+      .stat-label {
+        font-size: 12px;
+        color: #909399;
+        margin: 0 0 8px 0;
+        text-transform: uppercase;
+        letter-spacing: 0.5px;
+      }
+      
+      .stat-value {
+        font-size: 28px;
+        font-weight: bold;
+        color: #303133;
+        margin: 0 0 8px 0;
+      }
+      
+      .stat-desc {
+        font-size: 12px;
+        color: #909399;
+        margin: 0;
+        
+        &.stat-desc-success {
+          color: #67C23A;
+        }
+      }
+    }
+    
+    .stat-icon {
+      width: 48px;
+      height: 48px;
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 20px;
+      
+      &.stat-icon-purple {
+        background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%);
+        color: #6366f1;
+      }
+      
+      &.stat-icon-blue {
+        background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
+        color: #3b82f6;
+      }
+      
+      &.stat-icon-teal {
+        background: linear-gradient(135deg, #ccfbf1 0%, #99f6e4 100%);
+        color: #14b8a6;
+      }
+      
+      &.stat-icon-green {
+        background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
+        color: #10b981;
+      }
+      
+      &.stat-icon-yellow {
+        background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
+        color: #f59e0b;
+      }
+      
+      &.stat-icon-red {
+        background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
+        color: #ef4444;
+      }
+    }
+  }
+}
+
+::v-deep .el-card__header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  
+  .header-desc {
+    font-size: 12px;
+    color: #909399;
+    font-weight: normal;
+  }
+}
+</style>
+

+ 1 - 1
src/views/login.vue

@@ -71,7 +71,7 @@ export default {
   name: "Login",
   data() {
     return {
-      title: process.env.VUE_APP_TITLE,
+      title: "冬塔米CSO系统",
       footerContent: defaultSettings.footerContent,
       codeUrl: "",
       loginForm: {

+ 1 - 1
src/views/module-submenu.vue

@@ -2,7 +2,7 @@
   <div class="app-container submenu-container">
     <div class="page-header">
       <h2>{{ moduleTitle }}</h2>
-      <p>请选择具体功能</p>
+      <!-- <p>请选择具体功能</p> -->
     </div>
 
     <div class="submenu-grid">