2 次代碼提交 748550c6c1 ... 42981283a3

作者 SHA1 備註 提交日期
  Yihao Kang 42981283a3 Merge branch 'master' of http://106.14.194.251:3000/dtm/dtm_vue 2 周之前
  Yihao Kang 19ddf4786f 生命周期模块各部分整合 2 周之前

+ 186 - 14
src/api/client.js

@@ -2,21 +2,177 @@ import axios from 'axios'
 
 const http = axios.create({
   baseURL: process.env.VUE_APP_PYTHON_API,
-  timeout: 300000
+  timeout: 300000, // 5分钟超时,用于大数据量分析
 })
 
-function uploadFileTo(url, file) {
+export async function uploadFile(file) {
   const form = new FormData()
   form.append('file', file)
-  return http.post(url, form, {
+  // 不要手动设置 Content-Type,让浏览器自动添加 boundary
+  const { data } = await http.post('/api/sku-lifecycle/upload', form, {
     headers: { 'Content-Type': 'multipart/form-data' }
-  }).then(({ data }) => 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
 }
 
-// 爆品分析相关 API
-// 销售概览相关 API
+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
 export async function uploadSaleOverviewFile(file) {
-  return uploadFileTo('/api/sale-overview/upload', 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
 }
 
 export async function analyzeSaleOverview() {
@@ -25,12 +181,14 @@ 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')
   }
 
-  return analyzeSaleOverview()
+  // Analyze cached data
+  return await analyzeSaleOverview()
 }
 
 export async function getSaleOverviewResults() {
@@ -38,9 +196,14 @@ export async function getSaleOverviewResults() {
   return data
 }
 
-// 销售趋势预测相关 API
+// 销售趋势预测相关API
 export async function uploadSaleTrendFile(file) {
-  return uploadFileTo('/api/sale-trend/upload', 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
 }
 
 export async function analyzeSaleTrend() {
@@ -49,12 +212,14 @@ 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')
   }
 
-  return analyzeSaleTrend()
+  // Analyze cached data
+  return await analyzeSaleTrend()
 }
 
 export async function getSaleTrendResults() {
@@ -67,9 +232,14 @@ export async function predictSalesTrend(params) {
   return data
 }
 
-// 促销效果分析相关 API
+// 促销效果分析相关API
 export async function uploadSaleEffectFile(file) {
-  return uploadFileTo('/api/sale-effect/upload', 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
 }
 
 export async function analyzeSaleEffect() {
@@ -78,12 +248,14 @@ 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')
   }
 
-  return analyzeSaleEffect()
+  // Analyze cached data
+  return await analyzeSaleEffect()
 }
 
 export async function getSaleEffectResults() {

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

@@ -165,7 +165,7 @@
 </template>
 
 <script>
-import { analyzeFile, getResults } from '@/api/lifecycle'
+import { analyzeFile, getResults } from '@/api/client'
 import { getToken } from '@/utils/auth'
 import * as echarts from 'echarts'
 require('echarts/theme/macarons')

+ 272 - 794
src/views/sale/effect/index.vue

@@ -1,848 +1,326 @@
 <template>
-  <div class="app-container">
-    <!-- 页面标题 -->
-    <div class="page-header">
-      <h2><i class="el-icon-data-analysis"></i> 促销效果分析</h2>
-      <p class="page-desc">深入分析促销活动对销售的影响,评估促销效果</p>
-    </div>
-
-    <div class="upload-toolbar">
-      <div class="toolbar-left">
-        <el-upload
-          ref="upload"
-          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>
+  <div class="sales-page">
+    <section class="header-bar">
+      <h1 class="header-title">促销效果分析</h1>
+      <div class="header-actions">
+        <el-upload action="" :auto-upload="false" :show-file-list="false" accept=".xlsx,.xls,.csv" :on-change="handleFileChange">
+          <el-button size="small" icon="el-icon-upload2">销售数据</el-button>
+        </el-upload>
+        <el-upload action="" :auto-upload="false" :show-file-list="false" accept=".xlsx,.xls" :on-change="handleProductFileChange">
+          <el-button size="small" icon="el-icon-folder-add">产品资料</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>
+        <el-button size="small" type="primary" :loading="loading" icon="el-icon-data-analysis" @click="runAnalysis">新建分析</el-button>
+        <el-button size="small" icon="el-icon-download" :disabled="!hasResults" @click="exportReport">导出报告</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>
-
-    <!-- 关键指标卡片 -->
-    <el-row :gutter="20" class="mb-20">
-      <el-col :xs="24" :sm="12" :md="8" :lg="4">
-        <el-card class="stat-card">
-          <div class="stat-content">
-            <div class="stat-info">
-              <p class="stat-label">订单总数</p>
-              <p class="stat-value">{{ summary.total_orders || 0 }}</p>
-              <p class="stat-desc">分析期间</p>
-            </div>
-            <div class="stat-icon stat-icon-purple">
-              <i class="el-icon-s-order"></i>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-      <el-col :xs="24" :sm="12" :md="8" :lg="4">
-        <el-card class="stat-card">
-          <div class="stat-content">
-            <div class="stat-info">
-              <p class="stat-label">促销订单占比</p>
-              <p class="stat-value">{{ summary.promotional_ratio ? summary.promotional_ratio.toFixed(1) : 0 }}%</p>
-              <p class="stat-desc">促销订单数: {{ summary.promotional_orders || 0 }}</p>
-            </div>
-            <div class="stat-icon stat-icon-blue">
-              <i class="el-icon-s-marketing"></i>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-      <el-col :xs="24" :sm="12" :md="8" :lg="4">
-        <el-card class="stat-card">
-          <div class="stat-content">
-            <div class="stat-info">
-              <p class="stat-label">促销销售额</p>
-              <p class="stat-value">{{ summary.promo_revenue || 0 }}</p>
-              <p class="stat-desc">非促销: {{ summary.non_promo_revenue || 0 }}</p>
-            </div>
-            <div class="stat-icon stat-icon-teal">
-              <i class="el-icon-s-finance"></i>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-      <el-col :xs="24" :sm="12" :md="8" :lg="4">
-        <el-card class="stat-card">
-          <div class="stat-content">
-            <div class="stat-info">
-              <p class="stat-label">促销销量</p>
-              <p class="stat-value">{{ summary.promo_quantity || 0 }}</p>
-              <p class="stat-desc">非促销: {{ summary.non_promo_quantity || 0 }}</p>
-            </div>
-            <div class="stat-icon stat-icon-green">
-              <i class="el-icon-s-goods"></i>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-      <el-col :xs="24" :sm="12" :md="8" :lg="4">
-        <el-card class="stat-card">
-          <div class="stat-content">
-            <div class="stat-info">
-              <p class="stat-label">平均促销力度</p>
-              <p class="stat-value">{{ summary.avg_promotion ? summary.avg_promotion.toFixed(1) : 0 }}%</p>
-              <p class="stat-desc">折扣幅度</p>
-            </div>
-            <div class="stat-icon stat-icon-yellow">
-              <i class="el-icon-s-printer"></i>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-      <el-col :xs="24" :sm="12" :md="8" :lg="4">
-        <el-card class="stat-card">
-          <div class="stat-content">
-            <div class="stat-info">
-              <p class="stat-label">促销效果评分</p>
-              <p class="stat-value">{{ effectEvaluation.score ? effectEvaluation.score.toFixed(1) : 0 }}</p>
-              <p class="stat-desc stat-desc-success">{{ effectEvaluation.level || '需改进' }}</p>
-            </div>
-            <div class="stat-icon stat-icon-red">
-              <i class="el-icon-star-on"></i>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <!-- 促销类型分析与时间趋势 -->
-    <el-row :gutter="20" class="mb-20">
-      <el-col :xs="24" :sm="24" :md="12" :lg="12">
-        <el-card>
-          <div slot="header">
-            <span><i class="el-icon-pie-chart"></i> 促销类型分布</span>
-            <span class="header-desc">按促销力度划分</span>
-          </div>
-          <div ref="promotionTypeChart" style="height: 400px"></div>
-        </el-card>
-      </el-col>
-      <el-col :xs="24" :sm="24" :md="12" :lg="12">
-        <el-card>
-          <div slot="header">
-            <span><i class="el-icon-data-line"></i> 促销效果时间趋势</span>
-            <span class="header-desc">销量与销售额对比</span>
-          </div>
-          <div ref="timeTrendChart" style="height: 400px"></div>
-        </el-card>
-      </el-col>
-    </el-row>
+    </section>
+
+    <section class="activity-bar">
+      <button v-for="activity in activities" :key="activity.name" :class="{ active: activeActivity === activity.name }" @click="activeActivity = activity.name">
+        <strong>{{ activity.name }}</strong>
+        <span>{{ activity.date }}</span>
+      </button>
+      <button class="add" @click="dialogVisible = true"><i class="el-icon-plus"></i> 自定义活动</button>
+    </section>
+
+    <section class="chart-panel">
+      <div class="chart-header">
+        <h3>活动前后销量对比</h3>
+        <p>{{ activeActivity }} · 基于上传销售数据自动评估</p>
+      </div>
+      <div ref="effectChart" class="chart"></div>
+    </section>
+
+    <section class="two-col">
+      <div class="panel">
+        <h3>ROI 分析</h3>
+        <div class="roi-score">
+          <strong>{{ roiValue }}</strong>
+          <p>{{ roiLevel }}</p>
+        </div>
+        <div class="roi-details">
+          <div><span>投入估算</span><b>{{ formatMoney(investment) }}</b></div>
+          <div><span>增量产出</span><b>{{ formatMoney(incrementRevenue) }}</b></div>
+        </div>
+      </div>
+      <div class="panel">
+        <h3>增量效果</h3>
+        <div class="increment-grid">
+          <div><span>销量提升率</span><strong>{{ percent(effect.quantity_lift) }}</strong></div>
+          <div><span>销售额提升率</span><strong>{{ percent(effect.revenue_lift) }}</strong></div>
+          <div><span>促销占比</span><strong>{{ percent(effect.promo_ratio) }}</strong></div>
+          <div><span>综合评分</span><strong>{{ score }}</strong></div>
+        </div>
+      </div>
+    </section>
 
-    <!-- 品类分析与SKU分析 -->
-    <el-row :gutter="20" class="mb-20">
-      <el-col :xs="24" :sm="24" :md="12" :lg="12">
-        <el-card>
-          <div slot="header">
-            <span><i class="el-icon-s-grid"></i> 品类促销效果</span>
-            <span class="header-desc">销量提升率</span>
-          </div>
-          <div ref="categoryEffectChart" style="height: 400px"></div>
-        </el-card>
-      </el-col>
-      <el-col :xs="24" :sm="24" :md="12" :lg="12">
-        <el-card>
-          <div slot="header">
-            <span><i class="el-icon-s-operation"></i> 促销效果评估</span>
-            <span class="header-desc">综合评分与各项指标</span>
-          </div>
-          <div ref="effectEvaluationChart" style="height: 400px"></div>
-        </el-card>
-      </el-col>
-    </el-row>
+    <section class="panel">
+      <div class="panel-head">
+        <h3>详细数据对比</h3>
+        <el-button type="text" icon="el-icon-view" @click="showDailyDetail">查看活动明细</el-button>
+      </div>
+      <el-table :data="detailRows" size="small">
+        <el-table-column prop="name" label="指标名称" min-width="140" />
+        <el-table-column prop="before" label="活动前" min-width="120" />
+        <el-table-column prop="during" label="活动中" min-width="120" />
+        <el-table-column prop="after" label="活动后" min-width="120" />
+        <el-table-column prop="change" label="变化率" min-width="120">
+          <template slot-scope="{ row }"><span :class="row.changeValue >= 0 ? 'positive' : 'negative'">{{ row.change }}</span></template>
+        </el-table-column>
+      </el-table>
+    </section>
+
+    <el-dialog title="新建促销活动分析" :visible.sync="dialogVisible" width="460px">
+      <el-form label-position="top">
+        <el-form-item label="活动名称"><el-input v-model="customActivity.name" /></el-form-item>
+        <el-form-item label="活动日期"><el-date-picker v-model="customActivity.range" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" /></el-form-item>
+      </el-form>
+      <span slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="createActivity">创建</el-button>
+      </span>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-import { analyzeSaleEffectWithFile, getSaleEffectResults } from '@/api/client'
-import { getToken } from '@/utils/auth'
 import * as echarts from 'echarts'
-require('echarts/theme/macarons')
+import { analyzeSaleEffect, analyzeSaleEffectWithFile, getSaleEffectResults, uploadProductFile } from '@/api/client'
 
 export default {
   name: 'SaleEffectAnalysis',
   data() {
     return {
-      // 图表实例
-      promotionTypeChart: null,
-      timeTrendChart: null,
-      categoryEffectChart: null,
-      effectEvaluationChart: null,
-      // 数据
+      chart: null,
+      loading: false,
+      file: null,
+      productFile: null,
+      activeActivity: '双 11 大促',
+      activities: [
+        { name: '双 11 大促', date: '11-01 至 11-11' },
+        { name: '618 大促', date: '06-01 至 06-18' },
+        { name: '新品首发', date: '03-15 至 03-21' },
+        { name: '清仓特卖', date: '01-10 至 01-20' }
+      ],
       results: {},
-      // 计算属性数据
-      summary: {},
-      effectEvaluation: {},
-      // 文件上传相关
-      upload: {
-        // 是否显示弹出层
-        open: false,
-        // 弹出层标题
-        title: '',
-        // 是否禁用上传
-        isUploading: false,
-        // 是否更新已经存在的文件
-        updateSupport: 0,
-        // 设置上传的请求头部
-        headers: { Authorization: 'Bearer ' + getToken() },
-        // 上传的地址
-        url: process.env.VUE_APP_PYTHON_API + '/api/sale-effect/upload',
-        // 文件名称
-        fileName: '',
-        // 已选择文件名称
-        pendingFileName: '',
-        // 是否忽略文件选择改变
-        ignoreFileChange: false,
-      }
+      dialogVisible: false,
+      customActivity: { name: '', range: [] }
     }
   },
   computed: {
     hasResults() {
       return Object.keys(this.results || {}).length > 0
+    },
+    summary() {
+      return this.results.summary || {}
+    },
+    effect() {
+      return this.results.effect_evaluation || {}
+    },
+    customEffect() {
+      return this.results.custom_activity || null
+    },
+    score() {
+      return Number(this.effect.score || 0).toFixed(1)
+    },
+    roiValue() {
+      const roi = this.investment ? this.incrementRevenue / this.investment : Number(this.effect.score || 0) / 30
+      return Math.max(0, roi).toFixed(1)
+    },
+    roiLevel() {
+      const roi = Number(this.roiValue)
+      if (roi >= 3) return '投资回报率优秀'
+      if (roi >= 1.5) return '投资回报率良好'
+      return '需要优化促销投入'
+    },
+    incrementRevenue() {
+      return Number(this.effect.incremental_revenue || this.summary.total_revenue * (Number(this.effect.revenue_lift || 0) / 100) || 0)
+    },
+    investment() {
+      return Math.max(1, Number(this.summary.total_revenue || 0) * 0.05)
+    },
+    detailRows() {
+      const qLift = Number(this.effect.quantity_lift || 0)
+      const rLift = Number(this.effect.revenue_lift || 0)
+      const baseQty = Number(this.summary.total_quantity || 0) / 30 || 2340
+      const baseRevenue = Number(this.summary.total_revenue || 0) / 30 || 187200
+      if (this.customEffect) {
+        const before = this.customEffect.before || {}
+        const during = this.customEffect.during || {}
+        const after = this.customEffect.after || {}
+        return [
+          this.row('日均销量', before.quantity || 0, during.quantity || 0, after.quantity || 0, Number(this.customEffect.quantity_lift || 0), '件'),
+          this.row('日均销售额', before.revenue || 0, during.revenue || 0, after.revenue || 0, Number(this.customEffect.revenue_lift || 0), '元'),
+          this.row('促销占比', 0, Number(this.effect.promo_ratio || 0), Number(this.effect.promo_ratio || 0) * 0.6, Number(this.effect.promo_ratio || 0), '%'),
+          this.row('综合评分', 60, Number(this.effect.score || 0), 70, Number(this.effect.score || 0) - 60, '分')
+        ]
+      }
+      return [
+        this.row('日均销量', baseQty, baseQty * (1 + qLift / 100), baseQty * 1.15, qLift, '件'),
+        this.row('日均销售额', baseRevenue, baseRevenue * (1 + rLift / 100), baseRevenue * 1.12, rLift, '元'),
+        this.row('促销占比', 0, Number(this.effect.promo_ratio || 0), Number(this.effect.promo_ratio || 0) * 0.6, Number(this.effect.promo_ratio || 0), '%'),
+        this.row('综合评分', 60, Number(this.effect.score || 0), 70, Number(this.effect.score || 0) - 60, '分')
+      ]
     }
   },
   mounted() {
-    this.$nextTick(() => {
-      this.initCharts()
-    })
-    // 监听窗口大小变化
-    window.addEventListener('resize', this.handleResize)
+    this.chart = echarts.init(this.$refs.effectChart)
+    window.addEventListener('resize', this.resize)
+    this.loadCachedResults()
   },
   beforeDestroy() {
-    // 销毁图表实例
-    if (this.promotionTypeChart) {
-      this.promotionTypeChart.dispose()
-    }
-    if (this.timeTrendChart) {
-      this.timeTrendChart.dispose()
-    }
-    if (this.categoryEffectChart) {
-      this.categoryEffectChart.dispose()
-    }
-    if (this.effectEvaluationChart) {
-      this.effectEvaluationChart.dispose()
-    }
-    window.removeEventListener('resize', this.handleResize)
+    window.removeEventListener('resize', this.resize)
+    if (this.chart) this.chart.dispose()
   },
   methods: {
-    /** 文件选择改变处理 */
-    handleFileChange(file, fileList) {
-      if (this.upload.ignoreChange) 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
+    handleFileChange(file) { this.file = file.raw },
+    handleProductFileChange(file) { this.productFile = file.raw },
+    async loadCachedResults() {
+      try {
+        const response = await getSaleEffectResults()
+        if (response && response.success) this.results = response.data || {}
+      } catch (e) {
+        this.results = {}
       }
-      return true
+      this.$nextTick(this.renderChart)
     },
-    /** 自定义上传方法 */
-    customUpload(options) {
-      const file = options.file
-      this.upload.isUploading = true
-      analyzeSaleEffectWithFile(file).then(response => {
-        this.upload.isUploading = false
-        if (response && response.success) {
-          this.$modal.msgSuccess('文件上传并分析成功')
-          this.upload.fileName = this.upload.pendingFileName || file.name
-          this.upload.pendingFileName = ''
-          this.results = response.data || {}
-          this.updateMetrics()
-          this.$nextTick(() => {
-            this.renderCharts()
-          })
-          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)
-      })
-    },
-    /** 提交上传文件 */
-    submitUpload() {
-      const fileList = this.$refs.upload.uploadFiles
-      if (!fileList || fileList.length === 0) {
-        this.$modal.msgError('请选择要上传的文件')
+    async runAnalysis() {
+      if (!this.file && !this.hasResults) {
+        this.$modal.msgError('请先上传销售数据')
         return
       }
-      this.$refs.upload.submit()
-    },
-    /** 获取促销效果分析结果 */
-    getList() {
-      getSaleEffectResults().then(response => {
-        if (response && response.success && response.data) {
+      this.loading = true
+      try {
+        if (this.productFile) await uploadProductFile(this.productFile)
+        if (this.file) {
+          const response = await analyzeSaleEffectWithFile(this.file, this.activityPayload())
+          if (!response.success) throw new Error(response.message || '分析失败')
+          this.results = response.data || {}
+        } else if (this.hasResults && Object.keys(this.activityPayload()).length) {
+          const response = await analyzeSaleEffect(this.activityPayload())
+          if (!response.success) throw new Error(response.message || '分析失败')
           this.results = response.data || {}
-          this.updateMetrics()
-          this.$nextTick(() => {
-            this.renderCharts()
-          })
         }
-      }).catch(() => {
-        this.results = {}
-      })
-    },
-    /** 更新指标 */
-    updateMetrics() {
-      this.summary = this.results.summary || {}
-      this.effectEvaluation = this.results.effect_evaluation || {}
-    },
-    /** 初始化图表 */
-    initCharts() {
-      if (this.$refs.promotionTypeChart) {
-        this.promotionTypeChart = echarts.init(this.$refs.promotionTypeChart, 'macarons')
+        this.renderChart()
+        this.$modal.msgSuccess('促销效果分析完成')
+      } catch (error) {
+        this.$modal.msgError(error.message || '分析失败')
+      } finally {
+        this.loading = false
       }
-      if (this.$refs.timeTrendChart) {
-        this.timeTrendChart = echarts.init(this.$refs.timeTrendChart, 'macarons')
-      }
-      if (this.$refs.categoryEffectChart) {
-        this.categoryEffectChart = echarts.init(this.$refs.categoryEffectChart, 'macarons')
-      }
-      if (this.$refs.effectEvaluationChart) {
-        this.effectEvaluationChart = echarts.init(this.$refs.effectEvaluationChart, 'macarons')
-      }
-    },
-    /** 渲染所有图表 */
-    renderCharts() {
-      // 1. 促销类型分布
-      this.renderPromotionTypeChart()
-
-      // 2. 时间趋势
-      this.renderTimeTrendChart()
-
-      // 3. 品类效果
-      this.renderCategoryEffectChart()
-
-      // 4. 效果评估
-      this.renderEffectEvaluationChart()
     },
-    /** 渲染促销类型分布 */
-    renderPromotionTypeChart() {
-      const promotionTypes = this.results.promotion_types || {}
-      const types = promotionTypes.types || {}
-      const typeList = promotionTypes.type_list || []
-
-      const data = typeList.map(type => {
-        const info = types[type] || {}
-        return {
-          name: type,
-          value: info.order_count || 0
-        }
-      })
-
-      const option = {
-        tooltip: {
-          trigger: 'item',
-          formatter: '{a} <br/>{b}: {c} ({d}%)'
-        },
-        legend: {
-          orient: 'vertical',
-          left: 'left',
-          data: typeList
-        },
+    renderChart() {
+      if (!this.chart) return
+      const rows = this.detailRows
+      const qty = rows[0]
+      const rev = rows[1]
+      this.chart.setOption({
+        tooltip: { trigger: 'axis' },
+        legend: { top: 0 },
+        grid: { left: 45, right: 24, bottom: 40, top: 45 },
+        xAxis: { type: 'category', data: ['活动前', '活动中', '活动后'] },
+        yAxis: { type: 'value' },
         series: [
-          {
-            name: '促销类型',
-            type: 'pie',
-            radius: ['40%', '70%'],
-            avoidLabelOverlap: false,
-            itemStyle: {
-              borderRadius: 10,
-              borderColor: '#fff',
-              borderWidth: 2
-            },
-            label: {
-              show: true,
-              formatter: '{b}: {c}\n({d}%)'
-            },
-            emphasis: {
-              label: {
-                show: true,
-                fontSize: 16,
-                fontWeight: 'bold'
-              }
-            },
-            data: data,
-            color: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444']
-          }
+          { name: '销量', type: 'bar', data: [qty.rawBefore, qty.rawDuring, qty.rawAfter], itemStyle: { color: '#f59e0b', borderRadius: [6, 6, 0, 0] } },
+          { name: '销售额', type: 'bar', data: [rev.rawBefore, rev.rawDuring, rev.rawAfter], itemStyle: { color: '#667eea', borderRadius: [6, 6, 0, 0] } }
         ]
-      }
-
-      if (this.promotionTypeChart) {
-        this.promotionTypeChart.setOption(option)
-      }
+      })
     },
-    /** 渲染时间趋势 */
-    renderTimeTrendChart() {
-      const timeAnalysis = this.results.time_analysis || {}
-      const dateSeries = timeAnalysis.date_series || []
-      const promoQuantity = timeAnalysis.promo_quantity_series || []
-      const nonPromoQuantity = timeAnalysis.non_promo_quantity_series || []
-      const promoRevenue = timeAnalysis.promo_revenue_series || []
-      const nonPromoRevenue = timeAnalysis.non_promo_revenue_series || []
-
-      const option = {
-        tooltip: {
-          trigger: 'axis',
-          axisPointer: {
-            type: 'cross',
-            label: {
-              backgroundColor: '#6a7985'
-            }
-          }
-        },
-        legend: {
-          data: ['促销销量', '非促销销量', '促销销售额', '非促销销售额']
-        },
-        grid: {
-          left: '3%',
-          right: '4%',
-          bottom: '3%',
-          containLabel: true
-        },
-        xAxis: {
-          type: 'category',
-          boundaryGap: false,
-          data: dateSeries
-        },
-        yAxis: [
-          {
-            type: 'value',
-            name: '销量',
-            position: 'left'
-          },
-          {
-            type: 'value',
-            name: '销售额',
-            position: 'right'
-          }
-        ],
-        series: [
-          {
-            name: '促销销量',
-            type: 'line',
-            data: promoQuantity,
-            itemStyle: {
-              color: '#3b82f6'
-            }
-          },
-          {
-            name: '非促销销量',
-            type: 'line',
-            data: nonPromoQuantity,
-            itemStyle: {
-              color: '#94a3b8'
-            }
-          },
-          {
-            name: '促销销售额',
-            type: 'line',
-            yAxisIndex: 1,
-            data: promoRevenue,
-            itemStyle: {
-              color: '#10b981'
-            }
-          },
-          {
-            name: '非促销销售额',
-            type: 'line',
-            yAxisIndex: 1,
-            data: nonPromoRevenue,
-            itemStyle: {
-              color: '#f59e0b'
-            }
-          }
-        ]
-      }
-
-      if (this.timeTrendChart) {
-        this.timeTrendChart.setOption(option)
+    row(name, before, during, after, change, unit) {
+      const format = value => unit === '元' ? this.formatMoney(value) : `${Math.round(value).toLocaleString()}${unit}`
+      return {
+        name,
+        rawBefore: before,
+        rawDuring: during,
+        rawAfter: after,
+        before: format(before),
+        during: format(during),
+        after: format(after),
+        changeValue: change,
+        change: `${change >= 0 ? '+' : ''}${Number(change).toFixed(1)}%`
       }
     },
-    /** 渲染品类效果 */
-    renderCategoryEffectChart() {
-      const categoryAnalysis = this.results.category_analysis || {}
-      const categoryEffects = categoryAnalysis.category_effects || {}
-      const categoryList = categoryAnalysis.category_list || []
-
-      const categories = categoryList
-      const quantityEffects = categories.map(category => {
-        const effect = categoryEffects[category] || {}
-        return effect.quantity_effect || 0
-      })
-      const revenueEffects = categories.map(category => {
-        const effect = categoryEffects[category] || {}
-        return effect.revenue_effect || 0
-      })
-
-      const option = {
-        tooltip: {
-          trigger: 'axis',
-          axisPointer: {
-            type: 'shadow'
-          }
-        },
-        legend: {
-          data: ['销量提升率', '销售额提升率']
-        },
-        grid: {
-          left: '3%',
-          right: '4%',
-          bottom: '3%',
-          containLabel: true
-        },
-        xAxis: {
-          type: 'category',
-          data: categories
-        },
-        yAxis: {
-          type: 'value',
-          name: '提升率(%)'
-        },
-        series: [
-          {
-            name: '销量提升率',
-            type: 'bar',
-            data: quantityEffects,
-            itemStyle: {
-              color: 'rgba(59,130,246,0.7)'
-            }
-          },
-          {
-            name: '销售额提升率',
-            type: 'bar',
-            data: revenueEffects,
-            itemStyle: {
-              color: 'rgba(16,185,129,0.7)'
-            }
-          }
-        ]
-      }
-
-      if (this.categoryEffectChart) {
-        this.categoryEffectChart.setOption(option)
-      }
+    percent(value) {
+      return `${Number(value || 0).toFixed(1)}%`
     },
-    /** 渲染效果评估 */
-    renderEffectEvaluationChart() {
-      const evaluation = this.results.effect_evaluation || {}
-
-      const data = [
-        {
-          name: '销量提升率',
-          value: evaluation.quantity_lift || 0
-        },
-        {
-          name: '销售额提升率',
-          value: evaluation.revenue_lift || 0
-        },
-        {
-          name: '促销占比',
-          value: evaluation.promo_ratio || 0
-        },
-        {
-          name: '平均促销力度',
-          value: evaluation.avg_promotion || 0
-        },
-        {
-          name: '综合评分',
-          value: evaluation.score || 0
+    formatMoney(value) {
+      return `¥${Math.round(Number(value || 0)).toLocaleString()}`
+    },
+    createActivity() {
+      if (!this.customActivity.name) return
+      this.activities.push({ name: this.customActivity.name, date: '自定义周期' })
+      this.activeActivity = this.customActivity.name
+      this.dialogVisible = false
+      if (this.file || this.hasResults) this.runAnalysis()
+    },
+    activityPayload() {
+      const range = this.customActivity.range || []
+      if (!range.length || this.activeActivity !== this.customActivity.name) return {}
+      return {
+        activity_range: {
+          start: this.formatDate(range[0]),
+          end: this.formatDate(range[1])
         }
-      ]
-
-      const option = {
-        tooltip: {
-          trigger: 'axis',
-          axisPointer: {
-            type: 'shadow'
-          }
-        },
-        grid: {
-          left: '3%',
-          right: '4%',
-          bottom: '3%',
-          containLabel: true
-        },
-        xAxis: {
-          type: 'value',
-          name: '数值'
-        },
-        yAxis: {
-          type: 'category',
-          data: data.map(item => item.name)
-        },
-        series: [
-          {
-            name: '指标值',
-            type: 'bar',
-            data: data.map(item => item.value),
-            itemStyle: {
-              color: function(params) {
-                const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6']
-                return colors[params.dataIndex % colors.length]
-              }
-            },
-            label: {
-              show: true,
-              position: 'right',
-              formatter: '{c}'
-            }
-          }
-        ]
-      }
-
-      if (this.effectEvaluationChart) {
-        this.effectEvaluationChart.setOption(option)
       }
     },
-    /** 窗口大小变化处理 */
-    handleResize() {
-      if (this.promotionTypeChart) {
-        this.promotionTypeChart.resize()
-      }
-      if (this.timeTrendChart) {
-        this.timeTrendChart.resize()
-      }
-      if (this.categoryEffectChart) {
-        this.categoryEffectChart.resize()
-      }
-      if (this.effectEvaluationChart) {
-        this.effectEvaluationChart.resize()
-      }
+    formatDate(value) {
+      const date = new Date(value)
+      if (Number.isNaN(date.getTime())) return ''
+      const year = date.getFullYear()
+      const month = String(date.getMonth() + 1).padStart(2, '0')
+      const day = String(date.getDate()).padStart(2, '0')
+      return `${year}-${month}-${day}`
     },
-    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}`
+    showDailyDetail() {
+      this.$modal.msg('明细数据已在下方表格展示')
     },
-    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' })
+    exportReport() {
+      const blob = new Blob([JSON.stringify(this.results, null, 2)], { type: 'application/json;charset=utf-8' })
       const url = URL.createObjectURL(blob)
-      const a = document.createElement('a')
-      a.href = url
-      a.download = `sale_effect_results_${this.formatUploadDate(new Date())}.json`
-      document.body.appendChild(a)
-      a.click()
-      document.body.removeChild(a)
+      const link = document.createElement('a')
+      link.href = url
+      link.download = `sale_effect_${Date.now()}.json`
+      link.click()
       URL.revokeObjectURL(url)
+    },
+    resize() {
+      if (this.chart) this.chart.resize()
     }
   }
 }
 </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;
-}
-
-::v-deep .el-card {
-  border-radius: 8px;
-  border: 1px solid #e6eaf2;
-  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
-}
-
-::v-deep .el-card__header {
-  border-bottom: 1px solid #eef2f7;
-}
-
-.stat-card {
-  .stat-content {
-    display: flex;
-    justify-content: space-between;
-    align-items: flex-start;
-
-    .stat-info {
-      flex: 1;
-
-      .stat-label {
-        font-size: 12px;
-        color: #909399;
-        margin: 0 0 8px 0;
-        text-transform: uppercase;
-        letter-spacing: 0.5px;
-      }
-
-      .stat-value {
-        font-size: 28px;
-        font-weight: bold;
-        color: #303133;
-        margin: 0 0 8px 0;
-      }
-
-      .stat-desc {
-        font-size: 12px;
-        color: #909399;
-        margin: 0;
-
-        &.stat-desc-success {
-          color: #67C23A;
-        }
-      }
-    }
-
-    .stat-icon {
-      width: 48px;
-      height: 48px;
-      border-radius: 50%;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      font-size: 20px;
-
-      &.stat-icon-purple {
-        background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%);
-        color: #6366f1;
-      }
-
-      &.stat-icon-blue {
-        background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
-        color: #3b82f6;
-      }
-
-      &.stat-icon-teal {
-        background: linear-gradient(135deg, #ccfbf1 0%, #99f6e4 100%);
-        color: #14b8a6;
-      }
-
-      &.stat-icon-green {
-        background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
-        color: #10b981;
-      }
-
-      &.stat-icon-yellow {
-        background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
-        color: #f59e0b;
-      }
-
-      &.stat-icon-red {
-        background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
-        color: #ef4444;
-      }
-    }
-  }
-}
-
-::v-deep .el-card__header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-
-  .header-desc {
-    font-size: 12px;
-    color: #909399;
-    font-weight: normal;
-  }
-}
-</style>
+.sales-page { padding: 20px; max-width: 1400px; margin: 0 auto; background: #f0f2f5; min-height: calc(100vh - 84px); }
+.header-bar, .activity-bar, .chart-panel, .panel { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,.06); }
+.header-bar { display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; margin-bottom: 20px; gap: 16px; flex-wrap: wrap; }
+.header-title { margin: 0; font-size: 20px; font-weight: 600; color: #303133; }
+.header-actions { display: flex; gap: 10px; flex-wrap: wrap; }
+.activity-bar { display: flex; gap: 12px; padding: 20px 24px; margin-bottom: 20px; flex-wrap: wrap; }
+.activity-bar button { border: 2px solid transparent; background: #f5f7fa; border-radius: 8px; padding: 10px 18px; cursor: pointer; color: #303133; }
+.activity-bar button strong, .activity-bar button span { display: block; }
+.activity-bar button span { font-size: 11px; opacity: .75; margin-top: 3px; }
+.activity-bar button.active { color: white; background: linear-gradient(135deg,#667eea,#764ba2); }
+.activity-bar button.add { border-style: dashed; }
+.chart-panel, .panel { padding: 24px; margin-bottom: 20px; }
+.chart-header { text-align: center; margin-bottom: 12px; }
+.chart-header h3 { margin: 0 0 6px; font-size: 18px; }
+.chart-header p { margin: 0; color: #909399; }
+.chart { height: 350px; }
+.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
+.panel h3 { margin: 0 0 20px; font-size: 16px; color: #303133; }
+.roi-score { text-align: center; }
+.roi-score strong { display: block; font-size: 48px; color: #667eea; }
+.roi-score p { color: #909399; }
+.roi-details { display: flex; justify-content: space-around; border-top: 1px solid #f0f0f0; padding-top: 16px; }
+.roi-details div { text-align: center; }
+.roi-details span { display: block; color: #909399; font-size: 12px; margin-bottom: 6px; }
+.increment-grid { display: grid; grid-template-columns: repeat(2,1fr); gap: 15px; }
+.increment-grid div { text-align: center; background: #f8f9fa; border-radius: 8px; padding: 20px; }
+.increment-grid span { display: block; color: #909399; font-size: 12px; margin-bottom: 8px; }
+.increment-grid strong { font-size: 24px; color: #303133; }
+.panel-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
+.panel-head h3 { margin: 0; }
+.positive { color: #67c23a; font-weight: 600; }
+.negative { color: #f56c6c; font-weight: 600; }
+@media (max-width: 1100px) { .two-col { grid-template-columns: 1fr; } }
+</style>

+ 298 - 4
src/views/sale/evaluation/index.vue

@@ -1,11 +1,305 @@
-<script setup>
+<template>
+  <div class="sales-page">
+    <section class="header-bar">
+      <h1 class="header-title">性能评估</h1>
+      <div class="header-actions">
+        <el-button size="small" type="success" :loading="loading" icon="el-icon-refresh" @click="startEvaluation">重新评估</el-button>
+        <el-button size="small" type="primary" icon="el-icon-download" @click="exportReport">导出报告</el-button>
+      </div>
+    </section>
 
-</script>
+    <section class="filter-bar">
+      <div class="filter-item">
+        <label>时间范围</label>
+        <el-select v-model="timeRange" size="small">
+          <el-option label="近 7 天" value="7" />
+          <el-option label="近 15 天" value="15" />
+          <el-option label="近 30 天" value="30" />
+          <el-option label="近 90 天" value="90" />
+        </el-select>
+      </div>
+      <div class="filter-item">
+        <label>产品范围</label>
+        <el-select v-model="productRange" size="small">
+          <el-option label="全部产品" value="all" />
+          <el-option label="主力产品" value="main" />
+          <el-option label="新品" value="new" />
+          <el-option label="淘汰品" value="old" />
+        </el-select>
+      </div>
+      <div class="filter-item">
+        <label>预测模型</label>
+        <el-select v-model="model" size="small">
+          <el-option label="当前模型" value="current" />
+          <el-option label="历史模型 v1" value="v1" />
+          <el-option label="历史模型 v2" value="v2" />
+        </el-select>
+      </div>
+    </section>
 
-<template>
+    <section class="metrics-panel">
+      <div class="metric-card"><span>MAPE</span><strong>{{ mape }}<em>%</em></strong><b>良好</b></div>
+      <div class="metric-card rmse"><span>RMSE</span><strong>{{ formatNumber(rmse) }}</strong><em>件</em></div>
+      <div class="metric-card mae"><span>MAE</span><strong>{{ formatNumber(mae) }}</strong><em>件</em></div>
+      <div class="metric-card r2"><span>R²</span><strong>{{ r2 }}</strong><b>拟合度良好</b></div>
+      <div class="gauge-card">
+        <div class="gauge" :style="{ background: gaugeGradient }"><div><strong>{{ accuracy }}%</strong><span>整体准确度</span></div></div>
+        <div class="gauge-summary">
+          <div><span>评估样本数</span><strong>{{ formatNumber(evaluationRows.length) }}</strong></div>
+          <div><span>达标率</span><strong>{{ passRate }}%</strong></div>
+          <div><span>异常点数</span><strong>{{ abnormalCount }}</strong></div>
+          <div><span>平均误差</span><strong>{{ mape }}%</strong></div>
+        </div>
+      </div>
+    </section>
+
+    <section class="panel">
+      <div class="panel-head">
+        <h3>预测值 vs 实际值对比</h3>
+        <div class="legend"><span><i class="blue"></i>实际值</span><span><i class="orange"></i>预测值</span><span><i class="gray"></i>误差区域</span></div>
+      </div>
+      <div ref="comparisonChart" class="chart"></div>
+      <div v-if="!hasEvaluation" class="empty-hint">暂无回测数据,请先在趋势预测页上传并完成一次分析。</div>
+    </section>
+
+    <section class="panel">
+      <div class="panel-head">
+        <h3>误差分析明细</h3>
+        <el-input v-model="dateKeyword" size="small" prefix-icon="el-icon-search" placeholder="搜索日期" />
+      </div>
+      <el-table :data="filteredRows" size="small" :row-class-name="rowClassName">
+        <el-table-column prop="date" label="日期" min-width="120" />
+        <el-table-column prop="actual" label="实际值" min-width="100" />
+        <el-table-column prop="predicted" label="预测值" min-width="100" />
+        <el-table-column prop="absError" label="绝对误差" min-width="100" />
+        <el-table-column prop="relativeError" label="相对误差" min-width="100">
+          <template slot-scope="{ row }"><span :class="errorClass(row.relativeError)">{{ row.relativeError }}%</span></template>
+        </el-table-column>
+        <el-table-column prop="status" label="是否异常" min-width="120" />
+      </el-table>
+    </section>
 
+    <section class="panel">
+      <h3>优化建议</h3>
+      <div class="suggestions">
+        <div v-for="item in suggestions" :key="item.issue" class="suggestion-card" :class="item.level">
+          <strong>{{ item.issue }}</strong>
+          <p>{{ item.reason }}</p>
+          <em>{{ item.solution }}</em>
+          <el-button size="mini" type="text" @click="copySuggestion(item)">复制</el-button>
+        </div>
+      </div>
+    </section>
+  </div>
 </template>
 
-<style scoped lang="scss">
+<script>
+import * as echarts from 'echarts'
+import { backtestSalesTrend } from '@/api/client'
+
+export default {
+  name: 'SalePerformanceEvaluation',
+  data() {
+    return {
+      chart: null,
+      loading: false,
+      timeRange: '30',
+      productRange: 'all',
+      model: 'current',
+      dateKeyword: '',
+      backtest: {}
+    }
+  },
+  computed: {
+    historicalSeries() {
+      return this.evaluationRows.map(row => row.actual)
+    },
+    historicalDates() {
+      return this.evaluationRows.map(row => row.date)
+    },
+    predictedSeries() {
+      return this.evaluationRows.map(row => row.predicted)
+    },
+    hasEvaluation() {
+      return this.evaluationRows.length > 0
+    },
+    evaluationRows() {
+      return (this.backtest.rows || []).map(row => ({
+        ...row,
+        actual: Math.round(Number(row.actual || 0)),
+        predicted: Math.round(Number(row.predicted || 0)),
+        absError: Math.round(Number(row.absError || 0)),
+        relativeError: Number(row.relativeError || 0)
+      }))
+    },
+    filteredRows() {
+      if (!this.dateKeyword) return this.evaluationRows
+      return this.evaluationRows.filter(row => row.date.includes(this.dateKeyword))
+    },
+    mape() {
+      return Number(((this.backtest.metrics || {}).mape) || 0).toFixed(1)
+    },
+    mae() {
+      return Number(((this.backtest.metrics || {}).mae) || 0)
+    },
+    rmse() {
+      return Number(((this.backtest.metrics || {}).rmse) || 0)
+    },
+    r2() {
+      return Number(((this.backtest.metrics || {}).r2) || 0).toFixed(2)
+    },
+    accuracy() {
+      return Math.max(0, 100 - Number(this.mape)).toFixed(1)
+    },
+    passRate() {
+      const passed = this.evaluationRows.filter(row => row.relativeError <= 15).length
+      return ((passed / Math.max(1, this.evaluationRows.length)) * 100).toFixed(1)
+    },
+    abnormalCount() {
+      return this.evaluationRows.filter(row => row.status === '异常').length
+    },
+    gaugeGradient() {
+      const degree = Number(this.accuracy) * 3.6
+      return `conic-gradient(#667eea 0deg,#764ba2 ${degree}deg,#e5e7eb ${degree}deg)`
+    },
+    suggestions() {
+      return [
+        { level: 'warning', issue: `当前异常点 ${this.abnormalCount} 个`, reason: '高误差日期可能存在促销或流量突变。', solution: '建议补充活动日历、平台流量和节假日特征。' },
+        { level: 'info', issue: `整体 MAPE 为 ${this.mape}%`, reason: '模型整体表现稳定,但仍需关注异常样本。', solution: '建议定期回测并按品类拆分评估。' },
+        { level: 'primary', issue: 'RMSE 接近波动阈值', reason: '连续多日误差偏高会影响库存和营销决策。', solution: '建议重新训练模型并调整季节性参数。' }
+      ]
+    }
+  },
+  watch: {
+    timeRange() {
+      this.startEvaluation()
+    }
+  },
+  mounted() {
+    this.chart = echarts.init(this.$refs.comparisonChart)
+    window.addEventListener('resize', this.resize)
+    this.startEvaluation()
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.resize)
+    if (this.chart) this.chart.dispose()
+  },
+  methods: {
+    async startEvaluation() {
+      this.loading = true
+      try {
+        const response = await backtestSalesTrend({ window: Number(this.timeRange) })
+        if (response && response.success) {
+          this.backtest = response.data || {}
+        } else {
+          throw new Error((response && response.message) || '回测失败')
+        }
+      } catch (error) {
+        this.backtest = {}
+        this.$modal.msgError(error.message || '请先上传并分析销售数据后再评估')
+      } finally {
+        this.loading = false
+        this.$nextTick(this.renderChart)
+      }
+    },
+    renderChart() {
+      if (!this.chart) return
+      const rows = this.evaluationRows
+      this.chart.setOption({
+        tooltip: { trigger: 'axis' },
+        grid: { left: 45, right: 24, bottom: 40, top: 25 },
+        xAxis: { type: 'category', data: rows.map(row => row.date) },
+        yAxis: { type: 'value' },
+        series: [
+          { name: '实际值', type: 'scatter', symbolSize: 9, data: rows.map(row => row.actual), itemStyle: { color: '#3b82f6' } },
+          { name: '预测值', type: 'line', smooth: true, lineStyle: { type: 'dashed' }, data: rows.map(row => row.predicted), itemStyle: { color: '#f59e0b' } }
+        ]
+      })
+    },
+    rowClassName({ row }) {
+      return row.status === '异常' ? 'abnormal-row' : ''
+    },
+    errorClass(value) {
+      if (value <= 5) return 'low'
+      if (value <= 15) return 'medium'
+      return 'high'
+    },
+    copySuggestion(item) {
+      const text = `${item.issue}\n${item.reason}\n${item.solution}`
+      navigator.clipboard && navigator.clipboard.writeText(text)
+      this.$modal.msgSuccess('已复制')
+    },
+    exportReport() {
+      if (!this.hasEvaluation) {
+        this.$modal.msgWarning('暂无可导出的评估结果')
+        return
+      }
+      const blob = new Blob([JSON.stringify({ metrics: { mape: this.mape, rmse: this.rmse, mae: this.mae, r2: this.r2 }, rows: this.evaluationRows }, null, 2)], { type: 'application/json;charset=utf-8' })
+      const url = URL.createObjectURL(blob)
+      const link = document.createElement('a')
+      link.href = url
+      link.download = `sale_evaluation_${Date.now()}.json`
+      link.click()
+      URL.revokeObjectURL(url)
+    },
+    formatNumber(value) {
+      return Math.round(Number(value || 0)).toLocaleString()
+    },
+    resize() {
+      if (this.chart) this.chart.resize()
+    }
+  }
+}
+</script>
 
+<style scoped lang="scss">
+.sales-page { padding: 20px; max-width: 1400px; margin: 0 auto; background: #f0f2f5; min-height: calc(100vh - 84px); }
+.header-bar, .filter-bar, .panel, .metric-card, .gauge-card { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,.06); }
+.header-bar { display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; margin-bottom: 20px; gap: 16px; flex-wrap: wrap; }
+.header-title { margin: 0; font-size: 20px; font-weight: 600; }
+.header-actions { display: flex; gap: 10px; }
+.filter-bar { display: flex; gap: 15px; align-items: flex-end; padding: 20px 24px; margin-bottom: 20px; flex-wrap: wrap; }
+.filter-item { display: flex; flex-direction: column; gap: 6px; min-width: 150px; }
+.filter-item label { color: #606266; font-size: 12px; }
+.metrics-panel { display: grid; grid-template-columns: repeat(4,1fr); gap: 15px; margin-bottom: 20px; }
+.metric-card { padding: 24px; text-align: center; position: relative; overflow: hidden; }
+.metric-card::before { content: ''; position: absolute; left: 0; right: 0; top: 0; height: 4px; background: linear-gradient(90deg,#667eea,#764ba2); }
+.metric-card.rmse::before { background: linear-gradient(90deg,#f59e0b,#f97316); }
+.metric-card.mae::before { background: linear-gradient(90deg,#3b82f6,#06b6d4); }
+.metric-card.r2::before { background: linear-gradient(90deg,#10b981,#34d399); }
+.metric-card span { color: #909399; }
+.metric-card strong { display: block; font-size: 34px; margin: 8px 0; }
+.metric-card em { color: #909399; font-style: normal; }
+.metric-card b { background: #d4edda; color: #155724; border-radius: 16px; padding: 4px 12px; font-size: 12px; font-weight: 400; }
+.gauge-card { grid-column: span 4; padding: 24px; display: flex; justify-content: center; align-items: center; gap: 60px; flex-wrap: wrap; }
+.gauge { width: 180px; height: 180px; border-radius: 50%; position: relative; }
+.gauge div { position: absolute; inset: 20px; border-radius: 50%; background: white; display: flex; align-items: center; justify-content: center; flex-direction: column; }
+.gauge strong { font-size: 36px; color: #667eea; }
+.gauge span { color: #909399; }
+.gauge-summary { display: flex; gap: 36px; flex-wrap: wrap; }
+.gauge-summary span { display: block; color: #909399; font-size: 12px; margin-bottom: 5px; }
+.gauge-summary strong { font-size: 18px; }
+.panel { padding: 24px; margin-bottom: 20px; }
+.panel-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 16px; }
+.panel-head h3, .panel h3 { margin: 0; font-size: 16px; }
+.legend { display: flex; gap: 16px; color: #606266; font-size: 12px; flex-wrap: wrap; }
+.legend span { display: inline-flex; align-items: center; gap: 6px; }
+.legend i { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
+.legend .blue { background: #3b82f6; }
+.legend .orange { width: 20px; height: 2px; border-radius: 0; background: #f59e0b; }
+.legend .gray { background: rgba(107,114,128,.2); border: 1px solid #9ca3af; }
+.chart { height: 350px; }
+.empty-hint { text-align: center; color: #909399; margin-top: -210px; height: 210px; pointer-events: none; }
+.low { color: #67c23a; font-weight: 600; }
+.medium { color: #e6a23c; font-weight: 600; }
+.high { color: #f56c6c; font-weight: 600; }
+::v-deep .abnormal-row { background: #fef2f2; }
+.suggestions { display: flex; flex-direction: column; gap: 15px; }
+.suggestion-card { border-left: 4px solid #667eea; background: #f8f9fa; border-radius: 8px; padding: 18px; }
+.suggestion-card.warning { border-left-color: #f59e0b; }
+.suggestion-card.info { border-left-color: #17a2b8; }
+.suggestion-card p { color: #606266; margin: 8px 0; }
+.suggestion-card em { color: #667eea; font-style: normal; }
+@media (max-width: 1200px) { .metrics-panel { grid-template-columns: repeat(2,1fr); } .gauge-card { grid-column: span 2; } }
+@media (max-width: 768px) { .metrics-panel { grid-template-columns: 1fr; } .gauge-card { grid-column: span 1; } }
 </style>

+ 233 - 574
src/views/sale/feature/index.vue

@@ -1,633 +1,292 @@
 <template>
-  <div class="app-container">
-    <!-- 页面标题 -->
-    <div class="page-header">
-      <h2><i class="el-icon-data-analysis"></i> 特征重要性分析</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="upload.isUploading"
-          :on-change="handleFileChange"
-          :on-success="handleUploadSuccess"
-          :on-error="handleUploadError"
-          :before-upload="beforeUpload"
-          :auto-upload="false"
-          :show-file-list="false"
-        >
-          <el-button plain>上传文件</el-button>
+  <div class="sales-page">
+    <section class="header-bar">
+      <h1 class="header-title">特征重要性分析</h1>
+      <div class="header-actions">
+        <el-upload ref="upload" action="" :auto-upload="false" :show-file-list="false" accept=".xlsx,.xls,.csv" :on-change="handleFileChange">
+          <el-button size="small" plain icon="el-icon-upload2">销售数据</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>
+        <el-button size="small" type="success" :loading="loading" icon="el-icon-refresh" @click="startAnalysis">重新分析</el-button>
+        <el-button size="small" type="primary" icon="el-icon-download" @click="exportModel">导出模型</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>
+    </section>
 
-    <!-- 特征重要性雷达图 -->
-    <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" v-if="hasResults">
-          <span class="text-green-700 font-medium">
-            <i class="fa fa-check-circle mr-1"></i>
-            特征重要性分析完成
-          </span>
-        </div>
+    <section class="settings-bar">
+      <div class="setting-item">
+        <label>目标变量</label>
+        <el-select v-model="targetVar" size="small">
+          <el-option label="销量" value="quantity" />
+          <el-option label="销售额" value="revenue" />
+        </el-select>
       </div>
-      <div class="h-96">
-        <canvas ref="featureRadarRef"></canvas>
+      <div class="setting-item">
+        <label>时间范围</label>
+        <el-select v-model="timeRange" size="small">
+          <el-option label="近 30 天" value="30" />
+          <el-option label="近 60 天" value="60" />
+          <el-option label="近 90 天" value="90" />
+          <el-option label="全部" value="all" />
+        </el-select>
       </div>
-    </div>
-
-    <!-- 特征重要性详情 -->
-    <div class="bg-white rounded-xl p-6 mb-20 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>
-          </tr>
-          </thead>
-          <tbody class="bg-white divide-y divide-gray-200">
-          <tr v-for="(feature, index) in featureDetails" :key="index">
-            <td class="px-4 py-3 text-sm font-medium text-gray-800">{{ feature.name }}</td>
-            <td class="px-4 py-3 text-sm">
-              <div class="flex items-center">
-                <span class="text-sm font-medium">{{ feature.importance }}%</span>
-                <div class="ml-2 flex-1">
-                  <div class="w-full bg-gray-200 rounded-full h-2">
-                    <div class="bg-blue-500 h-2 rounded-full" :style="{ width: feature.importance + '%' }"></div>
-                  </div>
-                </div>
-              </div>
-            </td>
-            <td class="px-4 py-3 text-sm">
-              <span :class="feature.importanceClass">{{ feature.importanceLevel }}</span>
-            </td>
-            <td class="px-4 py-3 text-sm text-gray-600">{{ feature.description }}</td>
-          </tr>
-          </tbody>
-        </table>
+      <div class="setting-item">
+        <label>模型类型</label>
+        <el-select v-model="modelType" size="small">
+          <el-option label="销售趋势模型" value="trend" />
+          <el-option label="促销敏感模型" value="promo" />
+          <el-option label="线性回归" value="linear" />
+        </el-select>
       </div>
-    </div>
+      <span class="file-hint">{{ fileName || '可上传销售数据刷新特征权重' }}</span>
+    </section>
 
-    <!-- 特征相关性分析 -->
-    <div class="bg-white rounded-xl p-6 mb-20 shadow-sm">
-      <h3 class="text-lg font-semibold text-gray-800 mb-6">特征相关性分析</h3>
-      <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
-        <div v-for="(correlation, index) in featureCorrelations" :key="index" class="p-4 border border-gray-200 rounded-lg">
-          <h4 class="text-md font-medium text-gray-700 mb-3">{{ correlation.title }}</h4>
-          <div class="flex items-center">
-            <span class="text-sm font-medium">{{ correlation.value }}%</span>
-            <div class="ml-2 flex-1">
-              <div class="w-full bg-gray-200 rounded-full h-2">
-                <div class="bg-green-500 h-2 rounded-full" :style="{ width: Math.abs(correlation.value) + '%' }"></div>
-              </div>
+    <section class="panel">
+      <h3>特征重要性(贡献度)</h3>
+      <div class="feature-list">
+        <div
+          v-for="feature in normalizedFeatures"
+          :key="feature.name"
+          class="feature-item"
+          :class="{ active: selectedFeature.name === feature.name }"
+          @click="selectedFeature = feature"
+        >
+          <span class="feature-name">{{ feature.name }}</span>
+          <div class="feature-bar-container">
+            <div class="feature-bar" :class="feature.level" :style="{ width: feature.value + '%' }">
+              <span>{{ feature.value }}%</span>
             </div>
           </div>
-          <p class="text-xs text-gray-500 mt-2">{{ correlation.description }}</p>
+          <strong>{{ feature.value }}%</strong>
         </div>
       </div>
-    </div>
+    </section>
+
+    <section class="two-col">
+      <div class="panel">
+        <h3>特征相关性热力图</h3>
+        <div class="heatmap">
+          <div class="heat-row header-row">
+            <span></span>
+            <b v-for="feature in heatmapLabels" :key="feature">{{ feature }}</b>
+          </div>
+          <div v-for="row in heatmapMatrix" :key="row.name" class="heat-row">
+            <span>{{ row.name }}</span>
+            <button
+              v-for="cell in row.values"
+              :key="row.name + cell.name"
+              :style="{ backgroundColor: heatColor(cell.value) }"
+              :title="`${row.name} 与 ${cell.name}: ${cell.value}`"
+            >
+              {{ cell.value }}
+            </button>
+          </div>
+        </div>
+      </div>
+      <div class="panel">
+        <h3>特征详细分析</h3>
+        <div class="feature-detail">
+          <h2>{{ selectedFeature.name || '-' }}</h2>
+          <div class="detail-row">
+            <span>边际效应</span>
+            <strong class="positive">+{{ Math.max(1, selectedFeature.value / 2).toFixed(1) }}%</strong>
+          </div>
+          <div class="detail-row">
+            <span>建议区间</span>
+            <em>{{ selectedFeature.range || '保持稳定投入' }}</em>
+          </div>
+          <p>{{ selectedFeature.description || '请选择一个特征查看其对销量预测的影响。' }}</p>
+        </div>
+      </div>
+    </section>
+
+    <section class="panel">
+      <h3>AI 洞察与建议</h3>
+      <div class="insights">
+        <div v-for="item in insights" :key="item" class="insight-card">
+          <i class="el-icon-magic-stick"></i>
+          <p>{{ item }}</p>
+          <el-button size="mini" type="text" @click="copyText(item)">复制建议</el-button>
+        </div>
+      </div>
+    </section>
   </div>
 </template>
 
 <script>
-import { ref, computed, onMounted, beforeUnmount } from 'vue'
-import { analyzeSaleTrendWithFile, getSaleTrendResults, predictSalesTrend } from '@/api/client'
-import { Chart } from 'chart.js'
+import { analyzeSaleTrendWithFile, getSaleTrendResults } from '@/api/client'
 
 export default {
   name: 'FeatureImportanceAnalysis',
   data() {
     return {
-      featureRadarChart: null,
-      upload: {
-        isUploading: false,
-        fileName: '',
-        pendingFileName: '',
-        ignoreFileChange: false
-      },
-      results: {}
+      loading: false,
+      file: null,
+      fileName: '',
+      targetVar: 'quantity',
+      timeRange: '90',
+      modelType: 'trend',
+      results: {},
+      selectedFeature: {}
     }
   },
   computed: {
-    hasResults() {
-      return Object.keys(this.results || {}).length > 0
-    },
     featureImportance() {
       return this.results.feature_importance || {
-        features: [
-          '价格',
-          '促销力度',
-          '退款率',
-          '季节性',
-          '时间趋势',
-          '品类影响',
-          'SKU影响'
-        ],
+        features: ['价格', '促销力度', '退款率', '季节性', '时间趋势', '品类影响', 'SKU影响'],
         importance: [15, 15, 10, 15, 20, 10, 15]
       }
     },
-    featureDetails() {
-      const features = this.featureImportance.features
-      const importance = this.featureImportance.importance
-      
+    normalizedFeatures() {
       const descriptions = {
-        '价格': '产品价格对销量的影响程度',
-        '促销力度': '促销活动对销量的促进作用',
-        '退款率': '退款率对销售表现的负面影响',
-        '季节性': '季节因素对销量的影响',
-        '时间趋势': '时间推移对销量的整体影响',
-        '品类影响': '不同品类对销量的影响差异',
-        'SKU影响': '不同SKU对销量的影响差异'
+        '价格': '价格变化会直接影响用户购买决策,建议结合毛利率设置动态价格区间。',
+        '促销力度': '促销力度越高,销量通常越容易被拉动,但需要控制利润损耗。',
+        '退款率': '退款率高会削弱销量预测稳定性,应重点排查质量和售后问题。',
+        '季节性': '季节性决定周期波动,应提前规划旺季备货和预算。',
+        '时间趋势': '长期趋势反映自然增长或衰退,适合作为基准预测因子。',
+        '品类影响': '不同品类之间销量结构差异明显,需要分层预测。',
+        'SKU影响': '头部 SKU 对预测结果贡献高,应优先保证数据质量。'
       }
-      
-      return features.map((feature, index) => {
-        const importanceValue = importance[index] || 0
-        let importanceLevel = '低'
-        let importanceClass = 'text-red-600'
-        
-        if (importanceValue >= 20) {
-          importanceLevel = '高'
-          importanceClass = 'text-green-600'
-        } else if (importanceValue >= 10) {
-          importanceLevel = '中'
-          importanceClass = 'text-yellow-600'
-        }
-        
+      return (this.featureImportance.features || []).map((name, index) => {
+        const value = Math.round(Number((this.featureImportance.importance || [])[index] || 0))
         return {
-          name: feature,
-          importance: importanceValue,
-          importanceLevel: importanceLevel,
-          importanceClass: importanceClass,
-          description: descriptions[feature] || ''
+          name,
+          value,
+          level: value >= 20 ? 'high' : value >= 12 ? 'medium' : value >= 6 ? 'low' : 'minor',
+          description: descriptions[name] || `${name} 对销量预测有 ${value}% 的贡献。`,
+          range: value >= 15 ? '建议重点监控' : '维持常规监控'
         }
-      })
+      }).sort((a, b) => b.value - a.value)
+    },
+    heatmapLabels() {
+      return (this.featureImportance.features || []).slice(0, 7)
+    },
+    heatmapMatrix() {
+      const matrix = this.featureImportance.correlation_matrix || []
+      return this.heatmapLabels.map((name, rowIndex) => ({
+        name,
+        values: this.heatmapLabels.map((col, colIndex) => ({
+          name: col,
+          value: Number((((matrix[rowIndex] || [])[colIndex]) || (rowIndex === colIndex ? 1 : 0)).toFixed(2))
+        }))
+      }))
     },
-    featureCorrelations() {
+    insights() {
+      const top = this.normalizedFeatures[0] || { name: '时间趋势' }
       return [
-        {
-          title: '价格与销量相关性',
-          value: -35,
-          description: '价格上涨通常会导致销量下降'
-        },
-        {
-          title: '促销力度与销量相关性',
-          value: 65,
-          description: '促销力度越大,销量通常越高'
-        },
-        {
-          title: '退款率与销量相关性',
-          value: -40,
-          description: '退款率高会影响产品口碑,导致销量下降'
-        },
-        {
-          title: '季节性与销量相关性',
-          value: 50,
-          description: '季节性因素对销量有显著影响'
-        }
+        `${top.name} 是当前最主要驱动因素,建议优先保证该字段的数据完整性。`,
+        '促销、价格和季节性建议进入同一套对照分析,避免单因子误判。',
+        '对高贡献特征建立阈值告警,可以提升预测解释性和运营响应速度。'
       ]
     }
   },
-  mounted() {
-    console.log('Component mounted, calling getList()...')
-    this.getList()
+  watch: {
+    normalizedFeatures: {
+      immediate: true,
+      handler(list) {
+        if (!this.selectedFeature.name && list.length) this.selectedFeature = list[0]
+      }
+    }
   },
-  beforeUnmount() {
-    if (this.featureRadarChart) this.featureRadarChart.destroy()
+  mounted() {
+    this.loadCachedResults()
   },
   methods: {
-    /** 获取销售分析结果 */
-    getList() {
-      console.log('Getting sales trend results...')
-      getSaleTrendResults().then(response => {
-        console.log('Get results response:', response)
-        if (response && response.success && response.data) {
-          const results = response.data || {}
-          console.log('Sales trend results:', results)
-          this.results = results
-          this.$nextTick(() => {
-            console.log('Rendering feature radar chart...')
-            this.renderFeatureRadarChart()
-          })
-        }
-      }).catch(error => {
-        console.error('Error getting sales trend results:', error)
-        // 即使出错,也使用默认数据显示雷达图
-        this.results = {
-          feature_importance: {
-            features: [
-              '价格',
-              '促销力度',
-              '退款率',
-              '季节性',
-              '时间趋势',
-              '品类影响',
-              'SKU影响'
-            ],
-            importance: [15, 15, 10, 15, 20, 10, 15]
-          }
-        }
-        this.$nextTick(() => {
-          console.log('Rendering feature radar chart with default data...')
-          this.renderFeatureRadarChart()
-        })
-      })
-    },
-    /** 文件选择改变处理 */
-    handleFileChange(file, fileList) {
-      if (this.upload.ignoreFileChange) return
-      console.log('handleFileChange called')
-      console.log('file:', file)
-      console.log('fileList:', fileList)
-      if (!fileList || fileList.length === 0) return
-      if (!file || !file.raw) return
-
-      this.upload.pendingFileName = file.name
-      this.upload.fileName = ''
-      console.log('pendingFileName set to:', this.upload.pendingFileName)
-    },
-    handleUploadSuccess(response, file, fileList) {
-      console.log('handleUploadSuccess called')
-      console.log('response:', response)
-      console.log('file:', file)
+    handleFileChange(file) {
+      this.file = file.raw
+      this.fileName = file.name
     },
-    handleUploadError(error, file, fileList) {
-      console.error('handleUploadError called')
-      console.error('error:', error)
-      console.error('file:', file)
-    },
-    /** 文件上传前的校验 */
-    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
+    async loadCachedResults() {
+      try {
+        const response = await getSaleTrendResults()
+        if (response && response.success) this.results = response.data || {}
+      } catch (e) {
+        this.results = {}
       }
-      return true
     },
-    customUpload(options) {
-      console.log('customUpload called')
-      console.log('options:', options)
-      console.log('options.file:', options.file)
-      if (!options.file) {
-        console.error('No file in options')
-        this.$modal.msgError('请选择要上传的文件')
-        options.onError(new Error('No file to upload'))
-        return
-      }
-      console.log('options.file.raw:', options.file.raw)
-      const file = options.file.raw || options.file
-      console.log('file to upload:', file)
-      if (!file) {
-        console.error('No file to upload')
-        this.$modal.msgError('请选择要上传的文件')
-        options.onError(new Error('No file to upload'))
-        return
-      }
-      console.log('Starting file upload and analysis...')
-      this.upload.isUploading = true
-      analyzeSaleTrendWithFile(file).then(response => {
-        console.log('analyzeSaleTrendWithFile response:', response)
-        this.upload.isUploading = false
-        if (response && response.success) {
-          const results = response.data || {}
-          console.log('Upload analysis results:', results)
-          this.results = results
-          this.$modal.msgSuccess('文件上传并分析成功')
-          this.upload.fileName = this.upload.pendingFileName || file.name
-          this.upload.pendingFileName = ''
-          this.$nextTick(() => {
-            console.log('Rendering feature radar chart...')
-            this.renderFeatureRadarChart()
-          })
-          options.onSuccess(response)
+    async startAnalysis() {
+      this.loading = true
+      try {
+        if (this.file) {
+          const response = await analyzeSaleTrendWithFile(this.file)
+          if (!response.success) throw new Error(response.message || '分析失败')
+          this.results = response.data || {}
+          this.selectedFeature = this.normalizedFeatures[0] || {}
+          this.$modal.msgSuccess('特征分析完成')
         } else {
-          console.error('Upload failed with response:', response)
-          this.$modal.msgError(response.message || '分析失败')
-          options.onError(new Error(response.message || '分析失败'))
-        }
-      }).catch(error => {
-        console.error('analyzeSaleTrendWithFile error:', error)
-        this.upload.isUploading = false
-        const msg = (error && error.message) || '文件上传失败,请重试'
-        this.$modal.msgError(msg)
-        options.onError(error)
-        // 即使上传失败,也使用默认数据显示雷达图
-        this.results = {
-          feature_importance: {
-            features: [
-              '价格',
-              '促销力度',
-              '退款率',
-              '季节性',
-              '时间趋势',
-              '品类影响',
-              'SKU影响'
-            ],
-            importance: [15, 15, 10, 15, 20, 10, 15]
+          await this.loadCachedResults()
+          if (!Object.keys(this.results || {}).length) {
+            this.$modal.msgWarning('暂无趋势分析缓存,请先上传销售数据')
+            return
           }
+          this.selectedFeature = this.normalizedFeatures[0] || {}
+          this.$modal.msgSuccess('已加载缓存特征分析')
         }
-        this.$nextTick(() => {
-          console.log('Rendering feature radar chart with default data...')
-          this.renderFeatureRadarChart()
-        })
-      }).finally(() => {
-        if (this.$refs.toolbarUpload) {
-          this.upload.ignoreFileChange = true
-          this.$refs.toolbarUpload.clearFiles()
-          this.$nextTick(() => {
-            this.upload.ignoreFileChange = false
-          })
-        }
-      })
-    },
-    submitUpload() {
-      console.log('submitUpload called')
-      const target = this.$refs.toolbarUpload
-      console.log('target:', target)
-      if (!target) {
-        console.error('No upload component reference')
-        this.$modal.msgError('上传组件未初始化')
-        return
-      }
-      const fileList = target.uploadFiles || []
-      console.log('fileList:', fileList)
-      if (!fileList || fileList.length === 0) {
-        console.error('No files selected')
-        this.$modal.msgError('请选择要上传的文件')
-        return
+      } catch (error) {
+        this.$modal.msgError(error.message || '分析失败')
+      } finally {
+        this.loading = false
       }
-      console.log('Calling target.submit()')
-      target.submit()
     },
-    /** 渲染特征重要性雷达图 */
-    renderFeatureRadarChart() {
-      console.log('Rendering feature radar chart...')
-      const canvas = this.$refs.featureRadarRef
-      if (!canvas) {
-        console.error('No canvas reference found')
-        return
-      }
-      
-      console.log('Feature importance data:', this.featureImportance)
-      const features = this.featureImportance.features
-      const importance = this.featureImportance.importance
-      
-      console.log('Features:', features)
-      console.log('Importance:', importance)
-      
-      // 确保 features 和 importance 是数组且长度相同
-      if (!Array.isArray(features) || !Array.isArray(importance) || features.length !== importance.length) {
-        console.error('Invalid feature importance data:', { features, importance })
-        // 使用默认数据
-        const defaultFeatures = [
-          '价格',
-          '促销力度',
-          '退款率',
-          '季节性',
-          '时间趋势',
-          '品类影响',
-          'SKU影响'
-        ]
-        const defaultImportance = [15, 15, 10, 15, 20, 10, 15]
-        this.renderChart(canvas, defaultFeatures, defaultImportance)
-        return
-      }
-      
-      this.renderChart(canvas, features, importance)
+    heatColor(value) {
+      if (value >= 0.8) return '#f97316'
+      if (value >= 0.55) return '#22c55e'
+      if (value >= 0.35) return '#3b82f6'
+      return '#94a3b8'
     },
-    /** 渲染雷达图 */
-    renderChart(canvas, features, importance) {
-      if (this.featureRadarChart) this.featureRadarChart.destroy()
-      
-      try {
-        // 使用 Chart.js v2 语法
-        this.featureRadarChart = new Chart(canvas, {
-          type: 'radar',
-          data: {
-            labels: features,
-            datasets: [{
-              label: '特征重要性',
-              data: importance,
-              backgroundColor: 'rgba(59, 130, 246, 0.2)',
-              borderColor: 'rgba(59, 130, 246, 1)',
-              pointBackgroundColor: 'rgba(59, 130, 246, 1)',
-              pointBorderColor: '#fff',
-              pointHoverBackgroundColor: '#fff',
-              pointHoverBorderColor: 'rgba(59, 130, 246, 1)'
-            }]
-          },
-          options: {
-            responsive: true,
-            maintainAspectRatio: false,
-            scale: {
-              ticks: {
-                beginAtZero: true,
-                max: 100
-              }
-            },
-            legend: {
-              position: 'top'
-            },
-            tooltips: {
-              callbacks: {
-                label: function(tooltipItem, data) {
-                  return data.labels[tooltipItem.index] + ': ' + data.datasets[0].data[tooltipItem.index] + '%'
-                }
-              }
-            }
-          }
-        })
-        console.log('Chart rendered successfully')
-      } catch (error) {
-        console.error('Error rendering chart:', error)
-      }
+    copyText(text) {
+      navigator.clipboard && navigator.clipboard.writeText(text)
+      this.$modal.msgSuccess('已复制')
     },
-    /** 导出结果 */
-    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' })
+    exportModel() {
+      const blob = new Blob([JSON.stringify(this.results.feature_importance || {}, null, 2)], { type: 'application/json;charset=utf-8' })
       const url = URL.createObjectURL(blob)
-      const a = document.createElement('a')
-      a.href = url
-      a.download = `feature_importance_${this.formatUploadDate(new Date())}.json`
-      document.body.appendChild(a)
-      a.click()
-      document.body.removeChild(a)
+      const link = document.createElement('a')
+      link.href = url
+      link.download = `feature_importance_${Date.now()}.json`
+      link.click()
       URL.revokeObjectURL(url)
-    },
-    /** 格式化上传日期 */
-    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}`
     }
   }
 }
 </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-4 { padding-left: 16px; padding-right: 16px; }
-.py-3 { padding-top: 12px; padding-bottom: 12px; }
-.mb-6 { margin-bottom: 24px; }
-.mt-2 { margin-top: 8px; }
-.mt-3 { margin-top: 12px; }
-
-.flex { display: flex; }
-.items-center { align-items: center; }
-.justify-between { justify-content: space-between; }
-.gap-2 { gap: 8px; }
-.gap-6 { gap: 24px; }
-
-.grid { display: grid; }
-.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
-.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-md { font-size: 16px; 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-wider { letter-spacing: 0.06em; }
-
-.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-blue-600 { color: #2563eb; }
-.text-green-700 { color: #15803d; }
-.text-green-600 { color: #16a34a; }
-.text-red-600 { color: #dc2626; }
-.text-yellow-600 { color: #ca8a04; }
-
-.bg-white { background-color: #ffffff; }
-.bg-gray-200 { background-color: #e5e7eb; }
-.bg-blue-500 { background-color: #3b82f6; }
-.bg-green-500 { background-color: #10b981; }
-
-.border { border: 1px solid #e5e7eb; }
-.border-gray-200 { border-color: #e5e7eb; }
-.rounded-xl { border-radius: 0.75rem; }
-.rounded-lg { border-radius: 0.5rem; }
-.rounded-full { border-radius: 9999px; }
-
-.shadow-sm { box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); }
-
-.h-96 { height: 24rem; }
-
-.flex-1 { flex: 1; }
-.ml-2 { margin-left: 8px; }
-
-@media (min-width: 768px) {
-  .md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
-}
-</style>
+.sales-page { padding: 20px; max-width: 1400px; margin: 0 auto; background: #f0f2f5; min-height: calc(100vh - 84px); }
+.header-bar, .settings-bar, .panel { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,.06); }
+.header-bar { display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; margin-bottom: 20px; gap: 16px; flex-wrap: wrap; }
+.header-title { margin: 0; font-size: 20px; font-weight: 600; color: #303133; }
+.header-actions { display: flex; gap: 10px; flex-wrap: wrap; }
+.settings-bar { display: flex; gap: 15px; align-items: flex-end; padding: 20px 24px; margin-bottom: 20px; flex-wrap: wrap; }
+.setting-item { display: flex; flex-direction: column; gap: 6px; min-width: 150px; }
+.setting-item label { font-size: 12px; color: #606266; }
+.file-hint { margin-left: auto; color: #67c23a; font-size: 13px; }
+.panel { padding: 24px; margin-bottom: 20px; min-width: 0; }
+.panel h3 { margin: 0 0 20px; font-size: 16px; color: #303133; }
+.feature-list { display: flex; flex-direction: column; gap: 12px; }
+.feature-item { display: flex; align-items: center; gap: 15px; padding: 10px 12px; border-radius: 6px; cursor: pointer; }
+.feature-item:hover, .feature-item.active { background: #eef7fb; }
+.feature-item.active { border-left: 3px solid #17a2b8; }
+.feature-name { width: 120px; flex-shrink: 0; color: #606266; }
+.feature-bar-container { flex: 1; height: 24px; background: #f0f2f5; border-radius: 12px; overflow: hidden; }
+.feature-bar { height: 100%; color: white; display: flex; align-items: center; justify-content: flex-end; padding-right: 10px; border-radius: 12px; transition: width .3s; }
+.feature-bar.high { background: linear-gradient(90deg,#667eea,#764ba2); }
+.feature-bar.medium { background: linear-gradient(90deg,#409eff,#667eea); }
+.feature-bar.low { background: linear-gradient(90deg,#67c23a,#85ce61); }
+.feature-bar.minor { background: linear-gradient(90deg,#909399,#b4bccc); }
+.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
+.heatmap { overflow: auto; }
+.heat-row { display: grid; grid-template-columns: 80px repeat(7, 48px); gap: 4px; align-items: center; margin-bottom: 4px; }
+.heat-row span, .heat-row b { font-size: 12px; color: #606266; text-align: right; padding-right: 6px; }
+.heat-row b { text-align: center; padding: 0; }
+.heat-row button { width: 48px; height: 40px; border: 0; color: white; border-radius: 4px; cursor: pointer; }
+.feature-detail { background: #f8f9fa; border-radius: 8px; padding: 20px; }
+.feature-detail h2 { margin: 0 0 18px; font-size: 20px; }
+.detail-row { display: flex; justify-content: space-between; margin-bottom: 16px; color: #606266; }
+.positive { color: #67c23a; }
+.detail-row em { background: #67c23a; color: white; border-radius: 4px; padding: 4px 10px; font-style: normal; }
+.feature-detail p { color: #606266; line-height: 1.7; }
+.insights { display: grid; grid-template-columns: repeat(auto-fit,minmax(260px,1fr)); gap: 15px; }
+.insight-card { background: linear-gradient(135deg,#fffbeb,#fef3c7); border-radius: 8px; padding: 18px; }
+.insight-card i { font-size: 22px; color: #f59e0b; }
+.insight-card p { line-height: 1.6; color: #606266; }
+@media (max-width: 1100px) { .two-col { grid-template-columns: 1fr; } .file-hint { margin-left: 0; } }
+</style>

+ 193 - 1076
src/views/sale/overview/index.vue

@@ -1,1123 +1,240 @@
 <template>
-  <div class="app-container">
-    <!-- 页面标题 -->
-    <div class="page-header">
-      <h2><i class="el-icon-s-data"></i> 销售整体看板</h2>
-      <p class="page-desc">全局销售数据概览,包含关键指标和趋势分析</p>
-    </div>
-
-    <!-- 文件上传区域 -->
-    <el-card class="mb-20">
-      <div slot="header">
-        <span><i class="el-icon-upload"></i> 数据文件上传</span>
+  <div class="sales-page">
+    <section class="header-bar">
+      <h1 class="header-title">数据监控</h1>
+      <div class="header-actions">
+        <el-upload action="" :auto-upload="false" :show-file-list="false" accept=".xlsx,.xls,.csv" :on-change="handleFileChange">
+          <el-button size="small" icon="el-icon-upload2">上传销售数据</el-button>
+        </el-upload>
+        <el-button size="small" icon="el-icon-refresh" :loading="loading" @click="refreshData">刷新</el-button>
+        <el-button size="small" plain icon="el-icon-setting" @click="settingsVisible = true">设置</el-button>
+        <el-button size="small" type="primary" icon="el-icon-download" @click="exportData">导出</el-button>
       </div>
-      <el-upload
-        ref="upload"
-        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"
-        drag
-      >
-        <i class="el-icon-upload"></i>
-        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
-        <div class="el-upload__tip" slot="tip">
-          <el-checkbox v-model="upload.updateSupport" /> 是否覆盖已上传的文件
-          <div>只能上传xlsx/xls/csv文件,且不超过500MB</div>
+    </section>
+
+    <section class="panel">
+      <h3>数据同步状态</h3>
+      <div class="platform-grid">
+        <div v-for="platform in platforms" :key="platform.name" class="platform-card" :class="platform.status">
+          <strong>{{ platform.name }}</strong>
+          <i :class="platform.icon"></i>
+          <span>{{ platform.label }}</span>
+          <b>{{ platform.rate }}</b>
+          <em>最后同步:{{ platform.lastSync }}</em>
         </div>
-      </el-upload>
-      <div style="margin-top: 15px">
-        <el-button :loading="upload.isUploading" type="primary" @click="submitUpload">立即上传并分析</el-button>
-        <el-button type="success" :disabled="!hasResults" @click="exportResults">导出分析</el-button>
-        <el-button @click="resetUpload">重置</el-button>
       </div>
-    </el-card>
-
-    <!-- 视图选择区域 -->
-    <el-card v-if="results.summary" class="mb-20">
-      <div slot="header">
-        <span><i class="el-icon-s-operation"></i> 数据视图选择</span>
+      <div class="log-section">
+        <div class="log-header"><span>同步日志</span><small>最近 {{ logs.length }} 条记录</small></div>
+        <div v-for="log in logs" :key="log.id" class="log-item" :class="{ expanded: expandedLog === log.id }" @click="expandedLog = expandedLog === log.id ? '' : log.id">
+          <div class="log-line">
+            <i :class="log.icon"></i>
+            <span>{{ log.title }}</span>
+            <time>{{ log.time }}</time>
+          </div>
+          <p v-if="expandedLog === log.id">{{ log.detail }}</p>
+        </div>
       </div>
-      <div class="view-selector">
-        <el-radio-group v-model="selectedView" @change="handleViewChange">
-          <el-radio-button label="overall">总体概览</el-radio-button>
-          <el-radio-button label="category">按品类查看</el-radio-button>
-          <el-radio-button label="sku">按SKU查看</el-radio-button>
-        </el-radio-group>
-        
-        <!-- 品类选择 -->
-        <el-select v-if="selectedView === 'category' && results.category_list && results.category_list.length > 0" 
-                   v-model="selectedCategory" 
-                   placeholder="选择品类" 
-                   @change="handleCategoryChange"
-                   style="margin-left: 10px; width: 200px;">
-          <el-option v-for="category in results.category_list" 
-                     :key="category" 
-                     :label="category" 
-                     :value="category" />
-        </el-select>
-        
-        <!-- SKU选择 -->
-        <el-select v-if="(selectedView === 'sku' || selectedView === 'category') && getAvailableSkus().length > 0" 
-                   v-model="selectedSku" 
-                   placeholder="选择SKU" 
-                   @change="handleSkuChange"
-                   style="margin-left: 10px; width: 200px;">
-          <el-option v-for="sku in getAvailableSkus()" 
-                     :key="sku" 
-                     :label="sku" 
-                     :value="sku" />
-        </el-select>
+    </section>
+
+    <section class="overview-grid">
+      <div class="overview-card">
+        <div>
+          <span>今日销售额</span>
+          <strong>{{ formatMoney(todayRevenue) }}</strong>
+          <em class="up">+{{ revenueGrowth }}% 较昨日</em>
+        </div>
+        <i class="el-icon-money sales"></i>
       </div>
-    </el-card>
-
-    <!-- 关键指标卡片 -->
-    <el-row :gutter="20" class="mb-20">
-      <el-col :xs="24" :sm="12" :md="8" :lg="6">
-        <el-card class="stat-card">
-          <div class="stat-content">
-            <div class="stat-info">
-              <p class="stat-label">当前总销量</p>
-              <p class="stat-value">{{ totalSales }}</p>
-              <p class="stat-desc" :class="salesGrowthRate >= 0 ? 'stat-desc-success' : 'stat-desc-error'">
-                增长率 {{ salesGrowthRate >= 0 ? '+' : '' }}{{ salesGrowthRate }}%
-              </p>
-            </div>
-            <div class="stat-icon stat-icon-blue">
-              <i class="el-icon-s-order"></i>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-      <el-col :xs="24" :sm="12" :md="8" :lg="6">
-        <el-card class="stat-card">
-          <div class="stat-content">
-            <div class="stat-info">
-              <p class="stat-label">平均价格</p>
-              <p class="stat-value">¥{{ avgPrice.toFixed(2) }}</p>
-              <p class="stat-desc" :class="priceChange >= 0 ? 'stat-desc-success' : 'stat-desc-error'">
-                变化 {{ priceChange >= 0 ? '+' : '' }}{{ priceChange.toFixed(2) }}%
-              </p>
-            </div>
-            <div class="stat-icon stat-icon-green">
-              <i class="el-icon-s-finance"></i>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-      <el-col :xs="24" :sm="12" :md="8" :lg="6">
-        <el-card class="stat-card">
-          <div class="stat-content">
-            <div class="stat-info">
-              <p class="stat-label">平均促销力度</p>
-              <p class="stat-value">{{ avgPromotion.toFixed(2) }}%</p>
-              <p class="stat-desc" :class="promotionChange >= 0 ? 'stat-desc-success' : 'stat-desc-error'">
-                变化 {{ promotionChange >= 0 ? '+' : '' }}{{ promotionChange.toFixed(2) }}%
-              </p>
-            </div>
-            <div class="stat-icon stat-icon-yellow">
-              <i class="el-icon-s-marketing"></i>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-      <el-col :xs="24" :sm="12" :md="8" :lg="6">
-        <el-card class="stat-card">
-          <div class="stat-content">
-            <div class="stat-info">
-              <p class="stat-label">异常数据检测率</p>
-              <p class="stat-value">{{ anomalyDetectionRate.toFixed(2) }}%</p>
-              <p class="stat-desc">基于销售数据异常模式分析</p>
-            </div>
-            <div class="stat-icon stat-icon-red">
-              <i class="el-icon-warning-outline"></i>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <!-- 趋势图表 -->
-    <el-row :gutter="20" class="mb-20">
-      <el-col :xs="24" :sm="24" :md="12" :lg="12">
-        <el-card>
-          <div slot="header">
-            <span><i class="el-icon-data-line"></i> 平均价格变化趋势</span>
-            <span class="header-desc">按时间段统计</span>
-          </div>
-          <div ref="priceTrendChart" style="height: 400px"></div>
-        </el-card>
-      </el-col>
-      <el-col :xs="24" :sm="24" :md="12" :lg="12">
-        <el-card>
-          <div slot="header">
-            <span><i class="el-icon-data-line"></i> 平均促销力度变化趋势</span>
-            <span class="header-desc">按时间段统计</span>
-          </div>
-          <div ref="promotionTrendChart" style="height: 400px"></div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <!-- 销量与异常检测图表 -->
-    <el-row :gutter="20" class="mb-20">
-      <el-col :xs="24" :sm="24" :md="12" :lg="12">
-        <el-card>
-          <div slot="header">
-            <span><i class="el-icon-s-order"></i> 销量趋势</span>
-            <span class="header-desc">按时间段统计</span>
-          </div>
-          <div ref="salesTrendChart" style="height: 400px"></div>
-        </el-card>
-      </el-col>
-      <el-col :xs="24" :sm="24" :md="12" :lg="12">
-        <el-card>
-          <div slot="header">
-            <span><i class="el-icon-warning"></i> 异常数据检测</span>
-            <span class="header-desc">异常数据分布</span>
-          </div>
-          <div ref="anomalyDetectionChart" style="height: 400px"></div>
-        </el-card>
-      </el-col>
-    </el-row>
+      <div class="overview-card">
+        <div>
+          <span>今日订单量</span>
+          <strong>{{ formatNumber(todayOrders) }}</strong>
+          <em class="up">+{{ orderGrowth }}% 较昨日</em>
+        </div>
+        <i class="el-icon-s-order orders"></i>
+      </div>
+    </section>
 
-    <!-- 异常数据详情 -->
-    <el-card v-if="results.anomalies && results.anomalies.anomaly_count > 0" class="mb-20">
-      <div slot="header">
-        <span><i class="el-icon-warning-outline"></i> 异常数据详情</span>
-        <span class="header-desc">共检测到 {{ results.anomalies.anomaly_count }} 个异常,异常率 {{ results.anomalies.anomaly_rate.toFixed(2) }}%</span>
+    <section class="panel">
+      <div class="panel-head">
+        <h3>数据源配置</h3>
+        <el-button size="small" type="primary" icon="el-icon-plus" @click="addSource">添加数据源</el-button>
       </div>
-      <el-table :data="anomalyData" style="width: 100%">
-        <el-table-column prop="date" label="日期" width="120">
-          <template slot-scope="scope">
-            {{ scope.row.date || 'N/A' }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="sku" label="SKU" width="180">
-          <template slot-scope="scope">
-            {{ scope.row.sku || 'N/A' }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="type" label="异常类型" width="120">
-          <template slot-scope="scope">
-            <el-tag :type="getAnomalyTypeTag(scope.row.type)">
-              {{ getAnomalyTypeName(scope.row.type) }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="reason" label="异常原因" min-width="300">
-          <template slot-scope="scope">
-            <span class="anomaly-reason">{{ scope.row.reason }}</span>
-          </template>
+      <el-table :data="platforms" size="small">
+        <el-table-column prop="name" label="平台名称" min-width="120" />
+        <el-table-column label="API 状态" min-width="120">
+          <template slot-scope="{ row }"><span class="status-dot" :class="row.status"></span> {{ row.label }}</template>
         </el-table-column>
-        <el-table-column prop="value" label="实际值" width="100">
-          <template slot-scope="scope">
-            {{ scope.row.value.toFixed(2) }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="expected" label="预期值" width="100">
-          <template slot-scope="scope">
-            {{ scope.row.expected.toFixed(2) }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="deviation" label="偏差程度" width="100">
-          <template slot-scope="scope">
-            <el-progress 
-              :percentage="Math.min(scope.row.deviation * 20, 100)" 
-              :color="getDeviationColor(scope.row.deviation)"
-              :stroke-width="10"
-            />
-            <span class="deviation-value">{{ scope.row.deviation.toFixed(2) }}</span>
+        <el-table-column prop="frequency" label="同步频率" min-width="120" />
+        <el-table-column prop="lastSync" label="最后同步时间" min-width="160" />
+        <el-table-column label="操作" min-width="180">
+          <template slot-scope="{ row }">
+            <el-button size="mini" type="success" :disabled="row.status === 'syncing'" @click="syncPlatform(row)">立即同步</el-button>
+            <el-button size="mini" plain @click="editPlatform(row)">编辑配置</el-button>
           </template>
         </el-table-column>
       </el-table>
-    </el-card>
+    </section>
+
+    <el-dialog title="系统设置" :visible.sync="settingsVisible" width="480px">
+      <el-form label-position="top">
+        <el-form-item label="全局同步频率"><el-select v-model="settings.frequency"><el-option label="每 5 分钟" value="5" /><el-option label="每 30 分钟" value="30" /><el-option label="每小时" value="60" /></el-select></el-form-item>
+        <el-form-item label="失败重试次数"><el-input-number v-model="settings.retry" :min="1" :max="10" /></el-form-item>
+        <el-form-item label="销售额下降告警阈值"><el-input-number v-model="settings.warning" :min="1" :max="100" /> %</el-form-item>
+      </el-form>
+      <span slot="footer"><el-button @click="settingsVisible = false">取消</el-button><el-button type="primary" @click="saveSettings">保存设置</el-button></span>
+    </el-dialog>
   </div>
 </template>
 
 <script>
 import { analyzeSaleOverviewWithFile, getSaleOverviewResults } from '@/api/client'
-import { getToken } from '@/utils/auth'
-import * as echarts from 'echarts'
-require('echarts/theme/macarons')
 
 export default {
-  name: 'SalesOverview',
+  name: 'SaleDataMonitor',
   data() {
     return {
-      // 图表实例
-      priceTrendChart: null,
-      promotionTrendChart: null,
-      salesTrendChart: null,
-      anomalyDetectionChart: null,
-      // 数据
+      loading: false,
+      file: null,
       results: {},
-      // 计算属性数据
-      totalSales: 0,
-      salesGrowthRate: 0,
-      avgPrice: 0,
-      priceChange: 0,
-      avgPromotion: 0,
-      promotionChange: 0,
-      anomalyDetectionRate: 0,
-      // 趋势数据
-      timeSeries: [],
-      priceSeries: [],
-      promotionSeries: [],
-      salesSeries: [],
-      anomalySeries: [],
-      // 视图选择相关
-      selectedView: 'overall',
-      selectedCategory: '',
-      selectedSku: '',
-      // 文件上传相关
-      upload: {
-        // 是否显示弹出层
-        open: false,
-        // 弹出层标题
-        title: '',
-        // 是否禁用上传
-        isUploading: false,
-        // 是否更新已经存在的文件
-        updateSupport: 0,
-        // 设置上传的请求头部
-        headers: { Authorization: 'Bearer ' + getToken() },
-        // 上传的地址
-        url: process.env.VUE_APP_PYTHON_API + '/api/sale-overview/upload',
-        // 文件名称
-        fileName: '',
-        // 已选择文件名称
-        pendingFileName: '',
-        // 是否忽略文件选择改变
-        ignoreFileChange: false,
-      }
-    }
-  },
-  mounted() {
-    this.$nextTick(() => {
-      this.initCharts()
-    })
-    // 监听窗口大小变化
-    window.addEventListener('resize', this.handleResize)
-  },
-  beforeDestroy() {
-    // 销毁图表实例
-    if (this.priceTrendChart) {
-      this.priceTrendChart.dispose()
-    }
-    if (this.promotionTrendChart) {
-      this.promotionTrendChart.dispose()
-    }
-    if (this.salesTrendChart) {
-      this.salesTrendChart.dispose()
+      expandedLog: '',
+      settingsVisible: false,
+      settings: { frequency: '5', retry: 5, warning: 20 },
+      platforms: [
+        { name: '天猫', status: 'online', label: '在线', icon: 'el-icon-success', rate: '1,280 单/小时', lastSync: '刚刚', frequency: '每 5 分钟' },
+        { name: '京东', status: 'syncing', label: '同步中', icon: 'el-icon-loading', rate: '856 单/小时', lastSync: '2 分钟前', frequency: '每 10 分钟' },
+        { name: '抖音', status: 'online', label: '在线', icon: 'el-icon-success', rate: '2,340 单/小时', lastSync: '刚刚', frequency: '每 10 分钟' },
+        { name: '拼多多', status: 'error', label: '异常', icon: 'el-icon-error', rate: '0 单/小时', lastSync: '1 小时前', frequency: '每 30 分钟' },
+        { name: '快手', status: 'offline', label: '未连接', icon: 'el-icon-remove', rate: '0 单/小时', lastSync: '3 天前', frequency: '每 30 分钟' }
+      ],
+      logs: [
+        { id: '1', icon: 'el-icon-success', title: '天猫 - 同步完成', time: '14:32:15', detail: '同步数据量:2,845 条 | 耗时:4.2 秒 | 成功率:100%' },
+        { id: '2', icon: 'el-icon-loading', title: '京东 - 正在同步', time: '14:31:48', detail: '正在同步第 1,568/3,200 条数据' },
+        { id: '3', icon: 'el-icon-warning-outline', title: '拼多多 - 同步失败:API 连接超时', time: '14:28:15', detail: '连接超时 30 秒 | 重试次数:3/5 | 下次重试:5 分钟后' }
+      ]
     }
-    if (this.anomalyDetectionChart) {
-      this.anomalyDetectionChart.dispose()
-    }
-    window.removeEventListener('resize', this.handleResize)
   },
-  methods: {
-    /** 文件上传前的校验 */
-    beforeUpload(file) {
-      const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
-                      file.type === 'application/vnd.ms-excel' ||
-                      file.type === 'text/csv' ||
-                      file.name.endsWith('.xlsx') ||
-                      file.name.endsWith('.xls') ||
-                      file.name.endsWith('.csv')
-      const 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
-    },
-    /** 文件选择改变处理 */
-    handleFileChange(file, fileList) {
-      if (this.upload.ignoreChange) return
-      if (!fileList || fileList.length === 0) return
-      if (!file || !file.raw) return
-
-      this.upload.pendingFileName = file.name
-      this.upload.fileName = ''
-    },
-    /** 文件上传中处理 */
-    handleFileUploadProgress(event, file, fileList) {
-      this.upload.isUploading = true
+  computed: {
+    summary() {
+      return this.results.summary || {}
     },
-    /** 自定义上传方法 */
-    customUpload(options) {
-      const file = options.file
-      this.upload.isUploading = true
-      analyzeSaleOverviewWithFile(file).then(response => {
-        this.upload.isUploading = false
-        if (response && response.success) {
-          this.$modal.msgSuccess('文件上传并分析成功')
-          this.results = response.data || {}
-          this.calculateMetrics()
-          this.$nextTick(() => {
-            this.renderCharts()
-          })
-          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)
-      })
+    todayRevenue() {
+      return Number(this.summary.total_revenue || 128560)
     },
-    /** 提交上传文件 */
-    submitUpload() {
-      const fileList = this.$refs.upload.uploadFiles
-      if (!fileList || fileList.length === 0) {
-        this.$modal.msgError('请选择要上传的文件')
-        return
-      }
-      this.$refs.upload.submit()
+    todayOrders() {
+      return Number(this.summary.total_orders || 2340)
     },
-    /** 重置上传 */
-    resetUpload() {
-      this.$refs.upload.clearFiles()
+    revenueGrowth() {
+      return Number(this.summary.revenue_growth || 15.3).toFixed(1)
     },
-    /** 获取销售分析结果 */
-    getList() {
-      getSaleOverviewResults().then(response => {
-        if (response && response.success && response.data) {
-          this.results = response.data || {}
-          this.calculateMetrics()
-          this.$nextTick(() => {
-            this.renderCharts()
-          })
-        }
-      }).catch(() => {
+    orderGrowth() {
+      return Number(this.summary.order_growth || 8.7).toFixed(1)
+    }
+  },
+  mounted() {
+    this.loadCachedResults()
+  },
+  methods: {
+    handleFileChange(file) {
+      this.file = file.raw
+      this.refreshData()
+    },
+    async loadCachedResults() {
+      try {
+        const response = await getSaleOverviewResults()
+        if (response && response.success) this.results = response.data || {}
+      } catch (e) {
         this.results = {}
-      })
-    },
-    /** 计算关键指标 */
-    calculateMetrics() {
-      if (!this.results.summary) {
-        return
-      }
-      
-      // 根据选择的视图计算指标
-      switch (this.selectedView) {
-        case 'overall':
-          this.calculateOverallMetrics()
-          break
-        case 'category':
-          this.calculateCategoryMetrics()
-          break
-        case 'sku':
-          this.calculateSkuMetrics()
-          break
-        default:
-          this.calculateOverallMetrics()
-      }
-    },
-    
-    /** 计算总体指标 */
-    calculateOverallMetrics() {
-      if (this.results.summary) {
-        this.totalSales = this.results.summary.total_quantity || 0
-        this.avgPrice = this.results.summary.total_revenue / this.results.summary.total_quantity || 0
-        
-        // 模拟增长率和变化率数据
-        this.salesGrowthRate = 12.5
-        this.priceChange = -2.3
-        this.avgPromotion = 15.8
-        this.promotionChange = 3.2
-        this.anomalyDetectionRate = 5.7
-        
-        // 模拟趋势数据
-        this.timeSeries = ['1月', '2月', '3月', '4月', '5月', '6月']
-        this.priceSeries = [95.2, 92.8, 90.5, 88.9, 90.2, 89.6]
-        this.promotionSeries = [12.5, 13.2, 14.8, 15.5, 16.2, 15.8]
-        this.salesSeries = [8500, 9200, 10500, 11200, 11800, 12580]
-        this.anomalySeries = [4.2, 3.8, 5.1, 6.5, 7.2, 5.7]
       }
     },
-    
-    /** 计算品类指标 */
-    calculateCategoryMetrics() {
-      if (this.selectedCategory && this.results.categories && this.results.categories[this.selectedCategory]) {
-        const categoryData = this.results.categories[this.selectedCategory]
-        this.totalSales = categoryData.total_quantity || 0
-        this.avgPrice = categoryData.avg_price || 0
-        
-        // 模拟增长率和变化率数据
-        this.salesGrowthRate = 15.2
-        this.priceChange = 1.8
-        this.avgPromotion = 18.5
-        this.promotionChange = 2.5
-        this.anomalyDetectionRate = 4.8
-        
-        // 使用品类的趋势数据
-        this.timeSeries = categoryData.date_series || ['1月', '2月', '3月', '4月', '5月', '6月']
-        this.priceSeries = categoryData.price_series || [90.2, 91.5, 92.8, 93.1, 92.5, 91.8]
-        this.salesSeries = categoryData.quantity_series || [7500, 8200, 9100, 9800, 10500, 11200]
-        this.promotionSeries = [16.5, 17.2, 18.1, 19.0, 18.8, 18.5]
-        this.anomalySeries = [3.8, 4.2, 4.5, 5.1, 4.9, 4.8]
-      } else {
-        // 默认选择第一个品类
-        if (this.results.category_list && this.results.category_list.length > 0) {
-          this.selectedCategory = this.results.category_list[0]
-          this.calculateCategoryMetrics()
-        } else {
-          this.calculateOverallMetrics()
-        }
-      }
-    },
-    
-    /** 计算SKU指标 */
-    calculateSkuMetrics() {
-      if (this.selectedSku && this.results.data && this.results.data[this.selectedSku]) {
-        const skuData = this.results.data[this.selectedSku]
-        this.totalSales = skuData.total_quantity || 0
-        this.avgPrice = skuData.avg_price || 0
-        
-        // 模拟增长率和变化率数据
-        this.salesGrowthRate = 22.8
-        this.priceChange = -0.5
-        this.avgPromotion = 22.5
-        this.promotionChange = 3.8
-        this.anomalyDetectionRate = 3.2
-        
-        // 使用SKU的趋势数据
-        this.timeSeries = skuData.date_series || ['1月', '2月', '3月', '4月', '5月', '6月']
-        this.priceSeries = skuData.price_series || [85.2, 84.8, 85.1, 84.9, 84.7, 84.5]
-        this.salesSeries = skuData.quantity_series || [1200, 1350, 1500, 1650, 1800, 1950]
-        this.promotionSeries = [20.5, 21.2, 21.8, 22.5, 23.0, 22.5]
-        this.anomalySeries = [2.8, 3.1, 3.3, 3.5, 3.2, 3.0]
-      } else {
-        // 默认选择第一个SKU
-        if (this.results.sku_list && this.results.sku_list.length > 0) {
-          this.selectedSku = this.results.sku_list[0]
-          this.calculateSkuMetrics()
+    async refreshData() {
+      this.loading = true
+      try {
+        if (this.file) {
+          const response = await analyzeSaleOverviewWithFile(this.file)
+          if (!response.success) throw new Error(response.message || '分析失败')
+          this.results = response.data || {}
+          this.$modal.msgSuccess('数据已刷新')
         } else {
-          this.calculateOverallMetrics()
-        }
-      }
-    },
-    
-    /** 处理视图变化 */
-    handleViewChange() {
-      this.calculateMetrics()
-      this.renderCharts()
-    },
-    
-    /** 处理品类变化 */
-    handleCategoryChange() {
-      // Reset selected SKU and select first available in new category
-      const availableSkus = this.getAvailableSkus()
-      if (availableSkus.length > 0) {
-        this.selectedSku = availableSkus[0]
-      } else {
-        this.selectedSku = ''
-      }
-      this.calculateMetrics()
-      this.renderCharts()
-    },
-    
-    /** 处理SKU变化 */
-    handleSkuChange() {
-      this.calculateMetrics()
-      this.renderCharts()
-    },
-    
-    /** 获取可用的SKU列表 */
-    getAvailableSkus() {
-      if (this.selectedView === 'category' && this.selectedCategory && this.results.category_skus) {
-        return this.results.category_skus[this.selectedCategory] || []
-      } else {
-        return this.results.sku_list || []
-      }
-    },
-    
-    /** 获取异常类型标签 */
-    getAnomalyTypeTag(type) {
-      switch (type) {
-        case 'quantity_spike':
-        case 'sku_price_spike':
-          return 'warning'
-        case 'quantity_drop':
-        case 'sku_price_drop':
-          return 'danger'
-        case 'price_spike':
-          return 'info'
-        case 'price_drop':
-          return 'success'
-        default:
-          return 'primary'
-      }
-    },
-    
-    /** 获取异常类型名称 */
-    getAnomalyTypeName(type) {
-      switch (type) {
-        case 'quantity_spike':
-          return '销量激增'
-        case 'quantity_drop':
-          return '销量骤降'
-        case 'price_spike':
-          return '价格上涨'
-        case 'price_drop':
-          return '价格下降'
-        case 'sku_price_spike':
-          return 'SKU涨价'
-        case 'sku_price_drop':
-          return 'SKU降价'
-        default:
-          return '异常'
-      }
-    },
-    
-    /** 获取偏差程度颜色 */
-    getDeviationColor(deviation) {
-      if (deviation > 3) {
-        return '#ff4d4f'
-      } else if (deviation > 2.5) {
-        return '#fa8c16'
-      } else if (deviation > 2) {
-        return '#faad14'
-      } else {
-        return '#52c41a'
-      }
-    },
-    /** 初始化图表 */
-    initCharts() {
-      if (this.$refs.priceTrendChart) {
-        this.priceTrendChart = echarts.init(this.$refs.priceTrendChart, 'macarons')
-        }
-      if (this.$refs.promotionTrendChart) {
-        this.promotionTrendChart = echarts.init(this.$refs.promotionTrendChart, 'macarons')
-      }
-      if (this.$refs.salesTrendChart) {
-        this.salesTrendChart = echarts.init(this.$refs.salesTrendChart, 'macarons')
-      }
-      if (this.$refs.anomalyDetectionChart) {
-        this.anomalyDetectionChart = echarts.init(this.$refs.anomalyDetectionChart, 'macarons')
-      }
-    },
-    /** 渲染所有图表 */
-    renderCharts() {
-      // 1. 平均价格变化趋势
-      this.renderPriceTrend()
-      
-      // 2. 平均促销力度变化趋势
-      this.renderPromotionTrend()
-      
-      // 3. 销量趋势
-      this.renderSalesTrend()
-      
-      // 4. 异常数据检测
-      this.renderAnomalyDetection()
-    },
-    /** 渲染平均价格变化趋势图表 */
-    renderPriceTrend() {
-      const option = {
-        tooltip: {
-          trigger: 'axis',
-          axisPointer: {
-            type: 'cross',
-            label: {
-              backgroundColor: '#6a7985'
-            }
-          }
-        },
-        legend: {
-          data: ['平均价格']
-        },
-        grid: {
-          left: '3%',
-          right: '4%',
-          bottom: '3%',
-          containLabel: true
-        },
-        xAxis: {
-          type: 'category',
-          boundaryGap: false,
-          data: this.timeSeries
-        },
-        yAxis: {
-          type: 'value',
-          name: '价格 (¥)'
-        },
-        series: [
-          {
-            name: '平均价格',
-            type: 'line',
-            stack: 'Total',
-            smooth: true,
-            lineStyle: {
-              width: 3
-            },
-            areaStyle: {
-              opacity: 0.3
-            },
-            data: this.priceSeries,
-            itemStyle: {
-              color: '#10b981'
-            }
+          await this.loadCachedResults()
+          if (Object.keys(this.results || {}).length) {
+            this.$modal.msgSuccess('已加载缓存数据')
+          } else {
+            this.$modal.msgWarning('暂无缓存数据,请先上传销售数据')
           }
-        ]
-      }
-
-      if (this.priceTrendChart) {
-        this.priceTrendChart.setOption(option)
-      }
-    },
-    /** 渲染平均促销力度变化趋势图表 */
-    renderPromotionTrend() {
-      const option = {
-        tooltip: {
-          trigger: 'axis',
-          axisPointer: {
-            type: 'cross',
-            label: {
-              backgroundColor: '#6a7985'
-            }
-          }
-        },
-        legend: {
-          data: ['平均促销力度']
-        },
-        grid: {
-          left: '3%',
-          right: '4%',
-          bottom: '3%',
-          containLabel: true
-        },
-        xAxis: {
-          type: 'category',
-          boundaryGap: false,
-          data: this.timeSeries
-        },
-        yAxis: {
-          type: 'value',
-          name: '促销力度 (%)'
-        },
-        series: [
-          {
-            name: '平均促销力度',
-            type: 'line',
-            stack: 'Total',
-            smooth: true,
-            lineStyle: {
-              width: 3
-            },
-            areaStyle: {
-              opacity: 0.3
-            },
-            data: this.promotionSeries,
-            itemStyle: {
-              color: '#f59e0b'
-            }
-          }
-        ]
-      }
-
-      if (this.promotionTrendChart) {
-        this.promotionTrendChart.setOption(option)
+        }
+      } catch (error) {
+        this.$modal.msgError(error.message || '刷新失败')
+      } finally {
+        this.loading = false
       }
     },
-    /** 渲染销量趋势图表 */
-    renderSalesTrend() {
-      const option = {
-        tooltip: {
-          trigger: 'axis',
-          axisPointer: {
-            type: 'cross',
-            label: {
-              backgroundColor: '#6a7985'
-            }
-          }
-        },
-        legend: {
-          data: ['销量']
-        },
-        grid: {
-          left: '3%',
-          right: '4%',
-          bottom: '3%',
-          containLabel: true
-        },
-        xAxis: {
-          type: 'category',
-          boundaryGap: false,
-          data: this.timeSeries
-        },
-        yAxis: {
-          type: 'value',
-          name: '销量'
-        },
-        series: [
-          {
-            name: '销量',
-            type: 'line',
-            stack: 'Total',
-            smooth: true,
-            lineStyle: {
-              width: 3
-            },
-            areaStyle: {
-              opacity: 0.3
-            },
-            data: this.salesSeries,
-            itemStyle: {
-              color: '#3b82f6'
-            }
-          }
-        ]
-      }
-
-      if (this.salesTrendChart) {
-        this.salesTrendChart.setOption(option)
-      }
+    syncPlatform(row) {
+      this.$modal.msgSuccess(`开始同步 ${row.name}`)
     },
-    /** 渲染异常数据检测图表 */
-    renderAnomalyDetection() {
-      const option = {
-        tooltip: {
-          trigger: 'axis',
-          axisPointer: {
-            type: 'shadow'
-          }
-        },
-        grid: {
-          left: '3%',
-          right: '4%',
-          bottom: '3%',
-          containLabel: true
-        },
-        xAxis: {
-          type: 'category',
-          data: this.timeSeries
-        },
-        yAxis: {
-          type: 'value',
-          name: '异常检测率 (%)'
-        },
-        series: [
-          {
-            name: '异常检测率',
-            type: 'bar',
-            data: this.anomalySeries,
-            itemStyle: {
-              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                {
-                  offset: 0,
-                  color: '#ef4444'
-                },
-                {
-                  offset: 1,
-                  color: '#fca5a5'
-                }
-              ])
-            },
-            label: {
-              show: true,
-              position: 'top',
-              formatter: '{c}%'
-            }
-          }
-        ]
-      }
-
-      if (this.anomalyDetectionChart) {
-        this.anomalyDetectionChart.setOption(option)
-      }
+    editPlatform(row) {
+      this.$modal.msg(`编辑 ${row.name} 配置`)
     },
-    /** 窗口大小变化处理 */
-    handleResize() {
-      if (this.priceTrendChart) {
-        this.priceTrendChart.resize()
-      }
-      if (this.promotionTrendChart) {
-        this.promotionTrendChart.resize()
-      }
-      if (this.salesTrendChart) {
-        this.salesTrendChart.resize()
-      }
-      if (this.anomalyDetectionChart) {
-        this.anomalyDetectionChart.resize()
-      }
+    addSource() {
+      this.$modal.msg('数据源配置入口已打开')
     },
-    /** 工具函数 */
-    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}`
+    saveSettings() {
+      this.settingsVisible = false
+      this.$modal.msgSuccess('设置已保存')
     },
-    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' })
+    exportData() {
+      const blob = new Blob([JSON.stringify(this.results, null, 2)], { type: 'application/json;charset=utf-8' })
       const url = URL.createObjectURL(blob)
-      const a = document.createElement('a')
-      a.href = url
-      a.download = `sale_overview_results_${this.formatUploadDate(new Date())}.json`
-      document.body.appendChild(a)
-      a.click()
-      document.body.removeChild(a)
+      const link = document.createElement('a')
+      link.href = url
+      link.download = `sale_monitor_${Date.now()}.json`
+      link.click()
       URL.revokeObjectURL(url)
-    }
-  },
-  computed: {
-    hasResults() {
-      return Object.keys(this.results || {}).length > 0
     },
-    /** 异常数据列表 */
-    anomalyData() {
-      if (this.results.anomalies && this.results.anomalies.anomalies) {
-        return this.results.anomalies.anomalies.sort((a, b) => b.deviation - a.deviation)
-      }
-      return []
-    }
+    formatMoney(value) { return `¥${Math.round(Number(value || 0)).toLocaleString()}` },
+    formatNumber(value) { return Math.round(Number(value || 0)).toLocaleString() }
   }
 }
 </script>
 
 <style scoped lang="scss">
-.app-container {
-  padding: 20px;
-}
-
-.page-header {
-  margin-bottom: 20px;
-  
-  h2 {
-    font-size: 24px;
-    font-weight: 600;
-    color: #303133;
-    margin-bottom: 8px;
-    
-    i {
-      margin-right: 8px;
-      color: #409EFF;
-    }
-  }
-  
-  .page-desc {
-    color: #909399;
-    font-size: 14px;
-    margin: 0;
-  }
-}
-
-.mb-20 {
-  margin-bottom: 20px;
-}
-
-.stat-card {
-  .stat-content {
-    display: flex;
-    justify-content: space-between;
-    align-items: flex-start;
-    
-    .stat-info {
-      flex: 1;
-      
-      .stat-label {
-        font-size: 12px;
-        color: #909399;
-        margin: 0 0 8px 0;
-        text-transform: uppercase;
-        letter-spacing: 0.5px;
-      }
-      
-      .stat-value {
-        font-size: 28px;
-        font-weight: bold;
-        color: #303133;
-        margin: 0 0 8px 0;
-      }
-      
-      .stat-desc {
-        font-size: 12px;
-        color: #909399;
-        margin: 0;
-        
-        &.stat-desc-success {
-          color: #67C23A;
-        }
-        
-        &.stat-desc-error {
-          color: #F56C6C;
-        }
-      }
-    }
-    
-    .stat-icon {
-      width: 48px;
-      height: 48px;
-      border-radius: 50%;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      font-size: 20px;
-      
-      &.stat-icon-purple {
-        background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%);
-        color: #6366f1;
-      }
-      
-      &.stat-icon-blue {
-        background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
-        color: #3b82f6;
-      }
-      
-      &.stat-icon-teal {
-        background: linear-gradient(135deg, #ccfbf1 0%, #99f6e4 100%);
-        color: #14b8a6;
-      }
-      
-      &.stat-icon-green {
-        background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
-        color: #10b981;
-      }
-      
-      &.stat-icon-yellow {
-        background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
-        color: #f59e0b;
-      }
-      
-      &.stat-icon-red {
-        background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
-        color: #ef4444;
-      }
-    }
-  }
-}
-
-::v-deep .el-card__header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  
-  .header-desc {
-    font-size: 12px;
-    color: #909399;
-    font-weight: normal;
-  }
-}
-
-.view-selector {
-  display: flex;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 10px;
-  
-  .el-radio-group {
-    display: flex;
-    align-items: center;
-  }
-  
-  .el-select {
-    margin-top: 5px;
-  }
-}
-
-@media screen and (max-width: 768px) {
-  .view-selector {
-    flex-direction: column;
-    align-items: flex-start;
-    
-    .el-select {
-      width: 100% !important;
-      margin-left: 0 !important;
-    }
-  }
-}
-
-/* Anomaly styles */
-.anomaly-reason {
-  line-height: 1.4;
-  color: #303133;
-}
-
-.deviation-value {
-  display: block;
-  text-align: center;
-  font-size: 12px;
-  color: #909399;
-  margin-top: 4px;
-}
-
-::v-deep .el-table .cell {
-  padding: 12px 10px;
-}
-
-::v-deep .el-table__row:hover {
-  background-color: #f5f7fa !important;
-}
-
-::v-deep .el-tag {
-  margin-right: 0;
-}
-
-/* Anomaly type tag styles */
-::v-deep .el-tag--warning {
-  background-color: #fff7e6;
-  border-color: #ffd591;
-  color: #fa8c16;
-}
-
-::v-deep .el-tag--danger {
-  background-color: #fff1f0;
-  border-color: #ffccc7;
-  color: #f5222d;
-}
-
-::v-deep .el-tag--info {
-  background-color: #e6f7ff;
-  border-color: #91d5ff;
-  color: #1890ff;
-}
-
-::v-deep .el-tag--success {
-  background-color: #f6ffed;
-  border-color: #b7eb8f;
-  color: #52c41a;
-}
-
-/* Progress bar styles */
-::v-deep .el-progress {
-  margin-bottom: 4px;
-}
-
-::v-deep .el-progress-bar__outer {
-  background-color: #f0f0f0;
-}
-
-::v-deep .el-progress-bar__inner {
-  border-radius: 5px;
-}
-</style>
+.sales-page { padding: 20px; max-width: 1400px; margin: 0 auto; background: #f0f2f5; min-height: calc(100vh - 84px); }
+.header-bar, .panel, .overview-card { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,.06); }
+.header-bar { display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; margin-bottom: 20px; gap: 16px; flex-wrap: wrap; }
+.header-title { margin: 0; font-size: 20px; font-weight: 600; color: #303133; }
+.header-actions { display: flex; gap: 10px; flex-wrap: wrap; }
+.panel { padding: 24px; margin-bottom: 20px; }
+.panel h3 { margin: 0 0 20px; font-size: 16px; color: #303133; }
+.platform-grid { display: grid; grid-template-columns: repeat(5,1fr); gap: 15px; }
+.platform-card { background: #f8f9fa; border-left: 4px solid #909399; border-radius: 8px; padding: 18px; text-align: center; display: flex; flex-direction: column; gap: 8px; }
+.platform-card.online { border-left-color: #67c23a; }
+.platform-card.syncing { border-left-color: #e6a23c; }
+.platform-card.error { border-left-color: #f56c6c; }
+.platform-card i { font-size: 24px; }
+.platform-card span, .platform-card em { color: #909399; font-size: 12px; font-style: normal; }
+.log-section { margin-top: 22px; padding-top: 18px; border-top: 1px solid #f0f0f0; }
+.log-header { display: flex; justify-content: space-between; color: #606266; margin-bottom: 12px; }
+.log-item { background: #f8f9fa; border-radius: 8px; padding: 12px 15px; margin-bottom: 8px; cursor: pointer; }
+.log-item.expanded { background: #e8f4f8; border-left: 3px solid #17a2b8; }
+.log-line { display: flex; align-items: center; gap: 10px; }
+.log-line span { flex: 1; }
+.log-line time { color: #909399; font-size: 12px; }
+.log-item p { margin: 10px 0 0 26px; color: #606266; font-size: 12px; }
+.overview-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
+.overview-card { padding: 24px; display: flex; justify-content: space-between; align-items: center; }
+.overview-card span { color: #909399; font-size: 13px; }
+.overview-card strong { display: block; font-size: 34px; margin: 8px 0; }
+.overview-card em { font-style: normal; }
+.overview-card > i { width: 60px; height: 60px; border-radius: 15px; display: flex; align-items: center; justify-content: center; font-size: 28px; }
+.overview-card .sales { background: rgba(102,126,234,.12); color: #667eea; }
+.overview-card .orders { background: rgba(103,194,58,.12); color: #67c23a; }
+.up { color: #67c23a; }
+.panel-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
+.panel-head h3 { margin: 0; }
+.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; background: #909399; }
+.status-dot.online { background: #67c23a; }
+.status-dot.syncing { background: #e6a23c; }
+.status-dot.error { background: #f56c6c; }
+@media (max-width: 1200px) { .platform-grid { grid-template-columns: repeat(3,1fr); } }
+@media (max-width: 800px) { .platform-grid, .overview-grid { grid-template-columns: 1fr; } }
+</style>

+ 330 - 1033
src/views/sale/trendPred/index.vue

@@ -1,1114 +1,411 @@
 <template>
-  <div class="app-container">
-    <!-- 页面标题 -->
-    <div class="page-header">
-      <h2><i class="el-icon-data-line"></i> 销量趋势预测</h2>
-      <p class="page-desc">基于历史销量数据,预测未来一段时间的销量趋势,包含详细的预测指标和模型评估</p>
-    </div>
-
-    <div class="upload-toolbar">
-      <div class="toolbar-left">
+  <div class="sales-page">
+    <section class="header-bar">
+      <h1 class="header-title">趋势预测</h1>
+      <div class="header-actions">
         <el-upload
-          ref="toolbarUpload"
-          class="toolbar-upload"
-          :limit="1"
+          ref="salesUpload"
+          action=""
+          :auto-upload="false"
+          :show-file-list="false"
           accept=".xlsx,.xls,.csv"
-          :http-request="customUpload"
-          :disabled="upload.isUploading"
-          :on-change="handleFileChange"
-          :on-success="handleUploadSuccess"
-          :on-error="handleUploadError"
-          :before-upload="beforeUpload"
+          :on-change="handleSalesFileChange"
+        >
+          <el-button size="small" plain icon="el-icon-upload2">销售数据</el-button>
+        </el-upload>
+        <el-upload
+          ref="productUpload"
+          action=""
           :auto-upload="false"
           :show-file-list="false"
+          accept=".xlsx,.xls"
+          :on-change="handleProductFileChange"
         >
-          <el-button plain>上传文件</el-button>
+          <el-button size="small" plain icon="el-icon-folder-add">产品资料</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>
-
-    <!-- 预测设置 + 基础信息板块 -->
-    <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">预测类型:</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"
-                  v-model="predictType">
-            <option value="sku">按SKU预测</option>
-            <option value="category">按类别预测</option>
-          </select>
-        </div>
-        <div class="flex items-center gap-3">
-          <label class="text-sm font-medium text-gray-700">{{ predictType === 'sku' ? '选择SKU:' : '选择类别:' }}</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"
-                  v-model="selectedItem">
-            <option v-for="item in selectOptions" :key="item" :value="item">{{ item }}</option>
-          </select>
-        </div>
-        <div class="flex items-center gap-3">
-          <label class="text-sm font-medium text-gray-700">预测周期:</label>
-          <el-radio-group v-model="predictionPeriod" size="small">
-            <el-radio-button label="7">7天</el-radio-button>
-            <el-radio-button label="14">14天</el-radio-button>
-            <el-radio-button label="30">30天</el-radio-button>
-          </el-radio-group>
-        </div>
+        <el-button size="small" type="primary" :loading="loading" icon="el-icon-caret-right" @click="runPrediction">开始预测</el-button>
+        <el-button size="small" icon="el-icon-download" :disabled="!hasPrediction" @click="exportResults">导出</el-button>
       </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">{{ predictType === 'sku' ? 'SKU编码' : '类别名称' }}</p>
-          <p class="text-lg font-medium text-gray-800 truncate">{{ selectedItem }}</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">{{ totalHistoricalSales }}</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">{{ averageDailySales }}</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="predictionAccuracyClass">{{ predictionAccuracy }}</p>
-        </div>
+    </section>
+
+    <section class="filter-bar">
+      <div class="filter-group">
+        <label>预测类型</label>
+        <el-select v-model="predictType" size="small">
+          <el-option label="按 SKU" value="sku" />
+          <el-option label="按品类" value="category" />
+        </el-select>
       </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="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-green-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-purple-500"></span>
-            <span class="text-sm text-gray-600">趋势线</span>
-          </div>
-        </div>
+      <div class="filter-group wide">
+        <label>{{ predictType === 'sku' ? 'SKU' : '品类' }}</label>
+        <el-select v-model="selectedItem" size="small" filterable placeholder="请选择">
+          <el-option v-for="item in selectOptions" :key="item" :label="item" :value="item" />
+        </el-select>
       </div>
-      <div class="h-96">
-        <canvas ref="salesTrendRef"></canvas>
+      <div class="filter-group">
+        <label>预测周期</label>
+        <el-select v-model.number="predictionPeriod" size="small">
+          <el-option label="未来 7 天" :value="7" />
+          <el-option label="未来 15 天" :value="15" />
+          <el-option label="未来 30 天" :value="30" />
+          <el-option label="未来 60 天" :value="60" />
+        </el-select>
       </div>
-      <div class="mt-4 space-y-2">
-        <div v-if="hasResults" class="text-sm">
-          <span class="text-green-700 font-medium">
-            <i class="fa fa-check-circle mr-1"></i>
-            预测完成:基于历史数据预测未来{{ predictionPeriod }}天的销量趋势
-          </span>
-        </div>
-        <div v-else class="text-sm text-gray-500">
-          <i class="fa fa-info-circle mr-1"></i>
-          请上传历史销量数据并点击"开始预测"按钮生成预测结果
-        </div>
+      <div class="file-hint">
+        <span v-if="salesFileName">销售数据:{{ salesFileName }}</span>
+        <span v-if="productFileName">产品资料:{{ productFileName }}</span>
+        <span v-if="!salesFileName && !productFileName">请先上传历史销售数据</span>
       </div>
-    </div>
+    </section>
 
-    <!-- 预测指标与详情分组 -->
-    <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">{{ predictedTotalSales }}</span>
-          </div>
-          <div class="progress-bar"><div class="progress-value bg-blue-500" :style="{ width: predictedTotalSalesPct+'%' }"></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">{{ predictedAverageDailySales }}</span>
-          </div>
-          <div class="progress-bar"><div class="progress-value bg-green-500" :style="{ width: predictedAverageDailySalesPct+'%' }"></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">{{ predictedMaxSales }}</p>
-            <p class="text-xs text-gray-400 mt-1">{{ predictedMaxSalesDate }}</p>
+    <el-progress v-if="loading" :percentage="progress" :stroke-width="8" class="progress" />
+
+    <section class="chart-panel">
+      <div class="chart-header">
+        <div class="chart-summary">
+          <div>
+            <span class="summary-label">预测总量</span>
+            <strong>{{ formatNumber(predictedTotal) }}</strong>
           </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">{{ predictedMinSales }}</p>
-            <p class="text-xs text-gray-400 mt-1">{{ predictedMinSalesDate }}</p>
+          <div>
+            <span class="summary-label">环比变化</span>
+            <strong :class="growthRate >= 0 ? 'up' : 'down'">{{ growthRateText }}</strong>
           </div>
         </div>
-        
-        <!-- a+x+y模型组件 -->
-        <div v-if="axyComponents" class="pt-4 border-t border-gray-200">
-          <h4 class="text-md font-medium text-gray-700 mb-3">a+x+y模型组件</h4>
-          <div class="space-y-3">
-            <div class="flex justify-between">
-              <span class="text-sm text-gray-500">基础值 (a)</span>
-              <span class="text-sm font-medium text-gray-800">{{ axyComponents.base_value.toFixed(2) }}</span>
-            </div>
-            <div class="flex justify-between">
-              <span class="text-sm text-gray-500">趋势因子 (x)</span>
-              <span class="text-sm font-medium" :class="axyComponents.trend_factor >= 0 ? 'text-green-600' : 'text-red-600'">
-                {{ axyComponents.trend_factor >= 0 ? '+' : '' }}{{ (axyComponents.trend_factor * 100).toFixed(2) }}%
-              </span>
-            </div>
-            <div v-if="axyComponents.seasonal_factors && axyComponents.seasonal_factors.length > 0" class="pt-2">
-              <p class="text-xs text-gray-500 uppercase tracking-wide mb-2">季节性因子 (y)</p>
-              <div class="grid grid-cols-7 gap-2">
-                <div v-for="(factor, index) in axyComponents.seasonal_factors" :key="index" class="text-center">
-                  <span class="text-xs text-gray-400">第{{ index+1 }}天</span>
-                  <p class="text-sm font-medium" :class="factor >= 0 ? 'text-green-600' : 'text-red-600'">
-                    {{ factor >= 0 ? '+' : '' }}{{ factor.toFixed(1) }}
-                  </p>
-                </div>
-              </div>
-            </div>
-          </div>
+        <div class="legend">
+          <span><i class="solid blue"></i>历史销量</span>
+          <span><i class="dashed orange"></i>预测销量</span>
+          <span><i class="box gray"></i>置信区间</span>
         </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>
-            </tr>
-            </thead>
-            <tbody class="bg-white divide-y divide-gray-200">
-            <tr v-for="(item, index) in predictionDetails" :key="index">
-              <td class="px-4 py-3 text-sm">{{ item.date }}</td>
-              <td class="px-4 py-3 text-sm font-medium text-gray-800">{{ item.historicalSales }}</td>
-              <td class="px-4 py-3 text-sm font-medium text-green-700">{{ item.predictedSales }}</td>
-              <td class="px-4 py-3 text-sm" :class="item.deviationRate >= 0 ? 'text-red-600' : 'text-green-600'">
-                {{ item.deviationRate >= 0 ? '+' : '' }}{{ item.deviationRate }}%
-              </td>
-              <td class="px-4 py-3 text-sm">
-                <span :class="getTrendClass(item.trend)">{{ item.trend }}</span>
-              </td>
-              <td class="px-4 py-3 text-sm">
-                <span class="text-sm font-medium" :class="getConfidenceClass(item.confidence)">{{ (item.confidence * 100).toFixed(1) }}%</span>
-              </td>
-            </tr>
-            </tbody>
-          </table>
+      <div ref="trendChart" class="chart"></div>
+    </section>
+
+    <section class="data-grid">
+      <div class="panel">
+        <div class="panel-head">
+          <h3>预测数值表</h3>
+          <el-input v-model="dateKeyword" size="small" prefix-icon="el-icon-search" placeholder="搜索日期" />
         </div>
+        <el-table :data="filteredPredictionRows" size="small" height="330">
+          <el-table-column prop="date" label="日期" min-width="120" />
+          <el-table-column prop="quantity" label="预测销量" min-width="120">
+            <template slot-scope="{ row }">{{ formatNumber(row.quantity) }}</template>
+          </el-table-column>
+          <el-table-column prop="lower" label="置信区间下限" min-width="130">
+            <template slot-scope="{ row }">{{ formatNumber(row.lower) }}</template>
+          </el-table-column>
+          <el-table-column prop="upper" label="置信区间上限" min-width="130">
+            <template slot-scope="{ row }">{{ formatNumber(row.upper) }}</template>
+          </el-table-column>
+        </el-table>
       </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="modelAccuracyClass">
-          模型准确率 {{ modelAccuracy }}%
-          <span class="ml-2">{{ modelAccuracyLevel }}</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">平均绝对百分比误差 (MAPE)</p>
-          <p class="text-lg font-medium text-gray-800">{{ mape }}</p>
-          <p class="text-xs text-gray-400 mt-1">{{ mapeLevel }}</p>
+      <div class="panel metric-panel">
+        <h3>关键指标</h3>
+        <div class="metric-card">
+          <span>预测期总销量</span>
+          <strong>{{ formatNumber(predictedTotal) }}</strong>
         </div>
-        <div class="p-4 border border-gray-200 rounded-lg bg-gray-50">
-          <p class="text-xs text-gray-500 uppercase tracking-wide">均方根误差 (RMSE)</p>
-          <p class="text-lg font-medium text-gray-800">{{ rmse }}</p>
+        <div class="metric-card orange">
+          <span>预测期日均销量</span>
+          <strong>{{ formatNumber(predictedAverage) }}</strong>
         </div>
-        <div class="p-4 border border-gray-200 rounded-lg bg-gray-50">
-          <p class="text-xs text-gray-500 uppercase tracking-wide">平均绝对误差 (MAE)</p>
-          <p class="text-lg font-medium text-gray-800">{{ mae }}</p>
+        <div class="metric-card green">
+          <span>预测置信度</span>
+          <strong>{{ effectiveConfidence }}%</strong>
+          <el-progress :percentage="effectiveConfidence" :show-text="false" color="#67c23a" />
         </div>
-        <div class="p-4 border border-gray-200 rounded-lg bg-gray-50">
-          <p class="text-xs text-gray-500 uppercase tracking-wide">R² 决定系数</p>
-          <p class="text-lg font-medium text-gray-800">{{ rSquared }}</p>
+        <div class="metric-card blue">
+          <span>高增长期</span>
+          <strong>{{ highGrowthPeriod }}</strong>
         </div>
       </div>
-    </div>
+    </section>
   </div>
 </template>
 
 <script>
-import { ref, computed, onMounted, beforeUnmount, watch } from 'vue'
-import { analyzeSaleTrendWithFile, getSaleTrendResults, predictSalesTrend } from '@/api/client'
-import { getToken } from '@/utils/auth'
-import { Chart } from 'chart.js'
-import { formatDate } from '../../../utils/format'
+import * as echarts from 'echarts'
+import {
+  analyzeSaleTrendWithFile,
+  getSaleTrendResults,
+  predictSalesTrend,
+  uploadProductFile,
+  getUploadProgress
+} from '@/api/client'
 
 export default {
   name: 'SalesTrendPrediction',
   data() {
     return {
-      salesTrendChart: null,
-      upload: {
-        isUploading: false,
-        fileName: '',
-        pendingFileName: '',
-        ignoreFileChange: false
-      },
-      predictionPeriod: '7',
+      chart: null,
+      loading: false,
+      progress: 0,
+      progressTimer: null,
+      salesFile: null,
+      productFile: null,
+      salesFileName: '',
+      productFileName: '',
+      productUploaded: false,
       predictType: 'sku',
       selectedItem: '',
-      results: {}
+      predictionPeriod: 7,
+      analysisResults: {},
+      predictionResults: {},
+      dateKeyword: '',
+      confidence: 92
     }
   },
   computed: {
-    hasResults() {
-      return Object.keys(this.results || {}).length > 0
-    },
     selectOptions() {
+      return this.predictType === 'sku'
+        ? (this.analysisResults.sku_list || [])
+        : (this.analysisResults.category_list || [])
+    },
+    historicalDetail() {
+      if (!this.selectedItem) return {}
+      return this.predictType === 'sku'
+        ? ((this.analysisResults.data || {})[this.selectedItem] || {})
+        : ((this.analysisResults.categories || {})[this.selectedItem] || {})
+    },
+    predictionDetail() {
+      if (!this.selectedItem) return {}
       if (this.predictType === 'sku') {
-        return this.results.sku_list || []
-      } else {
-        return this.results.category_list || []
-      }
-    },
-    detail() {
-      if (!this.results || !this.selectedItem) return null
-      
-      if (this.predictType === 'sku') {
-        return this.results.data && this.results.data[this.selectedItem] || null
-      } else {
-        return this.results.categories && this.results.categories[this.selectedItem] || null
+        return ((this.predictionResults.sku_predictions || {})[this.selectedItem] || this.predictionResults.overall_prediction || {})
       }
-    },
-    totalHistoricalSales() {
-      const detail = this.detail || {}
-      return detail.total_quantity || 0
-    },
-    averageDailySales() {
-      const detail = this.detail || {}
-      const totalQuantity = detail.total_quantity || 0
-      const dateSeries = detail.date_series || []
-      return dateSeries.length > 0 ? Math.round(totalQuantity / dateSeries.length) : 0
-    },
-    predictionAccuracy() {
-      const detail = this.detail || {}
-      return detail.prediction_accuracy || '85%'
-    },
-    predictionAccuracyClass() {
-      const accuracy = parseFloat(this.predictionAccuracy) || 0
-      if (accuracy >= 80) return 'text-green-700'
-      if (accuracy >= 60) return 'text-yellow-600'
-      return 'text-red-600'
-    },
-    predictedTotalSales() {
-      const detail = this.detail || {}
-      return detail.predicted_total_sales || 0
-    },
-    predictedTotalSalesPct() {
-      const total = this.totalHistoricalSales || 1
-      const predicted = this.predictedTotalSales || 0
-      return Math.min(100, (predicted / total) * 100)
-    },
-    predictedAverageDailySales() {
-      const detail = this.detail || {}
-      return detail.predicted_average_daily_sales || 0
-    },
-    predictedAverageDailySalesPct() {
-      const avg = this.averageDailySales || 1
-      const predictedAvg = this.predictedAverageDailySales || 0
-      return Math.min(100, (predictedAvg / avg) * 100)
-    },
-    predictedMaxSales() {
-      const detail = this.detail || {}
-      return detail.predicted_max_sales || 0
-    },
-    predictedMaxSalesDate() {
-      const detail = this.detail || {}
-      return detail.predicted_max_sales_date ? formatDate(detail.predicted_max_sales_date) : '-'
-    },
-    predictedMinSales() {
-      const detail = this.detail || {}
-      return detail.predicted_min_sales || 0
-    },
-    predictedMinSalesDate() {
-      const detail = this.detail || {}
-      return detail.predicted_min_sales_date ? formatDate(detail.predicted_min_sales_date) : '-'
-    },
-    predictionDetails() {
-      const detail = this.detail || {}
-      const historicalSales = detail.historical_sales || []
-      const predictedSales = detail.predicted_sales || []
-      const dateSeries = detail.date_series || []
-      
-      // 生成预测详情
-      const details = []
-      for (let i = 0; i < predictedSales.length; i++) {
-        const date = dateSeries[i] || `预测第${i+1}天`
-        const historical = historicalSales[i] || 0
-        const predicted = predictedSales[i] || 0
-        const deviationRate = historical > 0 ? ((predicted - historical) / historical) * 100 : 0
-        
-        let trend = '稳定'
-        if (i > 0 && predictedSales[i] > predictedSales[i-1]) {
-          trend = '上升'
-        } else if (i > 0 && predictedSales[i] < predictedSales[i-1]) {
-          trend = '下降'
+      return ((this.predictionResults.category_predictions || {})[this.selectedItem] || this.predictionResults.overall_prediction || {})
+    },
+    hasPrediction() {
+      return (this.predictionDetail.quantity_series || []).length > 0
+    },
+    predictedTotal() {
+      return (this.predictionDetail.quantity_series || []).reduce((sum, value) => sum + Number(value || 0), 0)
+    },
+    predictedAverage() {
+      const series = this.predictionDetail.quantity_series || []
+      return series.length ? this.predictedTotal / series.length : 0
+    },
+    historicalAverage() {
+      const series = this.historicalDetail.quantity_series || []
+      return series.length ? series.reduce((sum, value) => sum + Number(value || 0), 0) / series.length : 0
+    },
+    growthRate() {
+      if (!this.historicalAverage) return 0
+      return ((this.predictedAverage - this.historicalAverage) / this.historicalAverage) * 100
+    },
+    growthRateText() {
+      const sign = this.growthRate >= 0 ? '+' : ''
+      return `${sign}${this.growthRate.toFixed(1)}%`
+    },
+    predictionRows() {
+      const dates = this.predictionDetail.date_series || []
+      const values = this.predictionDetail.quantity_series || []
+      return dates.map((date, index) => {
+        const quantity = Number(values[index] || 0)
+        const band = Math.max(quantity * this.confidenceBandRate, 1)
+        return {
+          date,
+          quantity,
+          lower: Math.max(0, quantity - band),
+          upper: quantity + band
         }
-        
-        details.push({
-          date: date,
-          historicalSales: historical,
-          predictedSales: predicted,
-          deviationRate: Math.round(deviationRate * 100) / 100,
-          trend: trend,
-          confidence: 0.85 // 模拟值
-        })
-      }
-      
-      return details
-    },
-    axyComponents() {
-      const detail = this.detail || {}
-      return detail.axymodel_components || null
-    },
-    mape() {
-      const detail = this.detail || {}
-      return detail.mape || '12%'
-    },
-    mapeLevel() {
-      const mapeValue = parseFloat(this.mape) || 0
-      if (mapeValue < 10) return '优秀'
-      if (mapeValue < 20) return '良好'
-      if (mapeValue < 30) return '一般'
-      return '需改进'
-    },
-    rmse() {
-      const detail = this.detail || {}
-      return detail.rmse || 150
-    },
-    mae() {
-      const detail = this.detail || {}
-      return detail.mae || 120
+      })
     },
-    rSquared() {
-      const detail = this.detail || {}
-      return detail.r_squared || 0.85
+    filteredPredictionRows() {
+      if (!this.dateKeyword) return this.predictionRows
+      return this.predictionRows.filter(row => row.date.includes(this.dateKeyword))
     },
-    modelAccuracy() {
-      const detail = this.detail || {}
-      return detail.model_accuracy || 85
+    highGrowthPeriod() {
+      if (!this.predictionRows.length) return '-'
+      const sorted = [...this.predictionRows].sort((a, b) => b.quantity - a.quantity)
+      const first = sorted[0]
+      return first ? first.date : '-'
     },
-    modelAccuracyClass() {
-      const accuracy = parseFloat(this.modelAccuracy) || 0
-      if (accuracy >= 80) return 'text-green-600'
-      if (accuracy >= 60) return 'text-yellow-600'
-      return 'text-red-600'
+    effectiveConfidence() {
+      const detailParams = this.predictionDetail.model_params || {}
+      const globalParams = this.predictionResults.control_params || {}
+      return Math.round(Number(detailParams.confidence || globalParams.confidence || this.confidence))
     },
-    modelAccuracyLevel() {
-      const accuracy = parseFloat(this.modelAccuracy) || 0
-      if (accuracy >= 80) return '(优秀)'
-      if (accuracy >= 60) return '(良好)'
-      return '(需改进)'
+    confidenceBandRate() {
+      return Math.max(0.04, Math.min(0.25, (100 - this.effectiveConfidence) / 100 + 0.04))
     }
   },
-  mounted() {
-    this.getList()
-  },
-  beforeUnmount() {
-    if (this.salesTrendChart) this.salesTrendChart.destroy()
-  },
   watch: {
-    detail() {
-      this.renderSalesTrend()
+    selectedItem() {
+      this.$nextTick(this.renderChart)
     },
-    results() {
-      if (!this.results || !this.selectedItem) {
-        const first = this.selectOptions[0] || ''
-        if (first) this.selectedItem = first
-      }
+    predictType() {
+      this.selectedItem = this.selectOptions[0] || ''
+      if (this.selectedItem && this.analysisResults.summary) this.runPrediction(false)
     },
     predictionPeriod() {
-      if (this.selectedItem) {
-        this.predictSales()
-      }
-    },
-    predictType() {
-      if (this.results) {
-        const first = this.selectOptions[0] || ''
-        if (first) this.selectedItem = first
-        if (this.selectedItem) {
-          this.predictSales()
-        }
-      }
+      if (this.selectedItem && this.analysisResults.summary) this.runPrediction(false)
     }
   },
+  mounted() {
+    this.chart = echarts.init(this.$refs.trendChart)
+    window.addEventListener('resize', this.resizeChart)
+    this.loadCachedResults()
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.resizeChart)
+    this.stopProgress()
+    if (this.chart) this.chart.dispose()
+  },
   methods: {
-    /** 获取销售分析结果 */
-    getList() {
-      console.log('Getting sales trend results...')
-      getSaleTrendResults().then(response => {
-        console.log('Get results response:', response)
-        if (response && response.success && response.data) {
-          const results = response.data || {}
-          console.log('Sales trend results:', results)
-          this.results = results
-          const firstItem = this.selectOptions[0] || ''
-          console.log('First item:', firstItem)
-          if (firstItem) {
-            this.selectedItem = firstItem
-            console.log('Selected first item:', firstItem)
-          }
-          this.$nextTick(() => {
-            console.log('Rendering sales trend after getting results...')
-            this.renderSalesTrend()
-          })
-        }
-      }).catch(error => {
-        console.error('Error getting sales trend results:', error)
-        this.results = {}
-      })
+    handleSalesFileChange(file) {
+      this.salesFile = file.raw
+      this.salesFileName = file.name
     },
-    /** 刷新数据 */
-    refreshData() {
-      this.getList()
-      this.$modal.msgSuccess('数据刷新成功')
+    handleProductFileChange(file) {
+      this.productFile = file.raw
+      this.productFileName = file.name
+      this.productUploaded = false
     },
-    /** 预测销量趋势 */
-    predictSales() {
-      if (!this.selectedItem) return
-      
-      const params = {
-        predict_days: parseInt(this.predictionPeriod)
-      }
-      
-      predictSalesTrend(params).then(response => {
-        console.log('Predict response:', response)
-        if (response && response.success && response.data) {
-          const results = response.data || {}
-          console.log('Prediction results:', results)
-          
-          // 更新results数据以匹配前端期望的结构
-          if (this.predictType === 'sku') {
-            if (!this.results.data) {
-              this.results.data = {}
-            }
-            
-            // 获取SKU的预测数据
-            const skuPrediction = results.sku_predictions && results.sku_predictions[this.selectedItem]
-            const overallPrediction = results.overall_prediction
-            
-            this.results.data[this.selectedItem] = {
-              historical_sales: this.detail && this.detail.quantity_series ? this.detail.quantity_series : [],
-              predicted_sales: skuPrediction ? skuPrediction.quantity_series : (overallPrediction ? overallPrediction.quantity_series : []),
-              date_series: this.detail && this.detail.date_series ? this.detail.date_series : [],
-              predicted_total_sales: skuPrediction ? skuPrediction.quantity_series.reduce((a, b) => a + b, 0) : (overallPrediction ? overallPrediction.quantity_series.reduce((a, b) => a + b, 0) : 0),
-              predicted_average_daily_sales: skuPrediction ? skuPrediction.quantity_series.reduce((a, b) => a + b, 0) / parseInt(this.predictionPeriod) : (overallPrediction ? overallPrediction.quantity_series.reduce((a, b) => a + b, 0) / parseInt(this.predictionPeriod) : 0),
-              predicted_max_sales: skuPrediction ? Math.max(...skuPrediction.quantity_series) : (overallPrediction ? Math.max(...overallPrediction.quantity_series) : 0),
-              predicted_min_sales: skuPrediction ? Math.min(...skuPrediction.quantity_series) : (overallPrediction ? Math.min(...overallPrediction.quantity_series) : 0),
-              predicted_max_sales_date: skuPrediction && skuPrediction.quantity_series.length > 0 ? skuPrediction.date_series[skuPrediction.quantity_series.indexOf(Math.max(...skuPrediction.quantity_series))] : (overallPrediction && overallPrediction.quantity_series.length > 0 ? overallPrediction.date_series[overallPrediction.quantity_series.indexOf(Math.max(...overallPrediction.quantity_series))] : null),
-              predicted_min_sales_date: skuPrediction && skuPrediction.quantity_series.length > 0 ? skuPrediction.date_series[skuPrediction.quantity_series.indexOf(Math.min(...skuPrediction.quantity_series))] : (overallPrediction && overallPrediction.quantity_series.length > 0 ? overallPrediction.date_series[overallPrediction.quantity_series.indexOf(Math.min(...overallPrediction.quantity_series))] : null),
-              prediction_accuracy: '85%', // 模拟值,实际应从模型评估中获取
-              mape: '12%', // 模拟值
-              rmse: 150, // 模拟值
-              mae: 120, // 模拟值
-              r_squared: 0.85, // 模拟值
-              model_accuracy: 85, // 模拟值
-              axymodel_components: skuPrediction ? {
-                base_value: skuPrediction.model_params.a,
-                trend_factor: skuPrediction.model_params.x,
-                seasonal_factors: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7] // 模拟值
-              } : (overallPrediction ? {
-                base_value: overallPrediction.model_params.a,
-                trend_factor: overallPrediction.model_params.x,
-                seasonal_factors: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7] // 模拟值
-              } : {})
-            }
-          } else {
-            if (!this.results.categories) {
-              this.results.categories = {}
-            }
-            
-            // 获取品类的预测数据
-            const categoryPrediction = results.category_predictions && results.category_predictions[this.selectedItem]
-            const overallPrediction = results.overall_prediction
-            
-            this.results.categories[this.selectedItem] = {
-              historical_sales: this.detail && this.detail.quantity_series ? this.detail.quantity_series : [],
-              predicted_sales: categoryPrediction ? categoryPrediction.quantity_series : (overallPrediction ? overallPrediction.quantity_series : []),
-              date_series: this.detail && this.detail.date_series ? this.detail.date_series : [],
-              predicted_total_sales: categoryPrediction ? categoryPrediction.quantity_series.reduce((a, b) => a + b, 0) : (overallPrediction ? overallPrediction.quantity_series.reduce((a, b) => a + b, 0) : 0),
-              predicted_average_daily_sales: categoryPrediction ? categoryPrediction.quantity_series.reduce((a, b) => a + b, 0) / parseInt(this.predictionPeriod) : (overallPrediction ? overallPrediction.quantity_series.reduce((a, b) => a + b, 0) / parseInt(this.predictionPeriod) : 0),
-              predicted_max_sales: categoryPrediction ? Math.max(...categoryPrediction.quantity_series) : (overallPrediction ? Math.max(...overallPrediction.quantity_series) : 0),
-              predicted_min_sales: categoryPrediction ? Math.min(...categoryPrediction.quantity_series) : (overallPrediction ? Math.min(...overallPrediction.quantity_series) : 0),
-              predicted_max_sales_date: categoryPrediction && categoryPrediction.quantity_series.length > 0 ? categoryPrediction.date_series[categoryPrediction.quantity_series.indexOf(Math.max(...categoryPrediction.quantity_series))] : (overallPrediction && overallPrediction.quantity_series.length > 0 ? overallPrediction.date_series[overallPrediction.quantity_series.indexOf(Math.max(...overallPrediction.quantity_series))] : null),
-              predicted_min_sales_date: categoryPrediction && categoryPrediction.quantity_series.length > 0 ? categoryPrediction.date_series[categoryPrediction.quantity_series.indexOf(Math.min(...categoryPrediction.quantity_series))] : (overallPrediction && overallPrediction.quantity_series.length > 0 ? overallPrediction.date_series[overallPrediction.quantity_series.indexOf(Math.min(...overallPrediction.quantity_series))] : null),
-              prediction_accuracy: '85%', // 模拟值
-              mape: '12%', // 模拟值
-              rmse: 150, // 模拟值
-              mae: 120, // 模拟值
-              r_squared: 0.85, // 模拟值
-              model_accuracy: 85, // 模拟值
-              axymodel_components: categoryPrediction ? {
-                base_value: categoryPrediction.model_params.a,
-                trend_factor: categoryPrediction.model_params.x,
-                seasonal_factors: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7] // 模拟值
-              } : (overallPrediction ? {
-                base_value: overallPrediction.model_params.a,
-                trend_factor: overallPrediction.model_params.x,
-                seasonal_factors: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7] // 模拟值
-              } : {})
-            }
-          }
-          
-          this.$nextTick(() => {
-            this.renderSalesTrend()
-          })
-          this.$modal.msgSuccess(`预测成功!预测了未来${this.predictionPeriod}天的销量趋势`)
-        } else if (response && !response.success) {
-          this.$modal.msgError(response.message || '预测失败,请重试')
-        } else {
-          this.$modal.msgError('预测失败,请重试')
-        }
-      }).catch(error => {
-        console.error('预测失败:', error)
-        this.$modal.msgError('预测失败,请检查服务是否正常运行')
-      })
-    },
-    /** 文件选择改变处理 */
-    handleFileChange(file, fileList) {
-      if (this.upload.ignoreFileChange) return
-      console.log('handleFileChange called')
-      console.log('file:', file)
-      console.log('fileList:', fileList)
-      if (!fileList || fileList.length === 0) return
-      if (!file || !file.raw) return
-
-      this.upload.pendingFileName = file.name
-      this.upload.fileName = ''
-      console.log('pendingFileName set to:', this.upload.pendingFileName)
-    },
-    handleUploadSuccess(response, file, fileList) {
-      console.log('handleUploadSuccess called')
-      console.log('response:', response)
-      console.log('file:', file)
-    },
-    handleUploadError(error, file, fileList) {
-      console.error('handleUploadError called')
-      console.error('error:', error)
-      console.error('file:', file)
-    },
-    /** 文件上传前的校验 */
-    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
-    },
-    customUpload(options) {
-      console.log('customUpload called')
-      console.log('options:', options)
-      console.log('options.file:', options.file)
-      if (!options.file) {
-        console.error('No file in options')
-        this.$modal.msgError('请选择要上传的文件')
-        options.onError(new Error('No file to upload'))
-        return
-      }
-      console.log('options.file.raw:', options.file.raw)
-      const file = options.file.raw || options.file
-      console.log('file to upload:', file)
-      if (!file) {
-        console.error('No file to upload')
-        this.$modal.msgError('请选择要上传的文件')
-        options.onError(new Error('No file to upload'))
-        return
-      }
-      console.log('Starting file upload and analysis...')
-      this.upload.isUploading = true
-      analyzeSaleTrendWithFile(file).then(response => {
-        console.log('analyzeSaleTrendWithFile response:', response)
-        this.upload.isUploading = false
+    async loadCachedResults() {
+      try {
+        const response = await getSaleTrendResults()
         if (response && response.success) {
-          const results = response.data || {}
-          console.log('Upload analysis results:', results)
-          this.results = results
-          const firstItem = this.selectOptions[0] || ''
-          if (firstItem) {
-            this.selectedItem = firstItem
-            console.log('Selected first item:', firstItem)
-            console.log('Results data structure:', this.results)
-          }
-          this.$modal.msgSuccess('文件上传并分析成功')
-          this.upload.fileName = this.upload.pendingFileName || file.name
-          this.upload.pendingFileName = ''
-          this.$nextTick(() => {
-            console.log('Rendering sales trend...')
-            this.renderSalesTrend()
-          })
-          options.onSuccess(response)
-        } else {
-          console.error('Upload failed with response:', response)
-          this.$modal.msgError(response.message || '分析失败')
-          options.onError(new Error(response.message || '分析失败'))
-        }
-      }).catch(error => {
-        console.error('analyzeSaleTrendWithFile error:', 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
-          })
+          this.analysisResults = response.data || {}
+          this.selectedItem = this.selectOptions[0] || ''
+          if (this.selectedItem) await this.runPrediction(false)
         }
-      })
-    },
-    submitUpload() {
-      console.log('submitUpload called')
-      const target = this.$refs.toolbarUpload
-      console.log('target:', target)
-      if (!target) {
-        console.error('No upload component reference')
-        this.$modal.msgError('上传组件未初始化')
-        return
-      }
-      const fileList = target.uploadFiles || []
-      console.log('fileList:', fileList)
-      if (!fileList || fileList.length === 0) {
-        console.error('No files selected')
-        this.$modal.msgError('请选择要上传的文件')
-        return
+      } catch (e) {
+        this.analysisResults = {}
       }
-      console.log('Calling target.submit()')
-      target.submit()
     },
-    /** 渲染销量趋势与预测图 */
-    renderSalesTrend() {
-      console.log('Rendering sales trend...')
-      const canvas = this.$refs.salesTrendRef
-      if (!canvas) {
-        console.error('No canvas reference found')
+    async runPrediction(requireFile = true) {
+      if (requireFile && !this.salesFile && !this.analysisResults.summary) {
+        this.$modal.msgError('请先选择销售数据文件')
         return
       }
-      
-      console.log('Detail data:', this.detail)
-      const detail = this.detail || {}
-      const historicalSales = detail.quantity_series || detail.historical_sales || []
-      const predictedSales = detail.predicted_sales || []
-      const dates = detail.date_series || []
-      
-      console.log('Historical sales:', historicalSales)
-      console.log('Predicted sales:', predictedSales)
-      console.log('Dates:', dates)
-      
-      // 生成标签
-      const labels = dates.map(d => formatDate(d))
-      console.log('Labels:', labels)
-      
-      // 计算趋势线数据
-      const trendLineData = this.calculateTrendLine(historicalSales)
-      console.log('Trend line data:', trendLineData)
-      
-      // 生成预测日期标签
-      const predictionDates = this.generatePredictionDates(parseInt(this.predictionPeriod))
-      
-      // 合并历史和预测数据
-      const combinedLabels = [...labels, ...predictionDates]
-      
-      console.log('Combined labels:', combinedLabels)
-      
-      if (this.salesTrendChart) this.salesTrendChart.destroy()
-      
+      this.loading = true
+      this.startProgress()
       try {
-        this.salesTrendChart = new Chart(canvas, {
-          type: 'line',
-          data: {
-            labels: combinedLabels,
-            datasets: [
-              {
-                label: '历史销量',
-                data: [...historicalSales, ...Array(predictedSales.length).fill(null)],
-                borderColor: '#3b82f6',
-                backgroundColor: 'rgba(59,130,246,0.15)',
-                lineTension: 0.25,
-                pointRadius: 0,
-                pointHoverRadius: 6,
-                pointHoverBackgroundColor: '#3b82f6',
-                pointHoverBorderColor: '#ffffff',
-                pointHoverBorderWidth: 2
-              },
-              {
-                label: '预测销量',
-                data: [...Array(historicalSales.length).fill(null), ...predictedSales],
-                borderColor: '#10b981',
-                backgroundColor: 'rgba(16,185,129,0.15)',
-                borderDash: [5, 5],
-                lineTension: 0.25,
-                pointRadius: 0,
-                pointHoverRadius: 6,
-                pointHoverBackgroundColor: '#10b981',
-                pointHoverBorderColor: '#ffffff',
-                pointHoverBorderWidth: 2
-              },
-              {
-                label: '趋势线',
-                data: [...trendLineData, ...Array(predictedSales.length).fill(null)],
-                borderColor: '#8b5cf6',
-                backgroundColor: 'transparent',
-                lineTension: 0.1,
-                pointRadius: 0,
-                pointHoverRadius: 0
-              }
-            ]
-          },
-          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: {
-              x: {
-                ticks: {
-                  maxRotation: 0,
-                  autoSkip: true,
-                  maxTicksLimit: 12
-                }
-              },
-              y: {
-                beginAtZero: true
-              }
-            }
-          },
-          plugins: [{
-            id: 'prediction-area',
-            beforeDatasetsDraw(chart) {
-              const ctx = chart.ctx
-              const chartArea = chart.chartArea
-              const xScale = chart.scales['x']
-              
-              if (historicalSales.length === 0 || predictedSales.length === 0) return
-              
-              const predictionStartIndex = historicalSales.length - 1
-              const x1 = xScale.getPixelForValue(predictionStartIndex)
-              
-              ctx.save()
-              ctx.fillStyle = 'rgba(16,185,129,0.05)'
-              ctx.fillRect(x1, chartArea.top, chartArea.right - x1, chartArea.bottom - chartArea.top)
-              
-              ctx.strokeStyle = 'rgba(16,185,129,0.3)'
-              ctx.setLineDash([5, 5])
-              ctx.lineWidth = 1
-              ctx.beginPath()
-              ctx.moveTo(x1, chartArea.top)
-              ctx.lineTo(x1, chartArea.bottom)
-              ctx.stroke()
-              
-              ctx.fillStyle = 'rgba(16,185,129,0.8)'
-              ctx.font = 'bold 12px sans-serif'
-              ctx.textAlign = 'left'
-              ctx.fillText('预测区域', x1 + 10, chartArea.top + 20)
-              ctx.restore()
-            }
-          }]
-        })
-        console.log('Chart rendered successfully')
+        if (this.productFile && !this.productUploaded) {
+          await uploadProductFile(this.productFile)
+          this.productUploaded = true
+        }
+        if (this.salesFile) {
+          const analyzed = await analyzeSaleTrendWithFile(this.salesFile)
+          if (!analyzed.success) throw new Error(analyzed.message || '销售数据分析失败')
+          this.analysisResults = analyzed.data || {}
+          this.selectedItem = this.selectOptions[0] || this.selectedItem
+          this.salesFile = null
+        }
+        const predicted = await predictSalesTrend({ predict_days: this.predictionPeriod })
+        if (!predicted.success) throw new Error(predicted.message || '预测失败')
+        this.predictionResults = predicted.data || {}
+        this.$nextTick(this.renderChart)
+        this.$modal.msgSuccess('预测完成')
       } catch (error) {
-        console.error('Error rendering chart:', error)
-      }
-    },
-    /** 计算趋势线 */
-    calculateTrendLine(data) {
-      if (data.length < 2) return data
-      
-      const n = data.length
-      let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0
-      
-      for (let i = 0; i < n; i++) {
-        sumX += i
-        sumY += data[i]
-        sumXY += i * data[i]
-        sumXX += i * i
+        this.$modal.msgError(error.message || '预测失败,请检查服务状态')
+      } finally {
+        this.loading = false
+        this.progress = 100
+        this.stopProgress()
       }
-      
-      const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX)
-      const intercept = (sumY - slope * sumX) / n
-      
-      return data.map((_, i) => slope * i + intercept)
     },
-    /** 生成预测日期 */
-    generatePredictionDates(period) {
-      const dates = []
-      const lastDate = new Date()
-      
-      for (let i = 1; i <= period; i++) {
-        const date = new Date(lastDate)
-        date.setDate(date.getDate() + i)
-        dates.push(formatDate(date))
-      }
-      
-      return dates
-    },
-    /** 获取趋势样式类 */
-    getTrendClass(trend) {
-      if (!trend) return ''
-      if (trend === '上升') return 'text-green-600 font-medium'
-      if (trend === '下降') return 'text-red-600 font-medium'
-      return 'text-gray-600 font-medium'
+    startProgress() {
+      this.progress = 5
+      this.stopProgress()
+      this.progressTimer = setInterval(async () => {
+        try {
+          const res = await getUploadProgress()
+          if (res && res.success) this.progress = Math.min(99, res.progress || this.progress)
+        } catch (e) {
+          this.progress = Math.min(95, this.progress + 8)
+        }
+      }, 600)
+    },
+    stopProgress() {
+      if (this.progressTimer) clearInterval(this.progressTimer)
+      this.progressTimer = null
+    },
+    renderChart() {
+      if (!this.chart) return
+      const histDates = this.historicalDetail.date_series || []
+      const histValues = this.historicalDetail.quantity_series || []
+      const predDates = this.predictionDetail.date_series || []
+      const predValues = this.predictionDetail.quantity_series || []
+      const labels = [...histDates, ...predDates]
+      const lower = predValues.map(v => Math.max(0, Number(v || 0) * (1 - this.confidenceBandRate)))
+      const upper = predValues.map(v => Number(v || 0) * (1 + this.confidenceBandRate))
+      this.chart.setOption({
+        tooltip: { trigger: 'axis' },
+        grid: { top: 30, left: 45, right: 24, bottom: 45 },
+        xAxis: { type: 'category', data: labels, boundaryGap: false },
+        yAxis: { type: 'value' },
+        series: [
+          { name: '历史销量', type: 'line', smooth: true, data: [...histValues, ...predDates.map(() => null)], itemStyle: { color: '#3b82f6' } },
+          { name: '预测销量', type: 'line', smooth: true, lineStyle: { type: 'dashed' }, data: [...histDates.map(() => null), ...predValues], itemStyle: { color: '#f59e0b' } },
+          { name: '置信下限', type: 'line', smooth: true, symbol: 'none', lineStyle: { opacity: 0 }, data: [...histDates.map(() => null), ...lower], stack: 'confidence' },
+          { name: '置信区间', type: 'line', smooth: true, symbol: 'none', lineStyle: { opacity: 0 }, areaStyle: { color: 'rgba(107,114,128,.16)' }, data: [...histDates.map(() => null), ...upper.map((v, i) => v - lower[i])], stack: 'confidence' }
+        ]
+      })
     },
-    /** 获取置信度样式类 */
-    getConfidenceClass(confidence) {
-      const confidenceValue = typeof confidence === 'number' ? confidence : parseFloat(confidence)
-      if (confidenceValue >= 0.8) return 'text-green-600'
-      if (confidenceValue >= 0.6) return 'text-yellow-600'
-      return 'text-red-600'
+    resizeChart() {
+      if (this.chart) this.chart.resize()
     },
-    /** 导出预测结果 */
     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 blob = new Blob([JSON.stringify({ analysis: this.analysisResults, prediction: this.predictionResults }, null, 2)], { type: 'application/json;charset=utf-8' })
       const url = URL.createObjectURL(blob)
-      const a = document.createElement('a')
-      a.href = url
-      a.download = `sales_trend_prediction_${this.formatUploadDate(new Date())}.json`
-      document.body.appendChild(a)
-      a.click()
-      document.body.removeChild(a)
+      const link = document.createElement('a')
+      link.href = url
+      link.download = `sales_prediction_${Date.now()}.json`
+      link.click()
       URL.revokeObjectURL(url)
     },
-    /** 格式化上传日期 */
-    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}`
-    },
-    /** 选择第一个SKU */
-    pickFirstSku(obj) {
-      const keys = Object.keys(obj || {})
-      const first = keys.find(k => k !== '_analysis_summary_')
-      return first || ''
+    formatNumber(value) {
+      return Math.round(Number(value || 0)).toLocaleString()
     }
   }
 }
 </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-yellow-600 { color: #ca8a04; }
-
-.bg-white { background-color: #ffffff; }
-.bg-gray-50 { background-color: #f9fafb; }
-.bg-blue-500 { background-color: #3b82f6; }
-.bg-green-500 { background-color: #10b981; }
-.bg-purple-500 { background-color: #8b5cf6; }
-
-.border { border: 1px solid #e5e7eb; }
-.border-gray-200 { border-color: #e5e7eb; }
-.rounded-xl { border-radius: 0.75rem; }
-.rounded-lg { border-radius: 0.5rem; }
-.rounded-full { border-radius: 9999px; }
-
-.shadow-sm { box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); }
-
-.h-96 { height: 24rem; }
-
-.progress-bar {
-  height: 8px;
-  background-color: #e5e7eb;
-  border-radius: 4px;
-  overflow: hidden;
-  
-  .progress-value {
-    height: 100%;
-    transition: width 0.3s ease;
-  }
-}
-
-@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)); }
-  .lg\:col-span-2 { grid-column: span 2 / span 2; }
-}
-</style>
+.sales-page { padding: 20px; max-width: 1400px; margin: 0 auto; background: #f0f2f5; min-height: calc(100vh - 84px); color: #303133; }
+.header-bar, .filter-bar, .chart-panel, .panel { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,.06); }
+.header-bar { display: flex; align-items: center; justify-content: space-between; padding: 16px 24px; margin-bottom: 20px; gap: 16px; flex-wrap: wrap; }
+.header-title { margin: 0; font-size: 20px; font-weight: 600; }
+.header-actions { display: flex; gap: 10px; flex-wrap: wrap; }
+.filter-bar { display: flex; align-items: flex-end; gap: 15px; padding: 20px 24px; margin-bottom: 20px; flex-wrap: wrap; }
+.filter-group { display: flex; flex-direction: column; gap: 6px; min-width: 150px; }
+.filter-group.wide { min-width: 240px; }
+.filter-group label { font-size: 12px; color: #606266; font-weight: 500; }
+.file-hint { margin-left: auto; display: flex; flex-wrap: wrap; gap: 12px; color: #67c23a; font-size: 13px; }
+.progress { margin-bottom: 16px; }
+.chart-panel { padding: 24px; margin-bottom: 20px; }
+.chart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; gap: 16px; flex-wrap: wrap; }
+.chart-summary { display: flex; gap: 28px; }
+.chart-summary div { display: flex; flex-direction: column; gap: 4px; }
+.summary-label { color: #909399; font-size: 12px; }
+.chart-summary strong { font-size: 24px; }
+.up { color: #67c23a; }
+.down { color: #f56c6c; }
+.legend { display: flex; gap: 15px; color: #606266; font-size: 12px; flex-wrap: wrap; }
+.legend span { display: inline-flex; align-items: center; gap: 6px; }
+.legend i { display: inline-block; }
+.solid { width: 20px; height: 2px; }
+.dashed { width: 20px; border-top: 2px dashed; }
+.box { width: 12px; height: 12px; background: rgba(107,114,128,.2); border: 1px solid #9ca3af; }
+.blue { background: #3b82f6; color: #3b82f6; }
+.orange { color: #f59e0b; }
+.gray { color: #9ca3af; }
+.chart { height: 400px; }
+.data-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; }
+.panel { padding: 24px; min-width: 0; }
+.panel h3 { margin: 0 0 18px; font-size: 16px; }
+.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
+.panel-head h3 { margin: 0; }
+.panel-head .el-input { max-width: 220px; }
+.metric-panel { display: flex; flex-direction: column; gap: 15px; }
+.metric-card { border-left: 4px solid #667eea; background: linear-gradient(135deg,#f8f9fa,#eef1f5); border-radius: 8px; padding: 18px; }
+.metric-card span { display: block; color: #909399; font-size: 12px; margin-bottom: 8px; }
+.metric-card strong { font-size: 24px; }
+.metric-card.orange { border-left-color: #f59e0b; }
+.metric-card.green { border-left-color: #67c23a; }
+.metric-card.blue { border-left-color: #3b82f6; }
+@media (max-width: 1100px) { .data-grid { grid-template-columns: 1fr; } .file-hint { margin-left: 0; } }
+</style>