فهرست منبع

生命周期模块生命周期分析、爆品分析sku与spu层级整合,前端数据缓存

Zhu Jiaqi 1 ماه پیش
والد
کامیت
3fb11f819f

+ 14 - 186
src/api/client.js

@@ -2,177 +2,21 @@ import axios from 'axios'
 
 const http = axios.create({
   baseURL: process.env.VUE_APP_PYTHON_API,
-  timeout: 300000, // 5分钟超时,用于大数据量分析
+  timeout: 300000
 })
 
-export async function uploadFile(file) {
+function uploadFileTo(url, file) {
   const form = new FormData()
   form.append('file', file)
-  // 不要手动设置 Content-Type,让浏览器自动添加 boundary
-  const { data } = await http.post('/api/sku-lifecycle/upload', form, {
+  return http.post(url, form, {
     headers: { 'Content-Type': 'multipart/form-data' }
-  })
-  return data
-}
-
-export async function analyzeFile(file) {
-  // Upload file first
-  const uploadResult = await uploadFile(file)
-  if (!uploadResult.success) {
-    throw new Error(uploadResult.message || 'File upload failed')
-  }
-
-  // Analyze cached data
-  const { data } = await http.post('/api/sku-lifecycle/analyze')
-  return data
-}
-
-
-
-// export async function analyzeProductLifecycle(file) {
-//   const form = new FormData()
-//   form.append('file', file)
-//   // 不要手动设置 Content-Type,让浏览器自动添加 boundary
-//   const { data } = await http.post('/sku-lifecycle/analyze-product', form)
-//   return data
-// }
-
-export async function getResults() {
-  const { data } = await http.get('/api/sku-lifecycle/results')
-  return data
-}
-
-export async function getResultBySku(sku) {
-  const { data } = await http.get(`/api/sku-lifecycle/results/${encodeURIComponent(sku)}`)
-  return data
-}
-
-// SPU 生命周期相关 API
-export async function uploadSpuFile(file) {
-  const form = new FormData()
-  form.append('file', file)
-  const { data } = await http.post('/api/spu-lifecycle/upload', form, {
-    headers: { 'Content-Type': 'multipart/form-data' }
-  })
-  return data
-}
-
-export async function analyzeSpuFile(file) {
-  const uploadResult = await uploadSpuFile(file)
-  if (!uploadResult.success) {
-    throw new Error(uploadResult.message || 'File upload failed')
-  }
-
-  const { data } = await http.post('/api/spu-lifecycle/analyze')
-  return data
+  }).then(({ data }) => data)
 }
 
-export async function getSpuResults() {
-  const { data } = await http.get('/api/spu-lifecycle/results')
-  return data
-}
-
-export async function getResultBySpu(spu) {
-  const { data } = await http.get(`/api/spu-lifecycle/results/${encodeURIComponent(spu)}`)
-  return data
-}
-
-export async function getCacheInfo() {
-  const { data } = await http.get('/api/sku-lifecycle/cache/info')
-  return data
-}
-
-export async function clearBackendCache() {
-  const { data } = await http.delete('/api/sku-lifecycle/cache')
-}
-
-export async function simulateStrategy(payload) {
-  const { data } = await http.post('/api/sku-lifecycle/strategy-simulation', payload)
-  return data
-}
-
-export async function getInsights(payload) {
-  const { data } = await http.post('/api/sku-lifecycle/insights', payload)
-  return data
-}
-
-// SKU 生命周期策略模拟相关 API(显式命名,便于页面直接对接)
-export async function simulateSkuLifecycleStrategy(payload) {
-  return simulateStrategy(payload)
-}
-
-export async function getSkuLifecycleInsights(payload) {
-  return getInsights(payload)
-}
-
-// 爆款分析相关API
-export async function uploadHotProductFile(file) {
-  const form = new FormData()
-  form.append('file', file)
-  const { data } = await http.post('/api/sku-lifecycle/upload', form, {
-    headers: { 'Content-Type': 'multipart/form-data' }
-  })
-  return data
-}
-
-export async function analyzeHotProduct() {
-  const { data } = await http.post('/api/hotproduct/analyze')
-  return data
-}
-
-export async function analyzeHotProductWithFile(file) {
-  // Upload file first
-  const uploadResult = await uploadHotProductFile(file)
-  if (!uploadResult.success) {
-    throw new Error(uploadResult.message || 'File upload failed')
-  }
-
-  // Analyze cached data
-  return await analyzeHotProduct()
-}
-
-// SPU 爆款分析相关 API
-export async function analyzeSpuHotProductWithFile(file) {
-  // Upload file first
-  const uploadResult = await uploadSpuFile(file)
-  if (!uploadResult.success) {
-    throw new Error(uploadResult.message || 'File upload failed')
-  }
-
-  const { data } = await http.post('/api/spu-hotproduct/analyze')
-  return data
-}
-
-export async function getSpuHotProductResults() {
-  const { data } = await http.get('/api/spu-hotproduct/results')
-  return data
-}
-
-export async function getSpuHotProductResultBySpu(spu) {
-  const { data } = await http.get(`/api/spu-hotproduct/results/${encodeURIComponent(spu)}`)
-  return data
-}
-
-
-
-export async function getHotProductResults() {
-  const { data } = await http.get('/api/hotproduct/results')
-  return data
-}
-
-export async function getHotProductResultBySku(sku) {
-  const { data } = await http.get(`/api/hotproduct/results/${encodeURIComponent(sku)}`)
-  return data
-}
-
-// 销售概览相关API
+// 爆品分析相关 API
+// 销售概览相关 API
 export async function uploadSaleOverviewFile(file) {
-  const form = new FormData()
-  form.append('file', file)
-  const { data } = await http.post('/api/sale-overview/upload', form, {
-    headers: { 'Content-Type': 'multipart/form-data' }
-  })
-  return data
+  return uploadFileTo('/api/sale-overview/upload', file)
 }
 
 export async function analyzeSaleOverview() {
@@ -181,14 +25,12 @@ export async function analyzeSaleOverview() {
 }
 
 export async function analyzeSaleOverviewWithFile(file) {
-  // Upload file first
   const uploadResult = await uploadSaleOverviewFile(file)
   if (!uploadResult.success) {
     throw new Error(uploadResult.message || 'File upload failed')
   }
 
-  // Analyze cached data
-  return await analyzeSaleOverview()
+  return analyzeSaleOverview()
 }
 
 export async function getSaleOverviewResults() {
@@ -196,14 +38,9 @@ export async function getSaleOverviewResults() {
   return data
 }
 
-// 销售趋势预测相关API
+// 销售趋势预测相关 API
 export async function uploadSaleTrendFile(file) {
-  const form = new FormData()
-  form.append('file', file)
-  const { data } = await http.post('/api/sale-trend/upload', form, {
-    headers: { 'Content-Type': 'multipart/form-data' }
-  })
-  return data
+  return uploadFileTo('/api/sale-trend/upload', file)
 }
 
 export async function analyzeSaleTrend() {
@@ -212,14 +49,12 @@ export async function analyzeSaleTrend() {
 }
 
 export async function analyzeSaleTrendWithFile(file) {
-  // Upload file first
   const uploadResult = await uploadSaleTrendFile(file)
   if (!uploadResult.success) {
     throw new Error(uploadResult.message || 'File upload failed')
   }
 
-  // Analyze cached data
-  return await analyzeSaleTrend()
+  return analyzeSaleTrend()
 }
 
 export async function getSaleTrendResults() {
@@ -232,14 +67,9 @@ export async function predictSalesTrend(params) {
   return data
 }
 
-// 促销效果分析相关API
+// 促销效果分析相关 API
 export async function uploadSaleEffectFile(file) {
-  const form = new FormData()
-  form.append('file', file)
-  const { data } = await http.post('/api/sale-effect/upload', form, {
-    headers: { 'Content-Type': 'multipart/form-data' }
-  })
-  return data
+  return uploadFileTo('/api/sale-effect/upload', file)
 }
 
 export async function analyzeSaleEffect() {
@@ -248,14 +78,12 @@ export async function analyzeSaleEffect() {
 }
 
 export async function analyzeSaleEffectWithFile(file) {
-  // Upload file first
   const uploadResult = await uploadSaleEffectFile(file)
   if (!uploadResult.success) {
     throw new Error(uploadResult.message || 'File upload failed')
   }
 
-  // Analyze cached data
-  return await analyzeSaleEffect()
+  return analyzeSaleEffect()
 }
 
 export async function getSaleEffectResults() {

+ 114 - 40
src/api/lifecycle.js

@@ -1,42 +1,116 @@
-import request from '@/utils/request'
-
-// 上传文件并分析
-export function uploadAndAnalyze(file) {
-  const formData = new FormData()
-  formData.append('file', file)
-  return request({
-    url: '/lifecycle/upload',
-    method: 'post',
-    data: formData,
-    headers: {
-      'Content-Type': 'multipart/form-data'
-    },
-    timeout: 300000 // 5分钟超时,因为分析可能需要较长时间
-  })
-}
-
-// 获取生命周期分析结果
-export function getLifecycleResults() {
-  return request({
-    url: '/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'
-  })
+import axios from 'axios'
+
+const http = axios.create({
+  baseURL: process.env.VUE_APP_PYTHON_API,
+  timeout: 300000
+})
+
+function uploadLifecycleFile(file, url) {
+  const form = new FormData()
+  form.append('file', file)
+  return http.post(url, form, {
+    headers: { 'Content-Type': 'multipart/form-data' }
+  }).then(({ data }) => data)
+}
+
+export async function uploadFile(file) {
+  return uploadLifecycleFile(file, '/api/sku-lifecycle/upload')
+}
+
+export async function analyzeFile(file) {
+  const uploadResult = await uploadFile(file)
+  if (!uploadResult.success) {
+    throw new Error(uploadResult.message || 'File upload failed')
+  }
+
+  const { data } = await http.post('/api/sku-lifecycle/analyze')
+  return data
+}
+
+export async function getResults() {
+  const { data } = await http.get('/api/sku-lifecycle/results')
+  return data
+}
+
+export async function uploadSpuFile(file) {
+  return uploadLifecycleFile(file, '/api/spu-lifecycle/upload')
+}
+
+export async function analyzeSpuFile(file) {
+  const uploadResult = await uploadSpuFile(file)
+  if (!uploadResult.success) {
+    throw new Error(uploadResult.message || 'File upload failed')
+  }
+
+  const { data } = await http.post('/api/spu-lifecycle/analyze')
+  return data
+}
+
+export async function getSpuResults() {
+  const { data } = await http.get('/api/spu-lifecycle/results')
+  return data
+}
+
+export async function uploadHotProductFile(file) {
+  return uploadLifecycleFile(file, '/api/sku-lifecycle/upload')
+}
+
+export async function analyzeHotProduct() {
+  const { data } = await http.post('/api/hotproduct/analyze')
+  return data
 }
 
+export async function analyzeHotProductWithFile(file) {
+  const uploadResult = await uploadHotProductFile(file)
+  if (!uploadResult.success) {
+    throw new Error(uploadResult.message || 'File upload failed')
+  }
+
+  return analyzeHotProduct()
+}
+
+async function uploadSpuHotProductFile(file) {
+  return uploadLifecycleFile(file, '/api/spu-lifecycle/upload')
+}
+
+export async function analyzeSpuHotProductWithFile(file) {
+  const uploadResult = await uploadSpuHotProductFile(file)
+  if (!uploadResult.success) {
+    throw new Error(uploadResult.message || 'File upload failed')
+  }
+
+  const { data } = await http.post('/api/spu-hotproduct/analyze')
+  return data
+}
+
+export async function getHotProductResults() {
+  const { data } = await http.get('/api/hotproduct/results')
+  return data
+}
+
+export async function getSpuHotProductResults() {
+  const { data } = await http.get('/api/spu-hotproduct/results')
+  return data
+}
+
+// export async function analyzeProductLifecycle(file) {
+//   return analyzeFile(file)
+// }
+
+async function simulateStrategy(payload) {
+  const { data } = await http.post('/api/sku-lifecycle/strategy-simulation', payload)
+  return data
+}
+
+async function getInsights(payload) {
+  const { data } = await http.post('/api/sku-lifecycle/insights', payload)
+  return data
+}
+
+export async function simulateSkuLifecycleStrategy(payload) {
+  return simulateStrategy(payload)
+}
+
+export async function getSkuLifecycleInsights(payload) {
+  return getInsights(payload)
+}

+ 11 - 6
src/api/sales.js

@@ -1,11 +1,13 @@
 import request from '@/utils/request'
-
 // 上传文件并分析销售数据
+
 export function uploadAndAnalyzeSales(file) {
   const formData = new FormData()
   formData.append('file', file)
+
   return request({
-    url: '/statistics/sales/upload',
+    baseURL: process.env.VUE_APP_PYTHON_API,
+    url: '/api/sale-effect/upload',
     method: 'post',
     data: formData,
     headers: {
@@ -18,7 +20,8 @@ export function uploadAndAnalyzeSales(file) {
 // 获取销售分析结果
 export function getSalesResults() {
   return request({
-    url: '/statistics/sales/results',
+    baseURL: process.env.VUE_APP_PYTHON_API,
+    url: '/api/sale-effect/results',
     method: 'get'
   })
 }
@@ -26,7 +29,8 @@ export function getSalesResults() {
 // 获取销售分析总览数据
 export function getSalesOverview(params) {
   return request({
-    url: '/statistics/sales/overview',
+    baseURL: process.env.VUE_APP_PYTHON_API,
+    url: '/api/sale-effect/overview',
     method: 'get',
     params: params
   })
@@ -35,11 +39,12 @@ export function getSalesOverview(params) {
 // 预测销量趋势
 export function predictSalesTrend(params) {
   return request({
-    url: 'http://localhost:8085/api/sales/predict',
+    baseURL: process.env.VUE_APP_PYTHON_API,
+    url: '/api/sales/predict',
     method: 'post',
     data: params,
     headers: {
       'Content-Type': 'application/json'
     }
   })
-}
+}

+ 0 - 142
src/store/analysis.js

@@ -1,142 +0,0 @@
-import * as api from '../api/client'
-
-const LS_KEY_RESULTS = 'analysis_results'
-const LS_KEY_SELECTED_SKU = 'analysis_selectedSku'
-
-function loadFromLocalStorage() {
-  try {
-    const raw = localStorage.getItem(LS_KEY_RESULTS)
-    const res = raw ? JSON.parse(raw) : {}
-    const sel = localStorage.getItem(LS_KEY_SELECTED_SKU) || ''
-    return { res, sel }
-  } catch (e) {
-    return { res: {}, sel: '' }
-  }
-}
-
-function saveToLocalStorage(results, selectedSku) {
-  try {
-    localStorage.setItem(LS_KEY_RESULTS, JSON.stringify(results || {}))
-    localStorage.setItem(LS_KEY_SELECTED_SKU, selectedSku || '')
-  } catch (e) {}
-}
-
-function pickFirstSku(obj) {
-  const keys = Object.keys(obj || {})
-  const first = keys.find(k => k !== '_analysis_summary_')
-  return first || ''
-}
-
-const state = () => {
-  const cached = loadFromLocalStorage()
-  return {
-    results: cached.res,
-    loading: false,
-    error: null,
-    selectedSku: cached.sel
-  }
-}
-
-const mutations = {
-  SET_RESULTS(state, payload) {
-    state.results = payload || {}
-  },
-  SET_LOADING(state, payload) {
-    state.loading = !!payload
-  },
-  SET_ERROR(state, payload) {
-    state.error = payload || null
-  },
-  SET_SELECTED_SKU(state, payload) {
-    state.selectedSku = payload || ''
-  }
-}
-
-const actions = {
-  async uploadFile({ commit }, file) {
-    commit('SET_LOADING', true)
-    commit('SET_ERROR', null)
-    try {
-      const res = await api.uploadFile(file)
-      return res
-    } catch (e) {
-      commit('SET_ERROR', (e && e.message) || '上传失败')
-      throw e
-    } finally {
-      commit('SET_LOADING', false)
-    }
-  },
-  async analyzeFile({ commit }, file) {
-    commit('SET_LOADING', true)
-    commit('SET_ERROR', null)
-    try {
-      const response = await api.analyzeFile(file)
-      const results = response && response.data ? response.data : {}
-      commit('SET_RESULTS', results)
-      const firstSku = pickFirstSku(results)
-      commit('SET_SELECTED_SKU', firstSku || '')
-      saveToLocalStorage(results, firstSku || '')
-    } catch (e) {
-      if (e && (e.code === 'ECONNABORTED' || (e.message && e.message.indexOf('timeout') >= 0))) {
-        commit('SET_ERROR', '分析超时,数据量较大,请稍后重试或联系管理员')
-      } else {
-        const msg = (e && e.response && e.response.data && e.response.data.message) || (e && e.message) || '分析失败'
-        commit('SET_ERROR', msg)
-      }
-      throw e
-    } finally {
-      commit('SET_LOADING', false)
-    }
-  },
-  async analyzeProductLifecycle({ commit }, file) {
-    commit('SET_LOADING', true)
-    commit('SET_ERROR', null)
-    try {
-      const response = await api.analyzeProductLifecycle(file)
-      return response
-    } catch (e) {
-      commit('SET_ERROR', (e && e.message) || '商品生命周期分析失败')
-      throw e
-    } finally {
-      commit('SET_LOADING', false)
-    }
-  },
-  async fetchResults({ commit, state }) {
-    commit('SET_LOADING', true)
-    commit('SET_ERROR', null)
-    try {
-      const results = await api.getResults()
-      if (results && !results.message) {
-        commit('SET_RESULTS', results)
-        let selectedSku = state.selectedSku
-        if (!selectedSku || !results[selectedSku]) {
-          selectedSku = pickFirstSku(results)
-          commit('SET_SELECTED_SKU', selectedSku)
-        }
-        saveToLocalStorage(results, selectedSku)
-      }
-    } catch (e) {
-      commit('SET_ERROR', (e && e.message) || '获取结果失败')
-    } finally {
-      commit('SET_LOADING', false)
-    }
-  },
-  selectSku({ commit, state }, sku) {
-    const next = sku || ''
-    commit('SET_SELECTED_SKU', next)
-    saveToLocalStorage(state.results, next)
-  },
-  clearResults({ commit }) {
-    commit('SET_RESULTS', {})
-    commit('SET_SELECTED_SKU', '')
-    commit('SET_ERROR', null)
-    saveToLocalStorage({}, '')
-  }
-}
-
-export default {
-  namespaced: true,
-  state,
-  mutations,
-  actions
-}

+ 1 - 5
src/store/index.js

@@ -6,8 +6,6 @@ import user from './modules/user'
 import tagsView from './modules/tagsView'
 import permission from './modules/permission'
 import settings from './modules/settings'
-import analysis from './analysis'
-import spuAnalysis from './spuAnalysis'
 import getters from './getters'
 
 Vue.use(Vuex)
@@ -19,9 +17,7 @@ const store = new Vuex.Store({
     user,
     tagsView,
     permission,
-    settings,
-    analysis,
-    spuAnalysis
+    settings
   },
   getters
 })

+ 0 - 142
src/store/spuAnalysis.js

@@ -1,142 +0,0 @@
-import * as api from '../api/client'
-
-const LS_KEY_RESULTS = 'spu_analysis_results'
-const LS_KEY_SELECTED_SPU = 'spu_analysis_selectedSpu'
-
-function loadFromLocalStorage() {
-  try {
-    const raw = localStorage.getItem(LS_KEY_RESULTS)
-    const res = raw ? JSON.parse(raw) : {}
-    const sel = localStorage.getItem(LS_KEY_SELECTED_SPU) || ''
-    return { res, sel }
-  } catch (e) {
-    return { res: {}, sel: '' }
-  }
-}
-
-function saveToLocalStorage(results, selectedSpu) {
-  try {
-    localStorage.setItem(LS_KEY_RESULTS, JSON.stringify(results || {}))
-    localStorage.setItem(LS_KEY_SELECTED_SPU, selectedSpu || '')
-  } catch (e) {}
-}
-
-function pickFirstSpu(obj) {
-  const keys = Object.keys(obj || {})
-  const first = keys.find(k => k !== '_analysis_summary_')
-  return first || ''
-}
-
-const state = () => {
-  const cached = loadFromLocalStorage()
-  return {
-    results: cached.res,
-    loading: false,
-    error: null,
-    selectedSpu: cached.sel
-  }
-}
-
-const mutations = {
-  SET_RESULTS(state, payload) {
-    state.results = payload || {}
-  },
-  SET_LOADING(state, payload) {
-    state.loading = !!payload
-  },
-  SET_ERROR(state, payload) {
-    state.error = payload || null
-  },
-  SET_SELECTED_SPU(state, payload) {
-    state.selectedSpu = payload || ''
-  }
-}
-
-const actions = {
-  async uploadFile({ commit }, file) {
-    commit('SET_LOADING', true)
-    commit('SET_ERROR', null)
-    try {
-      const res = await api.uploadSpuFile(file)
-      return res
-    } catch (e) {
-      commit('SET_ERROR', (e && e.message) || '上传失败')
-      throw e
-    } finally {
-      commit('SET_LOADING', false)
-    }
-  },
-  async analyzeFile({ commit }, file) {
-    commit('SET_LOADING', true)
-    commit('SET_ERROR', null)
-    try {
-      const response = await api.analyzeSpuFile(file)
-      const results = response && response.data ? response.data : {}
-      commit('SET_RESULTS', results)
-      const firstSpu = pickFirstSpu(results)
-      commit('SET_SELECTED_SPU', firstSpu || '')
-      saveToLocalStorage(results, firstSpu || '')
-    } catch (e) {
-      if (e && (e.code === 'ECONNABORTED' || (e.message && e.message.indexOf('timeout') >= 0))) {
-        commit('SET_ERROR', '分析超时,数据量较大,请稍后重试或联系管理员')
-      } else {
-        const msg = (e && e.response && e.response.data && e.response.data.message) || (e && e.message) || '分析失败'
-        commit('SET_ERROR', msg)
-      }
-      throw e
-    } finally {
-      commit('SET_LOADING', false)
-    }
-  },
-  async analyzeProductLifecycle({ commit }, file) {
-    commit('SET_LOADING', true)
-    commit('SET_ERROR', null)
-    try {
-      const response = await api.analyzeProductLifecycle(file)
-      return response
-    } catch (e) {
-      commit('SET_ERROR', (e && e.message) || '商品生命周期分析失败')
-      throw e
-    } finally {
-      commit('SET_LOADING', false)
-    }
-  },
-  async fetchResults({ commit, state }) {
-    commit('SET_LOADING', true)
-    commit('SET_ERROR', null)
-    try {
-      const results = await api.getSpuResults()
-      if (results && !results.message) {
-        commit('SET_RESULTS', results)
-        let selectedSpu = state.selectedSpu
-        if (!selectedSpu || !results[selectedSpu]) {
-          selectedSpu = pickFirstSpu(results)
-          commit('SET_SELECTED_SPU', selectedSpu)
-        }
-        saveToLocalStorage(results, selectedSpu)
-      }
-    } catch (e) {
-      commit('SET_ERROR', (e && e.message) || '获取结果失败')
-    } finally {
-      commit('SET_LOADING', false)
-    }
-  },
-  selectSpu({ commit, state }, spu) {
-    const next = spu || ''
-    commit('SET_SELECTED_SPU', next)
-    saveToLocalStorage(state.results, next)
-  },
-  clearResults({ commit }) {
-    commit('SET_RESULTS', {})
-    commit('SET_SELECTED_SPU', '')
-    commit('SET_ERROR', null)
-    saveToLocalStorage({}, '')
-  }
-}
-
-export default {
-  namespaced: true,
-  state,
-  mutations,
-  actions
-}

+ 103 - 33
src/views/lifecycle/hotProductAnalysis/index.vue

@@ -321,9 +321,17 @@ import {
   analyzeSpuHotProductWithFile,
   getHotProductResults,
   getSpuHotProductResults
-} from '@/api/client'
+} from '@/api/lifecycle'
 
 const STORAGE_KEY = 'hotproduct_analysis_active_view'
+const CACHE_CONFIG = {
+  sku: {
+    resultsKey: 'hotproduct_analysis_sku_results'
+  },
+  spu: {
+    resultsKey: 'hotproduct_analysis_spu_results'
+  }
+}
 const hotProductLevels = ['超级爆款', '潜力爆款', '常规款', '清货款']
 const metricDefinitions = [
   { key: 'sales_heat', label: '销售热度(单位时间销量)', weight: '40%', className: 'bg-blue' },
@@ -364,6 +372,16 @@ function createUploadState() {
   }
 }
 
+function loadCachedResults(view) {
+  const config = CACHE_CONFIG[view]
+  try {
+    const rawResults = localStorage.getItem(config.resultsKey)
+    return rawResults ? JSON.parse(rawResults) : null
+  } catch (e) {
+    return null
+  }
+}
+
 export default {
   name: 'HotProductAnalysis',
   data() {
@@ -378,8 +396,8 @@ export default {
         spu: ''
       },
       resultsByView: {
-        sku: null,
-        spu: null
+        sku: loadCachedResults('sku'),
+        spu: loadCachedResults('spu')
       },
       uploadByView: {
         sku: createUploadState(),
@@ -519,14 +537,24 @@ export default {
       if (view === this.activeView) return
       this.activeView = view
     },
-    setLoading(value) {
-      this.$set(this.loadingByView, this.activeView, value)
-    },
     setError(value) {
       this.$set(this.errorByView, this.activeView, value || '')
     },
-    setResults(value) {
-      this.$set(this.resultsByView, this.activeView, value || {})
+    setViewLoading(view, value) {
+      this.$set(this.loadingByView, view, value)
+    },
+    setViewError(view, value) {
+      this.$set(this.errorByView, view, value || '')
+    },
+    setViewResults(view, value) {
+      this.$set(this.resultsByView, view, value || {})
+      this.persistViewResults(view)
+    },
+    persistViewResults(view) {
+      const config = CACHE_CONFIG[view]
+      try {
+        localStorage.setItem(config.resultsKey, JSON.stringify(this.resultsByView[view] || {}))
+      } catch (e) {}
     },
     extractResultsPayload(response) {
       if (!response) return null
@@ -538,12 +566,47 @@ export default {
     fetchCurrentResults() {
       if (this.resultsByView[this.activeView]) return
 
-      this.currentConfig.fetchResults().then(response => {
+      this.fetchResultsByView(this.activeView).catch(() => {})
+    },
+    fetchResultsByView(view) {
+      const config = ANALYSIS_CONFIG[view]
+      return config.fetchResults().then(response => {
         const results = this.extractResultsPayload(response)
         if (results) {
-          this.setResults(results)
+          this.setViewResults(view, results)
         }
-      }).catch(() => {})
+        return results
+      })
+    },
+    refreshAllResults() {
+      return Promise.all(['sku', 'spu'].map(view => this.fetchResultsByView(view).catch(() => null)))
+    },
+    analyzeAllWithFile(file) {
+      const tasks = ['sku', 'spu'].map(view => {
+        const config = ANALYSIS_CONFIG[view]
+        this.setViewLoading(view, true)
+        this.setViewError(view, '')
+        return config.analyze(file).then(response => {
+          if (response && response.success) {
+            this.setViewResults(view, response.data || {})
+            return { view, success: true, response }
+          }
+
+          const message = response && (response.message || response.msg) ? response.message || response.msg : '分析失败'
+          this.setViewError(view, message)
+          return { view, success: false, message }
+        }).catch(error => {
+          const message = (error && error.response && error.response.data && error.response.data.message) ||
+            (error && error.message) ||
+            '文件上传失败,请重试'
+          this.setViewError(view, message)
+          return { view, success: false, error, message }
+        }).finally(() => {
+          this.setViewLoading(view, false)
+        })
+      })
+
+      return Promise.all(tasks)
     },
     handleFileChange(file, fileList) {
       const upload = this.upload
@@ -572,42 +635,49 @@ export default {
       }
       return true
     },
-    customUpload(options) {
+    uploadAllHotProductLevels(options) {
       const file = options.file
       const upload = this.upload
 
-      this.setLoading(true)
-      this.setError('')
+      return this.analyzeAllWithFile(file).then(results => {
+        const failed = results.filter(item => !item.success)
+        const succeeded = results.filter(item => item.success)
+
+        if (succeeded.length > 0) {
+          ;['sku', 'spu'].forEach(view => {
+            this.uploadByView[view].fileName = upload.pendingFileName || file.name
+            this.uploadByView[view].pendingFileName = ''
+          })
+
+          if (failed.length === 0) {
+            this.$message.success('SKU/SPU爆品分析完成')
+          } else {
+            this.$message.warning(failed.map(item => `${ANALYSIS_CONFIG[item.view].entityName}: ${item.message}`).join(';'))
+          }
+
+          return this.refreshAllResults().then(() => results)
+        }
 
-      this.currentConfig.analyze(file).then(response => {
-        this.setLoading(false)
-        if (response && response.success) {
-          this.setResults(response.data || {})
-          upload.fileName = upload.pendingFileName || file.name
-          upload.pendingFileName = ''
-          this.$message.success(`${this.currentConfig.entityName}爆品分析完成`)
+        const message = failed.map(item => `${ANALYSIS_CONFIG[item.view].entityName}: ${item.message}`).join(';') || '分析失败'
+        this.setError(message)
+        this.$message.error(message)
+        throw new Error(message)
+      }).then(results => {
+        if (results.some(item => item.success)) {
           this.$nextTick(() => {
             this.renderCharts()
           })
-          options.onSuccess(response)
-        } else {
-          const message = response && (response.message || response.msg) ? response.message || response.msg : '分析失败'
-          this.setError(message)
-          this.$message.error(message)
-          options.onError(new Error(message))
+          options.onSuccess({ success: results.every(item => item.success), data: this.resultsByView })
         }
       }).catch(error => {
-        this.setLoading(false)
-        const message = (error && error.response && error.response.data && error.response.data.message) ||
-          (error && error.message) ||
-          '文件上传失败,请重试'
-        this.setError(message)
-        this.$message.error(message)
         options.onError(error)
       }).finally(() => {
         this.resetUploaderSelection()
       })
     },
+    customUpload(options) {
+      return this.uploadAllHotProductLevels(options)
+    },
     submitUpload() {
       const target = this.$refs.toolbarUpload
       const fileList = target && target.uploadFiles ? target.uploadFiles : []

+ 59 - 25
src/views/lifecycle/lifecycleAnalysis/index.vue

@@ -241,11 +241,21 @@
 </template>
 
 <script>
-import { analyzeFile, analyzeSpuFile, getResults, getSpuResults } from '@/api/client'
+import { analyzeFile, analyzeSpuFile, getResults, getSpuResults } from '@/api/lifecycle'
 import { Chart } from 'chart.js'
 import { formatCurrency, formatDate } from '../../../utils/format'
 
 const STORAGE_KEY = 'lifecycle_analysis_active_view'
+const CACHE_CONFIG = {
+  sku: {
+    resultsKey: 'analysis_results',
+    selectedKey: 'analysis_selectedSku'
+  },
+  spu: {
+    resultsKey: 'spu_analysis_results',
+    selectedKey: 'spu_analysis_selectedSpu'
+  }
+}
 const stageOrder = ['引入期', '成长期', '成熟期', '衰退期']
 const breakdownMapping = [
   { key: 'sufficient_time', label: '时间长度≥120天' },
@@ -264,11 +274,6 @@ const ANALYSIS_CONFIG = {
     label: 'SKU分析',
     entityName: 'SKU',
     entityDisplayLabel: 'SKU编码',
-    moduleName: 'analysis',
-    selectAction: 'analysis/selectSku',
-    setSelectedMutation: 'analysis/SET_SELECTED_SKU',
-    setResultsMutation: 'analysis/SET_RESULTS',
-    selectedKey: 'selectedSku',
     maxSizeMB: 300,
     exportPrefix: 'sku_lifecycle_results',
     fetchResults: getResults,
@@ -278,11 +283,6 @@ const ANALYSIS_CONFIG = {
     label: 'SPU分析',
     entityName: 'SPU',
     entityDisplayLabel: 'SPU名称',
-    moduleName: 'spuAnalysis',
-    selectAction: 'spuAnalysis/selectSpu',
-    setSelectedMutation: 'spuAnalysis/SET_SELECTED_SPU',
-    setResultsMutation: 'spuAnalysis/SET_RESULTS',
-    selectedKey: 'selectedSpu',
     maxSizeMB: 500,
     exportPrefix: 'spu_lifecycle_results',
     fetchResults: getSpuResults,
@@ -290,6 +290,19 @@ const ANALYSIS_CONFIG = {
   }
 }
 
+function loadCachedAnalysis(view) {
+  const config = CACHE_CONFIG[view]
+  try {
+    const rawResults = localStorage.getItem(config.resultsKey)
+    return {
+      results: rawResults ? JSON.parse(rawResults) : {},
+      selected: localStorage.getItem(config.selectedKey) || ''
+    }
+  } catch (e) {
+    return { results: {}, selected: '' }
+  }
+}
+
 function createUploadState() {
   return {
     isUploading: false,
@@ -306,6 +319,10 @@ export default {
       activeView: localStorage.getItem(STORAGE_KEY) === 'spu' ? 'spu' : 'sku',
       trendChart: null,
       stageCompareChart: null,
+      cache: {
+        sku: loadCachedAnalysis('sku'),
+        spu: loadCachedAnalysis('spu')
+      },
       uploads: {
         sku: createUploadState(),
         spu: createUploadState()
@@ -325,24 +342,21 @@ export default {
     upload() {
       return this.uploads[this.activeView]
     },
-    currentState() {
-      const state = this.$store && this.$store.state && this.$store.state[this.currentConfig.moduleName]
-      return state || {}
+    currentCache() {
+      return this.cache[this.activeView] || { results: {}, selected: '' }
     },
     results() {
-      return this.currentState.results || {}
+      return this.currentCache.results || {}
     },
     hasResults() {
       return Object.keys(this.results || {}).length > 0
     },
     selectedValue: {
       get() {
-        return this.currentState[this.currentConfig.selectedKey] || ''
+        return this.currentCache.selected || ''
       },
       set(value) {
-        if (this.$store) {
-          this.$store.dispatch(this.currentConfig.selectAction, value)
-        }
+        this.setSelectedValue(value)
       }
     },
     entityOptions() {
@@ -508,6 +522,26 @@ export default {
     getCurrentUploadState() {
       return this.uploads[this.activeView]
     },
+    persistCurrentCache() {
+      const config = CACHE_CONFIG[this.activeView]
+      const cache = this.currentCache
+      try {
+        localStorage.setItem(config.resultsKey, JSON.stringify(cache.results || {}))
+        localStorage.setItem(config.selectedKey, cache.selected || '')
+      } catch (e) {}
+    },
+    setCurrentResults(results) {
+      this.$set(this.cache, this.activeView, Object.assign({}, this.currentCache, {
+        results: results || {}
+      }))
+      this.persistCurrentCache()
+    },
+    setSelectedValue(value) {
+      this.$set(this.cache, this.activeView, Object.assign({}, this.currentCache, {
+        selected: value || ''
+      }))
+      this.persistCurrentCache()
+    },
     extractResultsPayload(response) {
       if (!response) return null
       if (response.success && response.data) return response.data
@@ -534,18 +568,18 @@ export default {
       this.currentConfig.fetchResults().then(response => {
         const results = this.extractResultsPayload(response)
         if (results) {
-          this.$store.commit(this.currentConfig.setResultsMutation, results)
+          this.setCurrentResults(results)
           const first = this.pickFirstValue(results)
-          this.$store.commit(this.currentConfig.setSelectedMutation, first)
+          this.setSelectedValue(first)
           this.$nextTick(() => {
             this.renderTrend()
             this.renderStageCompare()
           })
         } else {
-          this.$store.commit(this.currentConfig.setResultsMutation, {})
+          this.setCurrentResults({})
         }
       }).catch(() => {
-        this.$store.commit(this.currentConfig.setResultsMutation, {})
+        this.setCurrentResults({})
         this.destroyCharts()
       })
     },
@@ -585,9 +619,9 @@ export default {
         upload.isUploading = false
         if (response && response.success) {
           const results = response.data || {}
-          this.$store.commit(this.currentConfig.setResultsMutation, results)
+          this.setCurrentResults(results)
           const first = this.pickFirstValue(results)
-          this.$store.commit(this.currentConfig.setSelectedMutation, first)
+          this.setSelectedValue(first)
           upload.fileName = upload.pendingFileName || file.name
           upload.pendingFileName = ''
           this.$modal.msgSuccess(`${this.currentConfig.entityName}生命周期分析完成`)

+ 48 - 21
src/views/lifecycle/overview/index.vue

@@ -165,11 +165,22 @@
 </template>
 
 <script>
-import { analyzeFile, getResults } from '@/api/client'
+import { analyzeFile, getResults } from '@/api/lifecycle'
 import { getToken } from '@/utils/auth'
 import * as echarts from 'echarts'
 require('echarts/theme/macarons')
 
+const CACHE_KEY_RESULTS = 'analysis_results'
+
+function loadCachedResults() {
+  try {
+    const rawResults = localStorage.getItem(CACHE_KEY_RESULTS)
+    return rawResults ? JSON.parse(rawResults) : {}
+  } catch (e) {
+    return {}
+  }
+}
+
 export default {
   name: 'LifecycleOverview',
   data() {
@@ -180,7 +191,7 @@ export default {
       avgDurationChart: null,
       funnelChart: null,
       // 数据
-      results: {},
+      results: loadCachedResults(),
       // 计算属性数据
       orderCount: 0,
       skuCount: 0,
@@ -220,6 +231,12 @@ export default {
   mounted() {
     this.$nextTick(() => {
       this.initCharts()
+      if (this.hasResults) {
+        this.calculateMetrics()
+        this.renderCharts()
+      } else {
+        this.getList()
+      }
     })
     // 监听窗口大小变化
     window.addEventListener('resize', this.handleResize)
@@ -243,7 +260,7 @@ export default {
   methods: {
     /** 文件选择改变处理 */
     handleFileChange(file, fileList) {
-      if (this.upload.ignoreChange) return
+      if (this.upload.ignoreFileChange) return
       if (!fileList || fileList.length === 0) return
       if (!file || !file.raw) return
 
@@ -284,29 +301,20 @@ export default {
           this.$modal.msgSuccess('文件上传并分析成功')
           this.upload.fileName = this.upload.pendingFileName || file.name
           this.upload.pendingFileName = ''
-          this.results = response.data || {}
-          this.calculateMetrics()
-          this.$nextTick(() => {
-            this.renderCharts()
-          })
+          this.setResults(response.data || {})
           options.onSuccess(response)
         } else {
           const message = (response && response.message) || '分析失败'
           this.$modal.msgError(message)
           options.onError(new Error(message))
         }
-        if (this.$refs.upload) {
-          this.upload.ignoreFileChange = true
-          this.$refs.upload.clearFiles()
-          this.$nextTick(() => {
-            this.upload.ignoreFileChange = false
-          })
-        }
       }).catch(error => {
         this.upload.isUploading = false
         const errorMsg = (error && error.response && error.response.data && error.response.data.message) || error.message || '文件上传失败,请重试'
         this.$modal.msgError(errorMsg)
         options.onError(error)
+      }).finally(() => {
+        this.resetUploaderSelection()
       })
     },
     /** 提交上传文件 */
@@ -323,18 +331,37 @@ export default {
       if (this.$refs.upload) this.$refs.upload.clearFiles()
       if (this.$refs.toolbarUpload) this.$refs.toolbarUpload.clearFiles()
     },
+    resetUploaderSelection() {
+      if (!this.$refs.upload) return
+      this.upload.ignoreFileChange = true
+      this.$refs.upload.clearFiles()
+      this.$nextTick(() => {
+        this.upload.ignoreFileChange = false
+      })
+    },
+    persistResults() {
+      try {
+        localStorage.setItem(CACHE_KEY_RESULTS, JSON.stringify(this.results || {}))
+      } catch (e) {}
+    },
+    setResults(results) {
+      this.results = results || {}
+      this.persistResults()
+      this.calculateMetrics()
+      this.$nextTick(() => {
+        this.renderCharts()
+      })
+    },
     /** 获取生命周期分析结果 */
     getList() {
       getResults().then(response => {
         if (response && response.success && response.data) {
-          this.results = response.data || {}
-          this.calculateMetrics()
-          this.$nextTick(() => {
-            this.renderCharts()
-          })
+          this.setResults(response.data || {})
         }
       }).catch(() => {
-        this.results = {}
+        if (!this.hasResults) {
+          this.setResults({})
+        }
       })
     },
     /** 计算关键指标 */

+ 35 - 15
src/views/lifecycle/simulation/index.vue

@@ -148,18 +148,36 @@
 
 <script>
 import { Chart } from 'chart.js'
-import { getResults, simulateSkuLifecycleStrategy, getSkuLifecycleInsights } from '@/api/client'
+import { getResults, simulateSkuLifecycleStrategy, getSkuLifecycleInsights } from '@/api/lifecycle'
 import { formatCurrency, formatDate } from '../../../utils/format'
 
+const LS_KEY_RESULTS = 'analysis_results'
+const LS_KEY_SELECTED_SKU = 'analysis_selectedSku'
+
+function loadCachedAnalysis() {
+  try {
+    const rawResults = localStorage.getItem(LS_KEY_RESULTS)
+    return {
+      results: rawResults ? JSON.parse(rawResults) : {},
+      selectedSku: localStorage.getItem(LS_KEY_SELECTED_SKU) || ''
+    }
+  } catch (e) {
+    return { results: {}, selectedSku: '' }
+  }
+}
+
 export default {
   name: 'LifecycleSimulation',
   data() {
+    const cached = loadCachedAnalysis()
     return {
       simulationLoading: false,
       insightsLoading: false,
       comparisonChart: null,
       simulationResult: null,
       insights: null,
+      results: cached.results,
+      selectedSkuValue: cached.selectedSku,
       strategyConfig: {
         type: 'promotion',
         intensity: 0.5,
@@ -176,19 +194,13 @@ export default {
     }
   },
   computed: {
-    results() {
-      const state = this.$store && this.$store.state && this.$store.state.analysis
-      return (state && state.results) || {}
-    },
     selectedSku: {
       get() {
-        const state = this.$store && this.$store.state && this.$store.state.analysis
-        return (state && state.selectedSku) || ''
+        return this.selectedSkuValue || ''
       },
       set(value) {
-        if (this.$store) {
-          this.$store.dispatch('analysis/selectSku', value)
-        }
+        this.selectedSkuValue = value || ''
+        this.persistCache()
       }
     },
     skuOptions() {
@@ -214,7 +226,7 @@ export default {
     }
     if (!this.selectedSku || !this.results[this.selectedSku]) {
       const first = this.skuOptions[0] || ''
-      if (first) this.$store.dispatch('analysis/selectSku', first)
+      if (first) this.selectedSku = first
     }
     this.initializeForSku()
   },
@@ -228,7 +240,7 @@ export default {
     results() {
       if (!this.results || !this.results[this.selectedSku]) {
         const first = this.skuOptions[0] || ''
-        if (first) this.$store.dispatch('analysis/selectSku', first)
+        if (first) this.selectedSku = first
       }
     },
     simulationResult() {
@@ -253,14 +265,22 @@ export default {
         const response = await getResults()
         const payload = response && response.data ? response.data : null
         if (payload) {
-          this.$store.commit('analysis/SET_RESULTS', payload)
+          this.results = payload
           const firstSku = this.pickFirstSku(payload)
-          this.$store.commit('analysis/SET_SELECTED_SKU', firstSku)
+          this.selectedSku = firstSku
+          this.persistCache()
         }
       } catch (e) {
-        this.$store.commit('analysis/SET_RESULTS', {})
+        this.results = {}
+        this.persistCache()
       }
     },
+    persistCache() {
+      try {
+        localStorage.setItem(LS_KEY_RESULTS, JSON.stringify(this.results || {}))
+        localStorage.setItem(LS_KEY_SELECTED_SKU, this.selectedSkuValue || '')
+      } catch (e) {}
+    },
     pickFirstSku(obj) {
       const keys = Object.keys(obj || {})
       const first = keys.find(k => k !== '_analysis_summary_')

+ 0 - 1120
src/views/lifecycle/spuAnalysis/index.vue

@@ -1,1120 +0,0 @@
-<template>
-  <div class="app-container">
-    <!-- 页面标题 -->
-    <div class="page-header">
-      <h2><i class="el-icon-data-analysis"></i> SPU生命周期详细分析</h2>
-      <p class="page-desc">单个SPU的完整生命周期分析,包含趋势、指标和阶段详情</p>
-    </div>
-
-    <div class="upload-toolbar">
-      <div class="toolbar-left">
-        <el-upload
-          ref="toolbarUpload"
-          class="toolbar-upload"
-          :limit="1"
-          accept=".xlsx,.xls,.csv"
-          :http-request="customUpload"
-          :disabled="upload.isUploading"
-          :on-change="handleFileChange"
-          :before-upload="beforeUpload"
-          :auto-upload="false"
-          :show-file-list="false"
-        >
-          <el-button plain>上传文件</el-button>
-        </el-upload>
-        <el-button :loading="upload.isUploading" type="primary" @click="submitUpload">开始分析</el-button>
-        <el-button type="success" :disabled="!hasResults" @click="exportResults">导出分析</el-button>
-      </div>
-      <div class="toolbar-status" v-if="upload.fileName">已上传:{{ upload.fileName }}</div>
-      <div class="toolbar-status" v-else-if="upload.pendingFileName">已选择:{{ upload.pendingFileName }}</div>
-      <div class="toolbar-status muted" v-else>未上传</div>
-    </div>
-
-    <!-- SPU选择 + 基础信息板块 -->
-    <div class="bg-white rounded-xl p-6 mb-20 shadow-sm">
-      <div class="flex flex-wrap items-center justify-between gap-4 mb-6">
-        <div class="flex items-center gap-3">
-          <label class="text-sm font-medium text-gray-700">选择SPU:</label>
-          <select class="border border-gray-300 rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-sm spu-select"
-                  v-model="selectedSpu">
-            <option v-for="k in spuOptions" :key="k" :value="k">{{ k }}</option>
-          </select>
-        </div>
-
-        <div class="flex items-center gap-2">
-          <span class="text-sm font-medium text-gray-700">当前阶段:</span>
-          <span class="px-3 py-1 bg-gradient-to-r from-yellow-100 to-amber-100 text-yellow-800 rounded-full text-sm font-medium flex items-center gap-2">
-            {{ currentStage }}
-          </span>
-        </div>
-      </div>
-
-      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
-        <div class="p-4 border border-gray-200 rounded-lg bg-gray-50">
-          <p class="text-xs text-gray-500 uppercase tracking-wide">SPU名称</p>
-          <p class="text-lg font-medium text-gray-800 truncate">{{ selectedSpu }}</p>
-        </div>
-        <div class="p-4 border border-gray-200 rounded-lg bg-gray-50">
-          <p class="text-xs text-gray-500 uppercase tracking-wide">生命周期完整性</p>
-          <p class="text-lg font-medium" :class="detail && detail.is_complete ? 'text-green-700' : 'text-orange-600'">
-            {{ detail && detail.is_complete ? '完整' : '不完整' }}
-            <span v-if="detail && detail.completeness_score != null" class="ml-2 text-sm text-gray-500">
-              (得分: {{ detail.completeness_score }}/100)
-            </span>
-          </p>
-        </div>
-        <div class="p-4 border border-gray-200 rounded-lg bg-gray-50">
-          <p class="text-xs text-gray-500 uppercase tracking-wide">总销售额</p>
-          <p class="text-lg font-medium text-gray-800">{{ formatCurrency(totalRevenue) }}</p>
-        </div>
-        <div class="p-4 border border-gray-200 rounded-lg bg-gray-50">
-          <p class="text-xs text-gray-500 uppercase tracking-wide">总销量</p>
-          <p class="text-lg font-medium text-gray-800">{{ totalQty }}</p>
-        </div>
-      </div>
-    </div>
-
-    <!-- 生命周期趋势图(含阶段分界可视化) -->
-    <div class="bg-white rounded-xl p-6 mb-20 shadow-sm">
-      <div class="flex justify-between items-center mb-6">
-        <h3 class="text-lg font-semibold text-gray-800">SPU生命周期趋势图</h3>
-        <div class="flex items-center gap-3">
-          <div class="flex items-center gap-2">
-            <span class="w-3 h-3 rounded-full bg-blue-500"></span>
-            <span class="text-sm text-gray-600">销售额</span>
-          </div>
-          <div class="flex items-center gap-2">
-            <span class="w-3 h-3 rounded-full bg-gray-500"></span>
-            <span class="text-sm text-gray-600">销量</span>
-          </div>
-        </div>
-      </div>
-      <div class="h-96">
-        <canvas ref="lifeCycleTrendRef"></canvas>
-      </div>
-      <div class="mt-4 space-y-2">
-        <div v-if="detail && detail.is_complete" class="text-sm">
-          <span :class="hasFourStages ? 'text-green-700 font-medium' : 'text-red-600 font-medium'">
-            <i :class="hasFourStages ? 'fa fa-check-circle' : 'fa fa-exclamation-triangle'" class="mr-1"></i>
-            {{ hasFourStages ? '阶段划分完整:已包含引入/成长/成熟/衰退四个阶段' : '阶段划分不完整:未包含所有四个标准阶段' }}
-          </span>
-        </div>
-        <div v-if="detail && !detail.is_complete && detail.completeness_score != null" class="text-sm text-orange-600">
-          <i class="fa fa-info-circle mr-1"></i>
-          不完整生命周期SPU - 完整性评估得分:{{ detail.completeness_score }}/100 (需要聓80分以上才能被评为完整周期)
-        </div>
-      </div>
-    </div>
-
-    <!-- 基本指标与阶段分析分组 -->
-    <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-20">
-      <!-- 基本指标板块 -->
-      <div class="bg-white rounded-xl p-6 shadow-sm space-y-6">
-        <h3 class="text-lg font-semibold text-gray-800">基本指标</h3>
-        <div>
-          <div class="flex justify-between mb-2">
-            <span class="text-sm text-gray-500">总销售额</span>
-            <span class="text-sm font-medium text-gray-800">{{ formatCurrency(totalRevenue) }}</span>
-          </div>
-          <div class="progress-bar"><div class="progress-value bg-blue-500" :style="{ width: revenuePct+'%' }"></div></div>
-        </div>
-        <div>
-          <div class="flex justify-between mb-2">
-            <span class="text-sm text-gray-500">总销量</span>
-            <span class="text-sm font-medium text-gray-800">{{ totalQty }}</span>
-          </div>
-          <div class="progress-bar"><div class="progress-value bg-gray-500" :style="{ width: qtyPct+'%' }"></div></div>
-        </div>
-        <div class="grid grid-cols-2 gap-4 pt-2">
-          <div class="p-4 border border-gray-200 rounded-lg bg-gray-50">
-            <p class="text-xs text-gray-500 uppercase tracking-wide">峰值销售额</p>
-            <p class="text-lg font-medium text-gray-800">{{ formatCurrency(detail && detail.peak_revenue) }}</p>
-            <p class="text-xs text-gray-400 mt-1">{{ formatDate(detail && detail.peak_revenue_date) }}</p>
-          </div>
-          <div class="p-4 border border-gray-200 rounded-lg bg-gray-50">
-            <p class="text-xs text-gray-500 uppercase tracking-wide">峰值销量</p>
-            <p class="text-lg font-medium text-gray-800">{{ detail && detail.peak_quantity != null ? detail.peak_quantity : '-' }}</p>
-            <p class="text-xs text-gray-400 mt-1">{{ formatDate(detail && detail.peak_quantity_date) }}</p>
-          </div>
-        </div>
-        <div class="p-4 border border-gray-200 rounded-lg bg-gray-50">
-          <p class="text-xs text-gray-500 uppercase tracking-wide">当前阶段说明</p>
-          <p class="text-sm text-gray-700">
-            处于 <span class="font-medium">{{ currentStage }}</span>,已持续
-            <span class="font-medium">{{ currentStageDurationDays }}</span> 天。
-          </p>
-          <p class="text-xs text-gray-600 mt-1">{{ detail && detail.details ? detail.details : '-' }}</p>
-          <p v-if="detail && !detail.is_complete && detail.next_stage_prediction" class="text-xs text-blue-600 mt-1">下一阶段预测:{{ detail.next_stage_prediction }}</p>
-        </div>
-      </div>
-
-      <!-- 阶段分析板块 -->
-      <div class="lg:col-span-2 bg-white rounded-xl p-6 shadow-sm">
-        <h3 class="text-lg font-semibold text-gray-800 mb-6">阶段分析</h3>
-        <div class="overflow-x-auto">
-          <table class="min-w-full divide-y divide-gray-200">
-            <thead>
-            <tr>
-              <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">阶段</th>
-              <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">总销售额</th>
-              <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">销售额占比</th>
-              <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">总销量</th>
-              <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">销量占比</th>
-              <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">日均销售额</th>
-              <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">日均销量</th>
-              <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">持续天数</th>
-              <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">起止日期</th>
-            </tr>
-            </thead>
-            <tbody class="bg-white divide-y divide-gray-200">
-            <tr v-for="(stats, stage) in displayStageStats" :key="stage">
-              <td class="px-4 py-3 text-sm"><span class="lifecycle-stage" :class="stageClass(stage)">{{ stage }}</span></td>
-              <td class="px-4 py-3 text-sm font-medium text-gray-800">{{ formatCurrency(stats.totalRevenue) }}</td>
-              <td class="px-4 py-3 text-sm text-gray-700">{{ formatPercent(stats.revenuePercentage) }}</td>
-              <td class="px-4 py-3 text-sm font-medium text-gray-800">{{ stats.totalQuantity != null ? stats.totalQuantity : '-' }}</td>
-              <td class="px-4 py-3 text-sm text-gray-700">{{ formatPercent(stats.quantityPercentage) }}</td>
-              <td class="px-4 py-3 text-sm text-gray-700">{{ formatCurrency(stats.avgDailyRevenue) }}</td>
-              <td class="px-4 py-3 text-sm text-gray-700">{{ stats.avgDailyQuantity != null ? stats.avgDailyQuantity : '-' }}</td>
-              <td class="px-4 py-3 text-sm text-gray-700">{{ stats.durationDays }}</td>
-              <td class="px-4 py-3 text-xs text-gray-600">{{ formatDate(stats.startDate) }} ~ {{ formatDate(stats.endDate) }}</td>
-            </tr>
-            </tbody>
-          </table>
-        </div>
-
-        <!-- 对比条形图:各阶段 销售额/销量/持续天数 -->
-        <div class="mt-8">
-          <div class="flex justify-between items-center mb-4">
-            <h4 class="text-base font-semibold text-gray-800">阶段对比条形图</h4>
-            <div class="text-xs text-gray-500">销售额/销量/持续天数</div>
-          </div>
-          <div class="h-64">
-            <canvas ref="stageCompareRef"></canvas>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- 完整性评估细则与权重 -->
-    <div class="bg-white rounded-xl p-6 mb-20 shadow-sm">
-      <div class="flex justify-between items-center mb-6">
-        <h3 class="text-lg font-semibold text-gray-800">完整性评估细项</h3>
-        <div class="text-sm" :class="detail && detail.is_complete ? 'text-green-600' : 'text-orange-600'">
-          总分 {{ detail && detail.completeness_score != null ? detail.completeness_score : 0 }}/100
-          <span class="ml-2">{{ detail && detail.is_complete ? '(已达完整标准80分)' : '(未达完整标准)' }}</span>
-        </div>
-      </div>
-      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
-        <div v-for="item in breakdownItems" :key="item.key" class="flex items-center justify-between border border-gray-200 rounded-lg px-4 py-3 bg-gray-50">
-          <div class="flex items-center gap-3">
-            <span :class="item.hit ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'" class="px-3 py-1 rounded-full text-xs font-medium">
-              {{ item.hit ? '命中' : '未命中' }}
-            </span>
-            <span class="text-sm text-gray-800">{{ item.label }}</span>
-          </div>
-          <span class="text-xs text-gray-500 font-medium">权重 {{ item.weight }}</span>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup>
-import { uploadAndAnalyze, getLifecycleResults } from '@/api/lifecycle'
-import { analyzeSpuFile, getSpuResults } from '@/api/client'
-import { getToken } from '@/utils/auth'
-import { Chart } from 'chart.js'
-import { formatCurrency, formatDate } from '../../../utils/format'
-
-const stageOrder = ['引入期', '成长期', '成熟期', '衰退期']
-
-export default {
-  name: 'LifecycleSpuDetail',
-  data() {
-    return {
-      trendChart: null,
-      stageCompareChart: null,
-      upload: {
-        // 是否显示弹出层
-        open: false,
-        // 弹出层标题
-        title: '',
-        // 是否禁用上传
-        isUploading: false,
-        // 是否更新已经存在的文件
-        updateSupport: 0,
-        // 设置上传的请求头部
-        headers: { Authorization: 'Bearer ' + getToken() },
-        // 上传的地址
-        url: process.env.VUE_APP_PYTHON_API + '/api/spu-lifecycle/upload',
-        // 文件名称
-        fileName: '',
-        // 已选择文件名称
-        pendingFileName: '',
-        // 是否忽略文件选择改变
-        ignoreFileChange: false,
-      }
-    }
-  },
-  computed: {
-    hasResults() {
-      return Object.keys(this.results || {}).length > 0
-    },
-    results() {
-      const state = this.$store && this.$store.state && this.$store.state.spuAnalysis
-      return (state && state.results) || {}
-    },
-    selectedSpu: {
-      get() {
-        const state = this.$store && this.$store.state && this.$store.state.spuAnalysis
-        return (state && state.selectedSpu) || ''
-      },
-      set(value) {
-        if (this.$store) {
-          this.$store.dispatch('spuAnalysis/selectSpu', value)
-        }
-      }
-    },
-    spuOptions() {
-      return Object.keys(this.results || {}).filter(k => k !== '_analysis_summary_')
-    },
-    detail() {
-      return (this.results && this.selectedSpu && this.results[this.selectedSpu]) || null
-    },
-    currentStage() {
-      return (this.detail && this.detail.current_stage) || '-'
-    },
-    currentStageDurationDays() {
-      const detail = this.detail || {}
-      const stats = detail.stage_statistics || {}
-      const cs = detail.current_stage
-      if (cs && stats[cs] && stats[cs].durationDays != null) return stats[cs].durationDays
-      return '-'
-    },
-    stageStats() {
-      const detail = this.detail || {}
-      return detail.stage_statistics || {}
-    },
-    displayStageStats() {
-      const detail = this.detail || {}
-      const stats = this.stageStats || {}
-      const revenue = detail.revenue_series || detail.smoothed_revenue || []
-      const qty = detail.quantity_series || detail.smoothed_quantity || []
-      const rawDates = detail.date_series || []
-      const labels = rawDates.map(d => formatDate(d))
-
-      if (detail.is_complete) {
-        const boundaries = (detail.stage_boundaries || []).map(b => formatDate(b.date))
-        const idxs = boundaries.map(d => labels.indexOf(d)).filter(i => i >= 0).sort((a, b) => a - b)
-        if (idxs.length >= 3) {
-          const ranges = [
-            { name: stageOrder[0], start: 0, end: idxs[0] },
-            { name: stageOrder[1], start: idxs[0], end: idxs[1] },
-            { name: stageOrder[2], start: idxs[1], end: idxs[2] },
-            { name: stageOrder[3], start: idxs[2], end: labels.length - 1 }
-          ]
-          const totalRev = revenue.reduce((s, v) => s + (Number(v) || 0), 0)
-          const totalQ = qty.reduce((s, v) => s + (Number(v) || 0), 0)
-          const built = {}
-          ranges.forEach(r => {
-            const revSum = revenue.slice(r.start, r.end + 1).reduce((s, v) => s + (Number(v) || 0), 0)
-            const qtySum = qty.slice(r.start, r.end + 1).reduce((s, v) => s + (Number(v) || 0), 0)
-            const days = Math.max(0, r.end - r.start + 1)
-            built[r.name] = {
-              totalRevenue: revSum,
-              revenuePercentage: totalRev ? (revSum / totalRev * 100) : 0,
-              totalQuantity: qtySum,
-              quantityPercentage: totalQ ? (qtySum / totalQ * 100) : 0,
-              avgDailyRevenue: days ? (revSum / days) : 0,
-              avgDailyQuantity: days ? (qtySum / days) : 0,
-              durationDays: days,
-              startDate: rawDates[r.start],
-              endDate: rawDates[r.end]
-            }
-          })
-          return built
-        }
-        if (stageOrder.every(s => stats[s])) return stats
-      }
-
-      const actual = {}
-      const keys = stageOrder.filter(s => stats[s]).concat(Object.keys(stats).filter(s => stageOrder.indexOf(s) === -1))
-      keys.forEach(k => {
-        const v = stats[k] || {}
-        let duration = v.durationDays
-        const sIdx = labels.indexOf(formatDate(v.startDate))
-        const eIdx = labels.indexOf(formatDate(v.endDate))
-        if (sIdx >= 0 && eIdx >= 0 && eIdx >= sIdx) {
-          duration = Math.max(0, eIdx - sIdx + 1)
-        }
-        actual[k] = Object.assign({}, v, { durationDays: duration })
-      })
-      return actual
-    },
-    breakdownItems() {
-      const detail = this.detail || {}
-      const bd = detail.completion_details || {}
-      const mapping = [
-        { key: 'sufficient_time', label: '时间长度≥120天' },
-        { key: 'reasonable_peak_position', label: '峰值位置25%-75%' },
-        { key: 'significant_growth', label: '显著增长(额≥100% 或 量≥80%)' },
-        { key: 'noticeable_decline', label: '明显衰退(额≤-35% 或 量≤-30%)' },
-        { key: 'has_lifecycle_shape', label: '生命周期形状(额CV≥0.3 且 量CV≥0.25)' },
-        { key: 'peak_significance', label: '峰值显著性(≥均值1.8x)' },
-        { key: 'data_quality_check', label: '数据质量(长度≥120,峰值>0,增/退均出现)' },
-        { key: 'cycle_completeness', label: '周期完整性(时间/峰值/增长/衰退/形状)' },
-        { key: 'trend_consistency', label: '趋势一致性(增差≤0.8 或 退差≤0.4 或同向)' }
-      ]
-      return mapping.map(m => {
-        const item = bd[m.key] || {}
-        return {
-          key: m.key,
-          label: m.label,
-          hit: !!item.hit,
-          weight: item.weight != null ? item.weight : '-'
-        }
-      })
-    },
-    hasFourStages() {
-      const stats = this.stageStats || {}
-      return stageOrder.every(s => !!stats[s])
-    },
-    totalRevenue() {
-      const detail = this.detail || {}
-      if (detail.total_revenue != null) return detail.total_revenue
-      return Object.values(this.stageStats || {}).reduce((s, v) => s + (v.totalRevenue || 0), 0)
-    },
-    totalQty() {
-      const detail = this.detail || {}
-      if (detail.total_quantity != null) return detail.total_quantity
-      return Object.values(this.stageStats || {}).reduce((s, v) => s + (v.totalQuantity || 0), 0)
-    },
-    revenuePct() {
-      return 100
-    },
-    qtyPct() {
-      return 100
-    }
-  },
-  mounted() {
-    if (!Object.keys(this.results || {}).length) {
-      this.getList()
-    }
-    if (!this.selectedSpu || !this.results[this.selectedSpu]) {
-      const first = this.spuOptions[0] || ''
-      if (first) this.$store.dispatch('spuAnalysis/selectSpu', first)
-    }
-    this.renderTrend()
-    this.renderStageCompare()
-  },
-  beforeDestroy() {
-    if (this.trendChart) this.trendChart.destroy()
-    if (this.stageCompareChart) this.stageCompareChart.destroy()
-  },
-  watch: {
-    detail() {
-      this.renderTrend()
-      this.renderStageCompare()
-    },
-    results() {
-      if (!this.results || !this.results[this.selectedSpu]) {
-        const first = this.spuOptions[0] || ''
-        if (first) this.$store.dispatch('spuAnalysis/selectSpu', first)
-      }
-    }
-  },
-  methods: {
-    /** 获取生命周期分析结果 */
-    getList() {
-      getSpuResults().then(response => {
-        const payload = response && response.data ? response.data : null
-        if (payload) {
-          const results = payload || {}
-          this.$store.commit('spuAnalysis/SET_RESULTS', results)
-          const firstSpu = this.pickFirstSpu(results)
-          this.$store.commit('spuAnalysis/SET_SELECTED_SPU', firstSpu)
-          this.$nextTick(() => {
-            this.renderTrend()
-            this.renderStageCompare()
-          })
-        }
-      }).catch(() => {
-        this.$store.commit('spuAnalysis/SET_RESULTS', {})
-      })
-    },
-    /** 文件选择改变处理 */
-    handleFileChange(file, fileList) {
-      if (this.upload.ignoreFileChange) return
-      if (!fileList || fileList.length === 0) return
-      if (!file || !file.raw) return
-
-      this.upload.pendingFileName = file.name
-      this.upload.fileName = ''
-    },
-    /** 文件上传前的校验 */
-    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 isLt500M = file.size / 1024 / 1024 < 500
-
-      if (!isExcel) {
-        this.$modal.msgError('上传文件只能是 xlsx/xls/csv 格式!')
-        return false
-      }
-      if (!isLt500M) {
-        this.$modal.msgError('上传文件大小不能超过 500MB!')
-        return false
-      }
-      return true
-    },
-    handleFileUploadProgress() {
-      this.upload.isUploading = true
-    },
-    customUpload(options) {
-      const file = options.file
-      this.upload.isUploading = true
-      analyzeSpuFile(file).then(response => {
-        this.upload.isUploading = false
-        if (response && response.success) {
-          const results = response.data || {}
-          this.$store.commit('spuAnalysis/SET_RESULTS', results)
-          const firstSpu = this.pickFirstSpu(results)
-          this.$store.commit('spuAnalysis/SET_SELECTED_SPU', firstSpu)
-          this.$modal.msgSuccess('文件上传并分析成功')
-          this.upload.fileName = this.upload.pendingFileName || file.name
-          this.upload.pendingFileName = ''
-          this.$nextTick(() => {
-            this.renderTrend()
-            this.renderStageCompare()
-          })
-          options.onSuccess(response)
-        } else {
-          this.$modal.msgError(response.msg || '分析失败')
-          options.onError(new Error(response.msg || '分析失败'))
-        }
-      }).catch(error => {
-        this.upload.isUploading = false
-        const msg = (error && error.message) || '文件上传失败,请重试'
-        this.$modal.msgError(msg)
-        options.onError(error)
-      }).finally(() => {
-        if (this.$refs.toolbarUpload) {
-          this.upload.ignoreFileChange = true
-          this.$refs.toolbarUpload.clearFiles()
-          this.$nextTick(() => {
-            this.upload.ignoreFileChange = false
-          })
-        }
-      })
-    },
-    submitUpload() {
-      const target = this.$refs.toolbarUpload
-      const fileList = target && target.uploadFiles ? target.uploadFiles : []
-      if (!fileList || fileList.length === 0) {
-        this.$modal.msgError('请选择要上传的文件')
-        return
-      }
-      target.submit()
-    },
-    formatCurrency,
-    formatDate,
-    formatPercent(n) {
-      if (n == null || Number.isNaN(n)) return '-'
-      return String(Number(n).toFixed(1)) + '%'
-    },
-    stageClass(s) {
-      if (!s) return ''
-      if (s.indexOf('引入') >= 0 || s.indexOf('导入') >= 0) return 'intro'
-      if (s.indexOf('成长') >= 0) return 'growth'
-      if (s.indexOf('成熟') >= 0) return 'maturity'
-      if (s.indexOf('衰退') >= 0) return 'decline'
-      return ''
-    },
-    normalizeDate(dateStr) {
-      if (!dateStr) return ''
-      const match = String(dateStr).match(/(\d{4}-\d{2}-\d{2})/)
-      if (match) return match[1]
-      const parts = String(dateStr).split('T')
-      return parts[0].split(' ')[0]
-    },
-    renderTrend() {
-      const canvas = this.$refs.lifeCycleTrendRef
-      if (!canvas) return
-      const detail = this.detail || {}
-      const revenue = detail.smoothed_revenue || detail.revenue_series || []
-      const qty = detail.smoothed_quantity || detail.quantity_series || []
-      const rawDates = detail.date_series || []
-      const labels = rawDates.map(d => formatDate(d))
-      const boundaries = (detail.stage_boundaries || []).map(b => ({
-        type: b.type,
-        date: b.date ? formatDate(b.date) : null,
-        index: b.index
-      }))
-
-      const stageColors = {
-        [stageOrder[0]]: 'rgba(59,130,246,0.08)',
-        [stageOrder[1]]: 'rgba(16,185,129,0.10)',
-        [stageOrder[2]]: 'rgba(245,158,11,0.10)',
-        [stageOrder[3]]: 'rgba(239,68,68,0.08)'
-      }
-
-      const segments = []
-      const stagesMap = detail.stages_map || []
-
-      console.log('renderTrend - stagesMap length:', stagesMap.length, 'labels length:', labels.length)
-      console.log('renderTrend - stagesMap sample:', stagesMap.slice(0, 10))
-      console.log('renderTrend - is_complete:', detail.is_complete)
-      console.log('renderTrend - stage_boundaries:', detail.stage_boundaries)
-
-      if (stagesMap.length > 0 && stagesMap.length === labels.length) {
-        let currentStage = stagesMap[0]
-        let segmentStart = 0
-
-        for (let i = 1; i < stagesMap.length; i++) {
-          if (stagesMap[i] !== currentStage) {
-            if (currentStage && stageColors[currentStage]) {
-              segments.push({
-                start: segmentStart,
-                end: i - 1,
-                stage: currentStage
-              })
-            }
-            currentStage = stagesMap[i]
-            segmentStart = i
-          }
-        }
-        if (currentStage && stageColors[currentStage] && segmentStart < stagesMap.length) {
-          segments.push({
-            start: segmentStart,
-            end: stagesMap.length - 1,
-            stage: currentStage
-          })
-        }
-      } else {
-        const statsObj = this.displayStageStats || {}
-        const orderedNames = stageOrder.filter(s => statsObj[s]).concat(Object.keys(statsObj).filter(s => stageOrder.indexOf(s) === -1))
-        orderedNames.forEach(name => {
-          const sIdx = labels.indexOf(formatDate(statsObj[name] && statsObj[name].startDate))
-          const eIdx = labels.indexOf(formatDate(statsObj[name] && statsObj[name].endDate))
-          if (sIdx >= 0 && eIdx >= 0 && eIdx >= sIdx) {
-            segments.push({ start: sIdx, end: eIdx, stage: name })
-          }
-        })
-      }
-
-      console.log('renderTrend - segments:', segments)
-
-      const peakRevenueDateRaw = detail.peak_revenue_date
-      const peakQuantityDateRaw = detail.peak_quantity_date
-
-      let peakRevIdx = -1
-      let peakQtyIdx = -1
-
-      if (peakRevenueDateRaw && rawDates.length > 0) {
-        const targetDate = this.normalizeDate(peakRevenueDateRaw)
-        peakRevIdx = rawDates.findIndex(d => this.normalizeDate(d) === targetDate)
-
-        if (peakRevIdx < 0 && detail.revenue_peak_idx != null) {
-          const backendIdx = detail.revenue_peak_idx
-          if (backendIdx >= 0 && backendIdx < rawDates.length) {
-            peakRevIdx = backendIdx
-          }
-        }
-      }
-
-      if (peakQuantityDateRaw && rawDates.length > 0) {
-        const targetDate = this.normalizeDate(peakQuantityDateRaw)
-        peakQtyIdx = rawDates.findIndex(d => this.normalizeDate(d) === targetDate)
-
-        if (peakQtyIdx < 0 && detail.quantity_peak_idx != null) {
-          const backendIdx = detail.quantity_peak_idx
-          if (backendIdx >= 0 && backendIdx < rawDates.length) {
-            peakQtyIdx = backendIdx
-          }
-        }
-      }
-
-      const boundaryIndices = []
-      if (boundaries && boundaries.length > 0) {
-        boundaries.forEach(b => {
-          let idx = -1
-
-          if (b.index != null && b.index >= 0 && b.index < rawDates.length) {
-            idx = b.index
-          } else if (b.date) {
-            const targetDate = this.normalizeDate(b.date)
-            idx = rawDates.findIndex(d => this.normalizeDate(d) === targetDate)
-          }
-
-          if (idx >= 0 && idx < rawDates.length) {
-            boundaryIndices.push({ index: idx, type: b.type })
-          }
-        })
-      }
-
-      if (this.trendChart) this.trendChart.destroy()
-      this.trendChart = new Chart(canvas, {
-        type: 'line',
-        data: {
-          labels: labels,
-          datasets: [
-            {
-              label: '销售额',
-              data: revenue,
-              borderColor: '#3b82f6',
-              backgroundColor: 'rgba(59,130,246,0.15)',
-              lineTension: 0.25,
-              pointRadius: 0,
-              pointHoverRadius: 6,
-              pointHoverBackgroundColor: '#3b82f6',
-              pointHoverBorderColor: '#ffffff',
-              pointHoverBorderWidth: 2
-            },
-            {
-              label: '销量',
-              data: qty,
-              borderColor: '#64748b',
-              backgroundColor: 'rgba(100,116,139,0.15)',
-              lineTension: 0.25,
-              pointRadius: 0,
-              pointHoverRadius: 6,
-              pointHoverBackgroundColor: '#64748b',
-              pointHoverBorderColor: '#ffffff',
-              pointHoverBorderWidth: 2
-            }
-          ]
-        },
-        options: {
-          responsive: true,
-          maintainAspectRatio: false,
-          tooltips: {
-            enabled: true,
-            backgroundColor: 'rgba(0,0,0,0.8)',
-            titleFontColor: '#ffffff',
-            bodyFontColor: '#ffffff',
-            borderColor: '#374151',
-            borderWidth: 1,
-            cornerRadius: 6,
-            displayColors: true,
-            callbacks: {
-              title: function(context) {
-                return '日期: ' + context[0].label
-              },
-              label: function(context, data) {
-                const datasetLabel = data.datasets[context.datasetIndex].label
-                const value = context.value
-                return datasetLabel + ': ' + Number(value).toLocaleString()
-              }
-            }
-          },
-          hover: {
-            mode: 'index',
-            intersect: false
-          },
-          scales: {
-            xAxes: [{
-              ticks: {
-                maxRotation: 0,
-                autoSkip: true,
-                maxTicksLimit: 12,
-                callback: function(value, index) {
-                  return labels[index]
-                }
-              }
-            }]
-          }
-        },
-        plugins: [{
-          id: 'stage-backgrounds',
-          beforeDatasetsDraw(chart) {
-            const ctx = chart.ctx
-            const chartArea = chart.chartArea
-            const xScale = chart.scales['x-axis-0']
-
-            console.log('stage-backgrounds plugin - segments:', segments)
-            console.log('stage-backgrounds plugin - chartArea:', chartArea)
-
-            if (!segments.length) {
-              console.log('stage-backgrounds plugin - no segments to draw')
-              return
-            }
-
-            ctx.save()
-            segments.forEach((seg, idx) => {
-              const x1 = xScale.getPixelForValue(seg.start)
-              const x2 = xScale.getPixelForValue(seg.end)
-              const color = stageColors[seg.stage] || 'rgba(0,0,0,0.04)'
-
-              console.log('Drawing segment ' + idx + ': ' + seg.stage + ', start=' + seg.start + '(' + x1 + 'px), end=' + seg.end + '(' + x2 + 'px), color=' + color)
-
-              ctx.fillStyle = color
-              ctx.fillRect(x1, chartArea.top, x2 - x1, chartArea.bottom - chartArea.top)
-
-              ctx.fillStyle = '#374151'
-              ctx.font = 'bold 12px sans-serif'
-              ctx.textAlign = 'left'
-              ctx.fillText(seg.stage, x1 + 4, chartArea.top + 14)
-            })
-            ctx.restore()
-          }
-        }, {
-          id: 'stage-markers',
-          afterDatasetsDraw(chart) {
-            const ctx = chart.ctx
-            const chartArea = chart.chartArea
-            const xScale = chart.scales['x-axis-0']
-            const yScale = chart.scales['y-axis-0']
-            ctx.save()
-
-            if (boundaryIndices.length > 0) {
-              boundaryIndices.forEach(boundary => {
-                const x = xScale.getPixelForValue(boundary.index)
-
-                ctx.strokeStyle = '#ef4444'
-                ctx.setLineDash([8, 4])
-                ctx.lineWidth = 2
-                ctx.beginPath()
-                ctx.moveTo(x, chartArea.top)
-                ctx.lineTo(x, chartArea.bottom)
-                ctx.stroke()
-                ctx.setLineDash([])
-
-                const labelText = boundary.type
-                const labelWidth = ctx.measureText(labelText).width + 8
-                const labelHeight = 16
-                const labelX = x - labelWidth / 2
-                const labelY = chartArea.top + 8
-
-                ctx.fillStyle = 'rgba(239, 68, 68, 0.1)'
-                ctx.fillRect(labelX, labelY, labelWidth, labelHeight)
-
-                ctx.strokeStyle = '#ef4444'
-                ctx.lineWidth = 1
-                ctx.strokeRect(labelX, labelY, labelWidth, labelHeight)
-
-                ctx.fillStyle = '#ef4444'
-                ctx.font = 'bold 11px sans-serif'
-                ctx.textAlign = 'center'
-                ctx.fillText(labelText, x, labelY + 12)
-              })
-            }
-
-            if (peakRevIdx >= 0 && peakRevIdx < revenue.length && revenue[peakRevIdx] != null) {
-              const x = xScale.getPixelForValue(peakRevIdx)
-              const y = yScale.getPixelForValue(revenue[peakRevIdx])
-
-              ctx.fillStyle = '#dc2626'
-              ctx.beginPath()
-              ctx.arc(x, y, 5, 0, Math.PI * 2)
-              ctx.fill()
-
-              ctx.strokeStyle = '#ffffff'
-              ctx.lineWidth = 2
-              ctx.stroke()
-
-              ctx.fillStyle = '#dc2626'
-              ctx.font = 'bold 12px sans-serif'
-              ctx.textAlign = 'left'
-              ctx.fillText('销售额峰值', x + 10, y - 10)
-
-              ctx.font = '11px sans-serif'
-              ctx.fillText('¥' + Number(revenue[peakRevIdx]).toLocaleString(), x + 10, y + 2)
-            }
-
-            if (peakQtyIdx >= 0 && peakQtyIdx < qty.length && qty[peakQtyIdx] != null) {
-              const x = xScale.getPixelForValue(peakQtyIdx)
-              const y = yScale.getPixelForValue(qty[peakQtyIdx])
-
-              ctx.fillStyle = '#2563eb'
-              ctx.beginPath()
-              ctx.arc(x, y, 5, 0, Math.PI * 2)
-              ctx.fill()
-
-              ctx.strokeStyle = '#ffffff'
-              ctx.lineWidth = 2
-              ctx.stroke()
-
-              ctx.fillStyle = '#2563eb'
-              ctx.font = 'bold 12px sans-serif'
-              ctx.textAlign = 'left'
-              ctx.fillText('销量峰值', x + 10, y - 10)
-
-              ctx.font = '11px sans-serif'
-              ctx.fillText(String(qty[peakQtyIdx]).toLocaleString() + '件', x + 10, y + 2)
-            }
-            ctx.restore()
-          }
-        }]
-      })
-    },
-    renderStageCompare() {
-      const canvas = this.$refs.stageCompareRef
-      if (!canvas) return
-      const stats = this.displayStageStats || {}
-      const orderedStages = stageOrder.filter(s => stats[s]).concat(Object.keys(stats).filter(s => stageOrder.indexOf(s) === -1))
-      const labels = orderedStages
-      const revenueData = labels.map(s => (stats[s] && stats[s].totalRevenue) || 0)
-      const qtyData = labels.map(s => (stats[s] && stats[s].totalQuantity) || 0)
-      const durationData = labels.map(s => (stats[s] && stats[s].durationDays) || 0)
-
-      if (this.stageCompareChart) this.stageCompareChart.destroy()
-      this.stageCompareChart = new Chart(canvas, {
-        type: 'bar',
-        data: {
-          labels: labels,
-          datasets: [
-            { label: '销售额', data: revenueData, backgroundColor: 'rgba(59,130,246,0.7)' },
-            { label: '销量', data: qtyData, backgroundColor: 'rgba(16,185,129,0.7)' },
-            { label: '持续天数', data: durationData, backgroundColor: 'rgba(245,158,11,0.7)' }
-          ]
-        },
-        options: {
-          responsive: true,
-          maintainAspectRatio: false,
-          scales: {
-            xAxes: [{ stacked: false }],
-            yAxes: [{ ticks: { beginAtZero: true } }]
-          }
-        }
-      })
-    },
-    formatUploadDate(date) {
-      const d = date instanceof Date ? date : new Date(date)
-      if (Number.isNaN(d.getTime())) return ''
-      const y = d.getFullYear()
-      const m = String(d.getMonth() + 1).padStart(2, '0')
-      const day = String(d.getDate()).padStart(2, '0')
-      return `${y}-${m}-${day}`
-    },
-    exportResults() {
-      if (!this.hasResults) {
-        this.$modal.msgError('暂无可导出的分析结果')
-        return
-      }
-      const payload = JSON.stringify(this.results || {}, null, 2)
-      const blob = new Blob([payload], { type: 'application/json;charset=utf-8' })
-      const url = URL.createObjectURL(blob)
-      const a = document.createElement('a')
-      a.href = url
-      a.download = `spu_lifecycle_results_${this.formatUploadDate(new Date())}.json`
-      document.body.appendChild(a)
-      a.click()
-      document.body.removeChild(a)
-      URL.revokeObjectURL(url)
-    },
-    pickFirstSpu(obj) {
-      const keys = Object.keys(obj || {})
-      const first = keys.find(k => k !== '_analysis_summary_')
-      return first || ''
-    }
-  }
-}
-</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; }
-
-.upload-toolbar {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  background: #ffffff;
-  border: 1px solid #e6eaf2;
-  border-radius: 8px;
-  padding: 12px 16px;
-  margin-bottom: 16px;
-  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
-}
-
-.toolbar-left {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-
-.toolbar-upload ::v-deep .el-upload {
-  display: inline-flex;
-}
-
-.toolbar-status {
-  font-size: 13px;
-  color: #16a34a;
-  background: #f0fdf4;
-  border: 1px solid #dcfce7;
-  border-radius: 6px;
-  padding: 6px 10px;
-}
-
-.toolbar-status.muted {
-  color: #6b7280;
-  background: #f8fafc;
-  border-color: #e2e8f0;
-}
-.p-6 { padding: 24px; }
-.p-4 { padding: 16px; }
-.px-3 { padding-left: 12px; padding-right: 12px; }
-.px-4 { padding-left: 16px; padding-right: 16px; }
-.py-1 { padding-top: 4px; padding-bottom: 4px; }
-.py-2 { padding-top: 8px; padding-bottom: 8px; }
-.py-3 { padding-top: 12px; padding-bottom: 12px; }
-.pt-2 { padding-top: 8px; }
-.mb-2 { margin-bottom: 8px; }
-.mb-4 { margin-bottom: 16px; }
-.mb-6 { margin-bottom: 24px; }
-.mb-8 { margin-bottom: 32px; }
-.mt-1 { margin-top: 4px; }
-.mt-4 { margin-top: 16px; }
-.mt-8 { margin-top: 32px; }
-.ml-2 { margin-left: 8px; }
-.mr-1 { margin-right: 4px; }
-
-.flex { display: flex; }
-.flex-wrap { flex-wrap: wrap; }
-.items-center { align-items: center; }
-.justify-between { justify-content: space-between; }
-.gap-2 { gap: 8px; }
-.gap-3 { gap: 12px; }
-.gap-4 { gap: 16px; }
-.gap-8 { gap: 20px; }
-
-.grid { display: grid; }
-.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
-.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
-.lg\:col-span-2 { grid-column: span 2 / span 2; }
-.overflow-x-auto { overflow-x: auto; }
-.min-w-full { min-width: 100%; }
-
-.text-3xl { font-size: 22px; line-height: 1.3; }
-.text-base { font-size: 16px; line-height: 1.5; }
-.text-lg { font-size: 18px; line-height: 1.5; }
-.text-sm { font-size: 14px; line-height: 1.5; }
-.text-xs { font-size: 12px; line-height: 1.4; }
-.font-bold { font-weight: 700; }
-.font-semibold { font-weight: 600; }
-.font-medium { font-weight: 500; }
-.text-left { text-align: left; }
-.uppercase { text-transform: uppercase; }
-.tracking-wide { letter-spacing: 0.04em; }
-.tracking-wider { letter-spacing: 0.06em; }
-.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-
-.text-gray-900 { color: #111827; }
-.text-gray-800 { color: #303133; }
-.text-gray-700 { color: #606266; }
-.text-gray-600 { color: #909399; }
-.text-gray-500 { color: #909399; }
-.text-gray-400 { color: #9ca3af; }
-.text-blue-600 { color: #2563eb; }
-.text-green-700 { color: #15803d; }
-.text-green-600 { color: #16a34a; }
-.text-red-700 { color: #b91c1c; }
-.text-red-600 { color: #dc2626; }
-.text-orange-600 { color: #ea580c; }
-.text-yellow-800 { color: #92400e; }
-
-.bg-white { background: #ffffff; }
-.bg-gray-50 { background: #f9fafb; }
-.bg-gray-500 { background: #6b7280; }
-.bg-blue-500 { background: #3b82f6; }
-.bg-green-100 { background: #dcfce7; }
-.bg-red-100 { background: #fee2e2; }
-
-.bg-gradient-to-r { background-image: linear-gradient(to right, var(--from-color), var(--to-color)); }
-.from-yellow-100 { --from-color: #fef9c3; }
-.to-amber-100 { --to-color: #fef3c7; }
-
-.border { border-width: 1px; border-style: solid; }
-.border-gray-200 { border-color: #e5e7eb; }
-.border-gray-300 { border-color: #d1d5db; }
-.rounded-xl { border-radius: 8px; }
-.rounded-lg { border-radius: 10px; }
-.rounded-full { border-radius: 9999px; }
-.shadow-sm { border: 1px solid #e6eaf2; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); }
-
-.h-96 { height: 400px; }
-.h-64 { height: 256px; }
-.w-3 { width: 12px; }
-.h-3 { height: 12px; }
-
-.space-y-2 > * + * { margin-top: 8px; }
-.space-y-6 > * + * { margin-top: 24px; }
-.divide-y > * + * { border-top: 1px solid #e5e7eb; }
-.divide-gray-200 > * + * { border-top-color: #e5e7eb; }
-
-.focus\:outline-none:focus { outline: none; }
-.focus\:border-transparent:focus { border-color: transparent; }
-.focus\:ring-2:focus { box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.35); }
-.focus\:ring-blue-500:focus { box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.45); }
-
-.spu-select {
-  min-width: 200px;
-  background: #ffffff;
-}
-
-table {
-  border-collapse: separate;
-  border-spacing: 0;
-}
-
-thead tr {
-  background: #f8fafc;
-}
-
-thead th:first-child {
-  border-top-left-radius: 8px;
-}
-
-thead th:last-child {
-  border-top-right-radius: 8px;
-}
-
-@media (min-width: 768px) {
-  .md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
-}
-
-@media (min-width: 1024px) {
-  .lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
-  .lg\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
-}
-
-/* 进度条基础样式 */
-.progress-bar { height: 8px; background: #f3f4f6; border-radius: 999px; overflow: hidden; }
-.progress-value { height: 100%; border-radius: 999px; }
-
-/* 阶段标签色块 */
-.lifecycle-stage { padding: 2px 8px; border-radius: 999px; font-size: 12px; }
-.lifecycle-stage.intro { background: #dbeafe; color: #1e40af; }
-.lifecycle-stage.growth { background: #dcfce7; color: #166534; }
-.lifecycle-stage.maturity { background: #fef3c7; color: #92400e; }
-.lifecycle-stage.decline { background: #fee2e2; color: #991b1b; }
-</style>

+ 0 - 1406
src/views/lifecycle/spuPred/index.vue

@@ -1,1406 +0,0 @@
-<template>
-  <div class="app-container">
-    <!-- 页面标题 -->
-    <div class="page-header">
-      <h2><i class="el-icon-s-data"></i> SPU爆品分析</h2>
-      <p class="page-desc">基于五维指标体系的爆款系数分析,快速识别超级爆款、潜力爆款、常规款和清货款</p>
-    </div>
-
-    <!-- 上传工具栏 -->
-    <div class="upload-toolbar">
-      <div class="toolbar-left">
-        <el-upload
-          ref="toolbarUpload"
-          class="toolbar-upload"
-          :limit="1"
-          accept=".xlsx,.xls,.csv"
-          :http-request="customUpload"
-          :disabled="loading"
-          :on-change="handleFileChange"
-          :before-upload="beforeUpload"
-          :auto-upload="false"
-          :show-file-list="false"
-        >
-          <el-button plain>上传文件</el-button>
-        </el-upload>
-        <el-button :loading="loading" type="primary" @click="submitUpload">
-          {{ loading ? '分析中...' : '开始分析' }}
-        </el-button>
-        <el-button type="success" :disabled="!hasResults" @click="exportResults">导出分析</el-button>
-      </div>
-      <div class="toolbar-status" v-if="fileName">已上传:{{ fileName }}</div>
-      <div class="toolbar-status" v-else-if="pendingFileName">已选择:{{ pendingFileName }}</div>
-      <div class="toolbar-status muted" v-else>未上传</div>
-    </div>
-
-    <!-- 错误提示 -->
-    <div v-if="error" class="error-alert">
-      <i class="el-icon-warning"></i>
-      {{ error }}
-    </div>
-
-    <!-- 关键指标卡片 -->
-    <div class="stats-grid mb-20">
-      <div class="stat-card">
-        <div class="stat-header">
-          <div>
-            <p class="stat-label">总SPU数</p>
-            <p class="stat-value">{{ summary.total_spu_count || 0 }}</p>
-          </div>
-          <div class="stat-icon bg-blue">
-            <i class="el-icon-files"></i>
-          </div>
-        </div>
-        <div class="stat-footer">分析对象</div>
-      </div>
-
-      <div class="stat-card">
-        <div class="stat-header">
-          <div>
-            <p class="stat-label">平均爆款系数</p>
-            <p class="stat-value">{{ summary.avg_score || '-' }}</p>
-          </div>
-          <div class="stat-icon bg-green">
-            <i class="el-icon-trend-charts"></i>
-          </div>
-        </div>
-        <div class="stat-footer success">整体表现</div>
-      </div>
-
-      <div class="stat-card">
-        <div class="stat-header">
-          <div>
-            <p class="stat-label">最高系数</p>
-            <p class="stat-value">{{ summary.max_score || '-' }}</p>
-          </div>
-          <div class="stat-icon bg-yellow">
-            <i class="el-icon-trophy"></i>
-          </div>
-        </div>
-        <div class="stat-footer warning">顶尖表现</div>
-      </div>
-
-      <div class="stat-card">
-        <div class="stat-header">
-          <div>
-            <p class="stat-label">超级爆款数</p>
-            <p class="stat-value">{{ levelDistribution['超级爆款'] || 0 }}</p>
-          </div>
-          <div class="stat-icon bg-red">
-            <i class="el-icon-star-on"></i>
-          </div>
-        </div>
-        <div class="stat-footer danger">明星产品</div>
-      </div>
-    </div>
-
-    <!-- 爆款等级分布与五维指标对比 -->
-    <div class="charts-grid mb-20">
-      <div class="chart-card">
-        <div class="chart-header">
-          <h3 class="chart-title">爆款等级分布</h3>
-          <div class="chart-subtitle">按SPU数量统计</div>
-        </div>
-        <div class="chart-container h-80">
-          <canvas ref="levelDistChartRef"></canvas>
-        </div>
-      </div>
-
-      <div class="chart-card">
-        <div class="chart-header">
-          <h3 class="chart-title">五维指标平均对比</h3>
-          <div class="chart-subtitle">各等级指标表现</div>
-        </div>
-        <div class="chart-container h-80">
-          <canvas ref="metricsCompareChartRef"></canvas>
-        </div>
-      </div>
-    </div>
-
-    <!-- SPU详情表格 -->
-    <div class="table-card mb-20">
-      <div class="table-header">
-        <h3 class="table-title">SPU详情列表</h3>
-        <div class="table-filters">
-          <el-select v-model="filterLevel" placeholder="全部等级" clearable size="small" class="filter-select">
-            <el-option value="超级爆款" label="超级爆款"></el-option>
-            <el-option value="潜力爆款" label="潜力爆款"></el-option>
-            <el-option value="常规款" label="常规款"></el-option>
-            <el-option value="清货款" label="清货款"></el-option>
-          </el-select>
-          <el-input 
-            v-model="searchKeyword" 
-            placeholder="搜索SPU或商品名称..." 
-            size="small"
-            prefix-icon="el-icon-search"
-            class="filter-input"
-            clearable
-          />
-        </div>
-      </div>
-
-      <div class="table-wrapper">
-        <table class="data-table">
-          <thead>
-            <tr>
-              <th @click="sortBy('hotproduct_score')" class="sortable">
-                爆款系数
-                <i :class="getSortIcon('hotproduct_score')"></i>
-              </th>
-              <th>等级</th>
-              <th>SPU??</th>
-              <th>商品名称</th>
-              <th @click="sortBy('total_quantity')" class="sortable">
-                总销量
-                <i :class="getSortIcon('total_quantity')"></i>
-              </th>
-              <th @click="sortBy('total_revenue')" class="sortable">
-                总销售额
-                <i :class="getSortIcon('total_revenue')"></i>
-              </th>
-              <th>操作</th>
-            </tr>
-          </thead>
-          <tbody>
-            <tr v-for="item in filteredAndSortedSpus" :key="item.spu_id" class="data-row">
-              <td>
-                <div class="score-cell">
-                  <div class="score-value">{{ item.hotproduct_score || '-' }}</div>
-                  <div class="score-bar">
-                    <div 
-                      class="score-progress" 
-                      :class="getScoreColorClass(item.hotproduct_score)"
-                      :style="{ width: ((item.hotproduct_score || 0) * 100) + '%' }"
-                    ></div>
-                  </div>
-                </div>
-              </td>
-              <td>
-                <span class="level-badge" :class="getLevelBadgeClass(item.hotproduct_level)">
-                  {{ item.hotproduct_level || '-' }}
-                </span>
-              </td>
-              <td class="text-sm">{{ item.spu_id || '-' }}</td>
-              <td class="text-sm product-title" :title="item.product_title">
-                {{ item.product_title || '-' }}
-              </td>
-              <td class="text-sm">{{ (item.raw_data && item.raw_data.total_quantity) || '-' }}</td>
-              <td class="text-sm">{{ formatCurrency((item.raw_data && item.raw_data.total_revenue) || 0) }}</td>
-              <td>
-                <el-button 
-                  type="text" 
-                  size="small"
-                  @click="showDetail(item)"
-                  class="action-btn"
-                >
-                  查看详情
-                </el-button>
-              </td>
-            </tr>
-            <tr v-if="filteredAndSortedSpus.length === 0">
-              <td colspan="7" class="empty-state">
-                <i class="el-icon-inbox"></i>
-                <p>未找到符合条件的SPU</p>
-              </td>
-            </tr>
-          </tbody>
-        </table>
-      </div>
-    </div>
-
-    <!-- SPU详情弹窗 -->
-    <el-dialog
-      :visible.sync="detailVisible"
-      :title="selectedSpu ? selectedSpu.product_title : ''"
-      width="80%"
-      top="5vh"
-      custom-class="detail-dialog"
-      :close-on-click-modal="false"
-    >
-      <div v-if="selectedSpu" class="detail-content">
-        <!-- SPU基本信息 -->
-        <div class="detail-info mb-6">
-          <p class="info-label">SPU名称: <span class="info-value">{{ selectedSpu.spu_id }}</span></p>
-        </div>
-
-        <!-- 爆款系数和等级 -->
-        <div class="detail-stats mb-6">
-          <div class="detail-stat-card bg-blue-gradient">
-            <p class="detail-stat-label">爆款系数</p>
-            <p class="detail-stat-value">{{ selectedSpu.hotproduct_score || '-' }}</p>
-          </div>
-          <div class="detail-stat-card bg-purple-gradient">
-            <p class="detail-stat-label">爆款等级</p>
-            <p class="detail-stat-value" :class="getLevelTextColor(selectedSpu.hotproduct_level)">
-              {{ selectedSpu.hotproduct_level || '-' }}
-            </p>
-          </div>
-        </div>
-
-        <!-- 五维指标雷达图 -->
-        <div class="detail-section mb-6">
-          <h4 class="section-title">五维指标雷达图</h4>
-          <div class="chart-container h-80">
-            <canvas ref="radarChartRef"></canvas>
-          </div>
-        </div>
-
-        <!-- 指标详情 -->
-        <div class="detail-section mb-6">
-          <h4 class="section-title">指标详情</h4>
-          <div class="metrics-grid">
-            <div class="metric-item">
-              <div class="metric-header">
-                <span class="metric-name">销售热度(单位时间销量)</span>
-                <span class="metric-score">{{ getMetricValue(selectedSpu, 'sales_heat') }}</span>
-              </div>
-              <div class="metric-bar">
-                <div class="metric-progress bg-blue" :style="{ width: (getMetricValue(selectedSpu, 'sales_heat') * 100) + '%' }"></div>
-              </div>
-              <p class="metric-weight">权重: 40%</p>
-            </div>
-
-            <div class="metric-item">
-              <div class="metric-header">
-                <span class="metric-name">价格接受度(实收率)</span>
-                <span class="metric-score">{{ getMetricValue(selectedSpu, 'price_acceptance') }}</span>
-              </div>
-              <div class="metric-bar">
-                <div class="metric-progress bg-green" :style="{ width: (getMetricValue(selectedSpu, 'price_acceptance') * 100) + '%' }"></div>
-              </div>
-              <p class="metric-weight">权重: 30%</p>
-            </div>
-
-            <div class="metric-item">
-              <div class="metric-header">
-                <span class="metric-name">退款稳定性(1-退款率)</span>
-                <span class="metric-score">{{ getMetricValue(selectedSpu, 'refund_stability') }}</span>
-              </div>
-              <div class="metric-bar">
-                <div class="metric-progress bg-yellow" :style="{ width: (getMetricValue(selectedSpu, 'refund_stability') * 100) + '%' }"></div>
-              </div>
-              <p class="metric-weight">权重: 10%</p>
-            </div>
-
-            <div class="metric-item">
-              <div class="metric-header">
-                <span class="metric-name">复购热度(复购率)</span>
-                <span class="metric-score">{{ getMetricValue(selectedSpu, 'repurchase_rate') }}</span>
-              </div>
-              <div class="metric-bar">
-                <div class="metric-progress bg-purple" :style="{ width: (getMetricValue(selectedSpu, 'repurchase_rate') * 100) + '%' }"></div>
-              </div>
-              <p class="metric-weight">权重: 10%</p>
-            </div>
-
-            <div class="metric-item">
-              <div class="metric-header">
-                <span class="metric-name">夜间爆发力(0-6点占比)</span>
-                <span class="metric-score">{{ getMetricValue(selectedSpu, 'night_burst') }}</span>
-              </div>
-              <div class="metric-bar">
-                <div class="metric-progress bg-indigo" :style="{ width: (getMetricValue(selectedSpu, 'night_burst') * 100) + '%' }"></div>
-              </div>
-              <p class="metric-weight">权重: 10%</p>
-            </div>
-          </div>
-        </div>
-
-        <!-- 原始数据 -->
-        <div class="detail-section mb-6">
-          <h4 class="section-title">原始数据</h4>
-          <div class="raw-data-grid">
-            <div class="raw-data-item">
-              <p class="raw-data-label">总销量</p>
-              <p class="raw-data-value">{{ getRawData(selectedSpu, 'total_quantity') }}</p>
-            </div>
-            <div class="raw-data-item">
-              <p class="raw-data-label">总销售额</p>
-              <p class="raw-data-value">{{ formatCurrency(getRawData(selectedSpu, 'total_revenue')) }}</p>
-            </div>
-            <div class="raw-data-item">
-              <p class="raw-data-label">平均价格</p>
-              <p class="raw-data-value">{{ formatCurrency(getRawData(selectedSpu, 'avg_price')) }}</p>
-            </div>
-            <div class="raw-data-item">
-              <p class="raw-data-label">天数跨度</p>
-              <p class="raw-data-value">{{ getRawData(selectedSpu, 'days_span') }}天</p>
-            </div>
-            <div class="raw-data-item">
-              <p class="raw-data-label">日均销量</p>
-              <p class="raw-data-value">{{ getRawData(selectedSpu, 'daily_sales') }}</p>
-            </div>
-            <div class="raw-data-item">
-              <p class="raw-data-label">独立买家数</p>
-              <p class="raw-data-value">{{ getRawData(selectedSpu, 'unique_buyers') }}</p>
-            </div>
-            <div class="raw-data-item">
-              <p class="raw-data-label">复购买家数</p>
-              <p class="raw-data-value">{{ getRawData(selectedSpu, 'repurchase_buyers') }}</p>
-            </div>
-            <div class="raw-data-item">
-              <p class="raw-data-label">退款率</p>
-              <p class="raw-data-value">{{ formatPercent(getRawData(selectedSpu, 'refund_rate') * 100) }}</p>
-            </div>
-          </div>
-        </div>
-
-        <!-- 运营建议 -->
-        <div class="advice-box">
-          <h4 class="advice-title">
-            <i class="el-icon-light-rain"></i>
-            运营建议
-          </h4>
-          <p class="advice-content">{{ getOperationAdvice(selectedSpu.hotproduct_level) }}</p>
-        </div>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import { Chart } from 'chart.js'
-import { analyzeSpuHotProductWithFile } from '@/api/client'
-
-export default {
-  name: 'SpuHotProductAnalysis',
-  data() {
-    return {
-      loading: false,
-      error: '',
-      results: null,
-      selectedSpu: null,
-      detailVisible: false,
-
-      filterLevel: '',
-      searchKeyword: '',
-      sortColumn: 'hotproduct_score',
-      sortOrder: 'desc',
-
-      fileName: '',
-      pendingFileName: '',
-      ignoreFileChange: false,
-
-      // Chart 实例
-      levelDistChart: null,
-      metricsCompareChart: null,
-      radarChart: null
-    }
-  },
-
-  computed: {
-    hasResults() {
-      return this.results !== null && Object.keys(this.results).length > 0
-    },
-    summary() {
-      return (this.results && this.results.summary) || {}
-    },
-    levelDistribution() {
-      return (this.results && this.results.summary && this.results.summary.level_distribution) || {}
-    },
-    spuList() {
-      if (!this.results || !this.results.spu_results) return []
-      return Object.values(this.results.spu_results)
-    },
-    filteredAndSortedSpus() {
-      let list = this.spuList.slice()
-
-      // 过滤等级
-      if (this.filterLevel) {
-        list = list.filter(item => item.hotproduct_level === this.filterLevel)
-      }
-
-      // 搜索关键词
-      if (this.searchKeyword) {
-        const keyword = String(this.searchKeyword).toLowerCase()
-        list = list.filter(item => {
-          const spuName = String(item.spu_id || '').toLowerCase()
-          const title = String(item.product_title || '').toLowerCase()
-          return spuName.includes(keyword) || title.includes(keyword)
-        })
-      }
-
-      // 排序
-      const col = this.sortColumn
-      const order = this.sortOrder
-      list.sort((a, b) => {
-        let aVal = 0
-        let bVal = 0
-
-        if (col === 'hotproduct_score') {
-          aVal = Number(a.hotproduct_score) || 0
-          bVal = Number(b.hotproduct_score) || 0
-        } else if (col === 'total_quantity') {
-          aVal = Number(a.raw_data && a.raw_data.total_quantity) || 0
-          bVal = Number(b.raw_data && b.raw_data.total_quantity) || 0
-        } else if (col === 'total_revenue') {
-          aVal = Number(a.raw_data && a.raw_data.total_revenue) || 0
-          bVal = Number(b.raw_data && b.raw_data.total_revenue) || 0
-        }
-
-        return order === 'asc' ? (aVal - bVal) : (bVal - aVal)
-      })
-
-      return list
-    }
-  },
-
-  watch: {
-    results() {
-      this.$nextTick(() => {
-        this.renderCharts()
-      })
-    },
-    detailVisible(val) {
-      if (!val && this.radarChart) {
-        this.radarChart.destroy()
-        this.radarChart = null
-      } else if (val && this.selectedSpu) {
-        this.$nextTick(() => {
-          this.renderRadarChart()
-        })
-      }
-    }
-  },
-
-  mounted() {
-    this.renderCharts()
-  },
-
-  beforeDestroy() {
-    if (this.levelDistChart) this.levelDistChart.destroy()
-    if (this.metricsCompareChart) this.metricsCompareChart.destroy()
-    if (this.radarChart) this.radarChart.destroy()
-  },
-
-  methods: {
-    handleFileChange(file, fileList) {
-      if (this.ignoreFileChange) return
-      if (!fileList || fileList.length === 0) return
-      if (!file || !file.raw) return
-
-      this.pendingFileName = file.name
-      this.fileName = ''
-    },
-
-    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 isLt500M = file.size / 1024 / 1024 < 500
-
-      if (!isExcel) {
-        this.$message.error('上传文件只能是 xlsx/xls/csv 格式!')
-        return false
-      }
-      if (!isLt500M) {
-        this.$message.error('上传文件大小不能超过 500MB!')
-        return false
-      }
-      return true
-    },
-
-    customUpload(options) {
-      const file = options.file
-      this.loading = true
-      this.error = ''
-
-      analyzeSpuHotProductWithFile(file).then(response => {
-        this.loading = false
-        if (response && response.success) {
-          this.results = response.data || {}
-          this.$message.success('文件上传并分析成功')
-          this.fileName = this.pendingFileName || file.name
-          this.pendingFileName = ''
-          this.$nextTick(() => {
-            this.renderCharts()
-          })
-          options.onSuccess(response)
-        } else {
-          const msg = (response && response.message) || '分析失败'
-          this.error = msg
-          this.$message.error(msg)
-          options.onError(new Error(msg))
-        }
-      }).catch(error => {
-        this.loading = false
-        const msg = (error && error.response && error.response.data && error.response.data.message) ||
-          (error && error.message) || '文件上传失败,请重试'
-        this.error = msg
-        this.$message.error(msg)
-        options.onError(error)
-      }).finally(() => {
-        if (this.$refs.toolbarUpload) {
-          this.ignoreFileChange = true
-          this.$refs.toolbarUpload.clearFiles()
-          this.$nextTick(() => {
-            this.ignoreFileChange = false
-          })
-        }
-      })
-    },
-
-    submitUpload() {
-      const target = this.$refs.toolbarUpload
-      const fileList = target && target.uploadFiles ? target.uploadFiles : []
-      if (!fileList || fileList.length === 0) {
-        this.$message.error('请选择要上传的文件')
-        return
-      }
-      target.submit()
-    },
-
-    renderCharts() {
-      this.renderLevelDistChart()
-      this.renderMetricsCompareChart()
-    },
-
-    renderLevelDistChart() {
-      if (this.levelDistChart) this.levelDistChart.destroy()
-
-      const canvas = this.$refs.levelDistChartRef
-      if (!canvas) return
-
-      const levels = ['超级爆款', '潜力爆款', '常规款', '清货款']
-      const data = levels.map(level => this.levelDistribution[level] || 0)
-      const colors = ['#ef4444', '#f59e0b', '#10b981', '#6b7280']
-
-      this.levelDistChart = new Chart(canvas, {
-        type: 'doughnut',
-        data: {
-          labels: levels,
-          datasets: [{
-            data,
-            backgroundColor: colors,
-            borderWidth: 2,
-            borderColor: '#fff'
-          }]
-        },
-        options: {
-          responsive: true,
-          maintainAspectRatio: false,
-          plugins: {
-            legend: {
-              position: 'right',
-              labels: {
-                padding: 15,
-                font: { size: 12 }
-              }
-            },
-            tooltip: {
-              callbacks: {
-                label: (context) => {
-                  const label = context.label || ''
-                  const value = context.parsed || 0
-                  const total = (context.dataset.data || []).reduce((a, b) => a + b, 0)
-                  const percentage = total ? ((value / total) * 100).toFixed(1) : '0.0'
-                  return `${label}: ${value} (${percentage}%)`
-                }
-              }
-            }
-          }
-        }
-      })
-    },
-
-    renderMetricsCompareChart() {
-      if (this.metricsCompareChart) this.metricsCompareChart.destroy()
-
-      const canvas = this.$refs.metricsCompareChartRef
-      if (!canvas) return
-
-      const levels = ['超级爆款', '潜力爆款', '常规款', '清货款']
-      const metricNames = ['销售热度', '价格接受度', '退款稳定性', '复购热度', '夜间爆发力']
-      const metricKeys = ['sales_heat', 'price_acceptance', 'refund_stability', 'repurchase_rate', 'night_burst']
-
-      // 计算各等级平均指标
-      const levelMetrics = {}
-      levels.forEach(level => {
-      const spus = this.spuList.filter(s => s.hotproduct_level === level)
-      if (spus.length > 0) {
-        levelMetrics[level] = metricKeys.map(key => {
-          const sum = spus.reduce((acc, spu) => acc + (Number(spu.metrics && spu.metrics[key]) || 0), 0)
-          return sum / spus.length
-        })
-      } else {
-        levelMetrics[level] = metricKeys.map(() => 0)
-      }
-      })
-
-      const baseColors = ['#ef4444', '#f59e0b', '#10b981', '#6b7280']
-      const datasets = levels.map((level, idx) => ({
-        label: level,
-        data: levelMetrics[level] || [],
-        backgroundColor: baseColors[idx] + '33',
-        borderColor: baseColors[idx],
-        borderWidth: 2
-      }))
-
-      this.metricsCompareChart = new Chart(canvas, {
-        type: 'radar',
-        data: {
-          labels: metricNames,
-          datasets
-        },
-        options: {
-          responsive: true,
-          maintainAspectRatio: false,
-          scales: {
-            r: {
-              beginAtZero: true,
-              max: 1,
-              ticks: { stepSize: 0.2 }
-            }
-          },
-          plugins: {
-            legend: { position: 'bottom' }
-          }
-        }
-      })
-    },
-
-    renderRadarChart() {
-      if (!this.selectedSpu) return
-      if (this.radarChart) this.radarChart.destroy()
-
-      const canvas = this.$refs.radarChartRef
-      if (!canvas) return
-
-      const metricNames = ['销售热度', '价格接受度', '退款稳定性', '复购热度', '夜间爆发力']
-      const metricKeys = ['sales_heat', 'price_acceptance', 'refund_stability', 'repurchase_rate', 'night_burst']
-      const data = metricKeys.map(key => Number(this.selectedSpu.metrics && this.selectedSpu.metrics[key]) || 0)
-
-      this.radarChart = new Chart(canvas, {
-        type: 'radar',
-        data: {
-          labels: metricNames,
-          datasets: [{
-            label: '指标得分',
-            data,
-            backgroundColor: 'rgba(59, 130, 246, 0.2)',
-            borderColor: 'rgb(59, 130, 246)',
-            borderWidth: 2,
-            pointBackgroundColor: 'rgb(59, 130, 246)',
-            pointBorderColor: '#fff',
-            pointHoverBackgroundColor: '#fff',
-            pointHoverBorderColor: 'rgb(59, 130, 246)'
-          }]
-        },
-        options: {
-          responsive: true,
-          maintainAspectRatio: false,
-          scales: {
-            r: {
-              beginAtZero: true,
-              max: 1,
-              ticks: { stepSize: 0.2 }
-            }
-          },
-          plugins: {
-            legend: { display: false }
-          }
-        }
-      })
-    },
-
-    showDetail(spu) {
-      this.selectedSpu = spu
-      this.detailVisible = true
-    },
-
-    closeDetail() {
-      this.detailVisible = false
-      this.selectedSpu = null
-    },
-
-    sortBy(column) {
-      if (this.sortColumn === column) {
-        this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc'
-      } else {
-        this.sortColumn = column
-        this.sortOrder = 'desc'
-      }
-    },
-
-    getSortIcon(column) {
-      if (this.sortColumn !== column) return 'el-icon-d-caret'
-      return this.sortOrder === 'asc' ? 'el-icon-caret-top' : 'el-icon-caret-bottom'
-    },
-
-    getScoreColorClass(score) {
-      const s = Number(score) || 0
-      if (s >= 0.8) return 'score-red'
-      if (s >= 0.6) return 'score-yellow'
-      if (s >= 0.4) return 'score-green'
-      return 'score-gray'
-    },
-
-    getLevelBadgeClass(level) {
-      const classes = {
-        '超级爆款': 'badge-red',
-        '潜力爆款': 'badge-yellow',
-        '常规款': 'badge-green',
-        '清货款': 'badge-gray'
-      }
-      return classes[level] || 'badge-gray'
-    },
-
-    getLevelTextColor(level) {
-      const colors = {
-        '超级爆款': 'text-red',
-        '潜力爆款': 'text-yellow',
-        '常规款': 'text-green',
-        '清货款': 'text-gray'
-      }
-      return colors[level] || 'text-gray'
-    },
-
-    getOperationAdvice(level) {
-      const advice = {
-        '超级爆款': '建议:立即加大库存备货,增加投流预算,抢占市场份额。这是明星产品,需要重点维护和推广。',
-        '潜力爆款': '建议:优化转化链路,提升用户体验,加强营销推广。这类产品有成为爆款的潜力,需要精心培育。',
-        '常规款': '建议:维持现状,持续观察市场反馈。可以尝试小规模优化,但不需要大量投入资源。',
-        '清货款': '建议:考虑清仓促销或直接下架,避免占用库存和运营资源。将精力投入到更有潜力的产品上。'
-      }
-      return advice[level] || '暂无建议'
-    },
-
-    getMetricValue(spu, key) {
-      if (!spu || !spu.metrics) return '-'
-      const val = spu.metrics[key]
-      return val != null ? String(val) : '-'
-    },
-
-    getRawData(spu, key) {
-      if (!spu || !spu.raw_data) return '-'
-      const val = spu.raw_data[key]
-      return val != null ? val : '-'
-    },
-
-    formatCurrency(val) {
-      if (val == null || val === '-') return '-'
-      const num = Number(val)
-      if (Number.isNaN(num)) return '-'
-      return '¥' + num.toFixed(2)
-    },
-
-    formatPercent(val) {
-      if (val == null || val === '-') return '-'
-      const num = Number(val)
-      if (Number.isNaN(num)) return '-'
-      return num.toFixed(2) + '%'
-    },
-
-    exportResults() {
-      if (!this.hasResults) {
-        this.$message.error('暂无可导出的分析结果')
-        return
-      }
-      const payload = JSON.stringify(this.results, null, 2)
-      const blob = new Blob([payload], { type: 'application/json;charset=utf-8' })
-      const url = URL.createObjectURL(blob)
-      const a = document.createElement('a')
-      a.href = url
-      a.download = `spu_hotproduct_results_${new Date().toISOString().slice(0, 10)}.json`
-      document.body.appendChild(a)
-      a.click()
-      document.body.removeChild(a)
-      URL.revokeObjectURL(url)
-    }
-  }
-}
-</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; }
-.mb-6 { margin-bottom: 24px; }
-
-.upload-toolbar {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  background: #ffffff;
-  border: 1px solid #e6eaf2;
-  border-radius: 8px;
-  padding: 12px 16px;
-  margin-bottom: 16px;
-  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
-}
-
-.toolbar-left {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-
-.toolbar-upload ::v-deep .el-upload {
-  display: inline-flex;
-}
-
-.toolbar-status {
-  font-size: 13px;
-  color: #16a34a;
-  background: #f0fdf4;
-  border: 1px solid #dcfce7;
-  border-radius: 6px;
-  padding: 6px 10px;
-}
-
-.toolbar-status.muted {
-  color: #6b7280;
-  background: #f8fafc;
-  border-color: #e2e8f0;
-}
-
-.error-alert {
-  background: #fef2f2;
-  border: 1px solid #fecaca;
-  border-radius: 8px;
-  padding: 12px 16px;
-  color: #dc2626;
-  margin-bottom: 16px;
-  display: flex;
-  align-items: center;
-  gap: 8px;
-
-  i {
-    font-size: 18px;
-  }
-}
-
-/* 统计卡片 */
-.stats-grid {
-  display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
-  gap: 20px;
-}
-
-.stat-card {
-  background: #ffffff;
-  border: 1px solid #e6eaf2;
-  border-radius: 8px;
-  padding: 20px;
-  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
-  transition: transform 0.2s ease, box-shadow 0.2s ease;
-
-  &:hover {
-    transform: translateY(-2px);
-    box-shadow: 0 4px 8px rgba(15, 23, 42, 0.1);
-  }
-}
-
-.stat-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: flex-start;
-  margin-bottom: 12px;
-}
-
-.stat-label {
-  font-size: 12px;
-  color: #909399;
-  text-transform: uppercase;
-  letter-spacing: 0.05em;
-  margin-bottom: 8px;
-}
-
-.stat-value {
-  font-size: 28px;
-  font-weight: 700;
-  color: #303133;
-  margin: 0;
-}
-
-.stat-icon {
-  width: 48px;
-  height: 48px;
-  border-radius: 50%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 20px;
-
-  &.bg-blue {
-    background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
-    color: #2563eb;
-  }
-
-  &.bg-green {
-    background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
-    color: #059669;
-  }
-
-  &.bg-yellow {
-    background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
-    color: #d97706;
-  }
-
-  &.bg-red {
-    background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
-    color: #dc2626;
-  }
-}
-
-.stat-footer {
-  font-size: 13px;
-  color: #606266;
-
-  &.success { color: #16a34a; }
-  &.warning { color: #d97706; }
-  &.danger { color: #dc2626; }
-}
-
-/* 图表卡片 */
-.charts-grid {
-  display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
-  gap: 20px;
-}
-
-.chart-card {
-  background: #ffffff;
-  border: 1px solid #e6eaf2;
-  border-radius: 8px;
-  padding: 24px;
-  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
-}
-
-.chart-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 24px;
-}
-
-.chart-title {
-  font-size: 18px;
-  font-weight: 600;
-  color: #303133;
-  margin: 0;
-}
-
-.chart-subtitle {
-  font-size: 13px;
-  color: #909399;
-}
-
-.chart-container {
-  position: relative;
-
-  &.h-80 {
-    height: 320px;
-  }
-}
-
-/* 表格卡片 */
-.table-card {
-  background: #ffffff;
-  border: 1px solid #e6eaf2;
-  border-radius: 8px;
-  padding: 24px;
-  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
-}
-
-.table-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 24px;
-  flex-wrap: wrap;
-  gap: 16px;
-}
-
-.table-title {
-  font-size: 18px;
-  font-weight: 600;
-  color: #303133;
-  margin: 0;
-}
-
-.table-filters {
-  display: flex;
-  gap: 12px;
-  align-items: center;
-}
-
-.filter-select {
-  width: 140px;
-}
-
-.filter-input {
-  width: 260px;
-}
-
-.table-wrapper {
-  overflow-x: auto;
-}
-
-.data-table {
-  width: 100%;
-  border-collapse: separate;
-  border-spacing: 0;
-
-  thead {
-    background: #f8fafc;
-
-    th {
-      padding: 12px 16px;
-      text-align: left;
-      font-size: 12px;
-      font-weight: 600;
-      color: #606266;
-      text-transform: uppercase;
-      letter-spacing: 0.05em;
-      border-bottom: 1px solid #e5e7eb;
-
-      &:first-child {
-        border-top-left-radius: 8px;
-      }
-
-      &:last-child {
-        border-top-right-radius: 8px;
-      }
-
-      &.sortable {
-        cursor: pointer;
-        user-select: none;
-
-        &:hover {
-          background: #f1f5f9;
-        }
-
-        i {
-          margin-left: 4px;
-          font-size: 14px;
-        }
-      }
-    }
-  }
-
-  tbody {
-    tr.data-row {
-      transition: background-color 0.2s;
-
-      &:hover {
-        background: #f9fafb;
-      }
-
-      td {
-        padding: 16px;
-        border-bottom: 1px solid #e5e7eb;
-        font-size: 14px;
-        color: #303133;
-
-        &.text-sm {
-          font-size: 13px;
-        }
-
-        &.product-title {
-          max-width: 300px;
-          overflow: hidden;
-          text-overflow: ellipsis;
-          white-space: nowrap;
-        }
-      }
-    }
-
-    tr:last-child td {
-      border-bottom: none;
-    }
-  }
-}
-
-.score-cell {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-}
-
-.score-value {
-  font-weight: 700;
-  min-width: 40px;
-}
-
-.score-bar {
-  flex: 1;
-  height: 8px;
-  background: #f3f4f6;
-  border-radius: 999px;
-  overflow: hidden;
-  max-width: 80px;
-}
-
-.score-progress {
-  height: 100%;
-  border-radius: 999px;
-  transition: width 0.3s;
-
-  &.score-red { background: #ef4444; }
-  &.score-yellow { background: #f59e0b; }
-  &.score-green { background: #10b981; }
-  &.score-gray { background: #6b7280; }
-}
-
-.level-badge {
-  display: inline-block;
-  padding: 4px 12px;
-  border-radius: 999px;
-  font-size: 12px;
-  font-weight: 600;
-
-  &.badge-red {
-    background: #fee2e2;
-    color: #991b1b;
-  }
-
-  &.badge-yellow {
-    background: #fef3c7;
-    color: #92400e;
-  }
-
-  &.badge-green {
-    background: #d1fae5;
-    color: #065f46;
-  }
-
-  &.badge-gray {
-    background: #f3f4f6;
-    color: #374151;
-  }
-}
-
-.action-btn {
-  color: #409EFF;
-  font-weight: 500;
-
-  &:hover {
-    color: #66b1ff;
-  }
-}
-
-.empty-state {
-  padding: 60px 20px !important;
-  text-align: center;
-  color: #909399;
-
-  i {
-    font-size: 48px;
-    display: block;
-    margin-bottom: 12px;
-  }
-
-  p {
-    margin: 0;
-    font-size: 14px;
-  }
-}
-
-/* 详情弹窗 */
-.detail-dialog ::v-deep .el-dialog__header {
-  border-bottom: 1px solid #e5e7eb;
-  padding: 20px 24px;
-}
-
-.detail-dialog ::v-deep .el-dialog__body {
-  padding: 24px;
-}
-
-.detail-content {
-  max-height: 70vh;
-  overflow-y: auto;
-}
-
-.detail-info {
-  .info-label {
-    font-size: 14px;
-    color: #606266;
-    margin: 0;
-  }
-
-  .info-value {
-    font-weight: 600;
-    color: #303133;
-  }
-}
-
-.detail-stats {
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  gap: 16px;
-}
-
-.detail-stat-card {
-  padding: 20px;
-  border-radius: 8px;
-
-  &.bg-blue-gradient {
-    background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
-  }
-
-  &.bg-purple-gradient {
-    background: linear-gradient(135deg, #f3e8ff 0%, #e9d5ff 100%);
-  }
-}
-
-.detail-stat-label {
-  font-size: 13px;
-  color: #606266;
-  margin-bottom: 8px;
-}
-
-.detail-stat-value {
-  font-size: 32px;
-  font-weight: 700;
-  color: #303133;
-  margin: 0;
-
-  &.text-red { color: #dc2626; }
-  &.text-yellow { color: #d97706; }
-  &.text-green { color: #059669; }
-  &.text-gray { color: #6b7280; }
-}
-
-.detail-section {
-  .section-title {
-    font-size: 16px;
-    font-weight: 600;
-    color: #303133;
-    margin-bottom: 16px;
-  }
-}
-
-.metrics-grid {
-  display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
-  gap: 16px;
-}
-
-.metric-item {
-  border: 1px solid #e5e7eb;
-  border-radius: 8px;
-  padding: 16px;
-  background: #f9fafb;
-}
-
-.metric-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 8px;
-}
-
-.metric-name {
-  font-size: 13px;
-  color: #606266;
-}
-
-.metric-score {
-  font-size: 16px;
-  font-weight: 700;
-  color: #303133;
-}
-
-.metric-bar {
-  height: 8px;
-  background: #e5e7eb;
-  border-radius: 999px;
-  overflow: hidden;
-  margin-bottom: 8px;
-}
-
-.metric-progress {
-  height: 100%;
-  border-radius: 999px;
-
-  &.bg-blue { background: #3b82f6; }
-  &.bg-green { background: #10b981; }
-  &.bg-yellow { background: #f59e0b; }
-  &.bg-purple { background: #8b5cf6; }
-  &.bg-indigo { background: #6366f1; }
-}
-
-.metric-weight {
-  font-size: 12px;
-  color: #909399;
-  margin: 0;
-}
-
-.raw-data-grid {
-  display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
-  gap: 16px;
-}
-
-.raw-data-item {
-  background: #f9fafb;
-  border-radius: 8px;
-  padding: 16px;
-}
-
-.raw-data-label {
-  font-size: 12px;
-  color: #909399;
-  margin-bottom: 4px;
-}
-
-.raw-data-value {
-  font-size: 18px;
-  font-weight: 700;
-  color: #303133;
-  margin: 0;
-}
-
-.advice-box {
-  background: #eff6ff;
-  border: 1px solid #bfdbfe;
-  border-radius: 8px;
-  padding: 16px;
-}
-
-.advice-title {
-  font-size: 16px;
-  font-weight: 600;
-  color: #1e40af;
-  margin-bottom: 8px;
-
-  i {
-    margin-right: 8px;
-  }
-}
-
-.advice-content {
-  font-size: 14px;
-  color: #1e3a8a;
-  margin: 0;
-  line-height: 1.6;
-}
-</style>

+ 2 - 2
src/views/sale/control/index.vue

@@ -165,7 +165,7 @@
 </template>
 
 <script>
-import { analyzeFile, getResults } from '@/api/client'
+import { analyzeFile, getResults } from '@/api/lifecycle'
 import { getToken } from '@/utils/auth'
 import * as echarts from 'echarts'
 require('echarts/theme/macarons')
@@ -929,4 +929,4 @@ export default {
     font-weight: normal;
   }
 }
-</style>
+</style>