Yihao Kang 4 сар өмнө
parent
commit
2504aff9fd

+ 42 - 0
src/api/sales.js

@@ -0,0 +1,42 @@
+import request from '@/utils/request'
+
+// 上传文件并分析销售数据
+export function uploadAndAnalyzeSales(file) {
+  const formData = new FormData()
+  formData.append('file', file)
+  return request({
+    url: '/statistics/sales/upload',
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    },
+    timeout: 300000 // 5分钟超时,因为分析可能需要较长时间
+  })
+}
+
+// 获取销售分析结果
+export function getSalesResults() {
+  return request({
+    url: '/statistics/sales/results',
+    method: 'get'
+  })
+}
+
+// 获取销售分析总览数据
+export function getSalesOverview(params) {
+  return request({
+    url: '/statistics/sales/overview',
+    method: 'get',
+    params: params
+  })
+}
+
+// 预测销量趋势
+export function predictSalesTrend(params) {
+  return request({
+    url: '/statistics/sales/predict',
+    method: 'post',
+    data: params
+  })
+}

+ 11 - 0
src/views/sale/control/index.vue

@@ -0,0 +1,11 @@
+<script setup>
+
+</script>
+
+<template>
+
+</template>
+
+<style scoped lang="scss">
+
+</style>

+ 11 - 0
src/views/sale/effect/index.vue

@@ -0,0 +1,11 @@
+<script setup>
+
+</script>
+
+<template>
+
+</template>
+
+<style scoped lang="scss">
+
+</style>

+ 11 - 0
src/views/sale/evaluation/index.vue

@@ -0,0 +1,11 @@
+<script setup>
+
+</script>
+
+<template>
+
+</template>
+
+<style scoped lang="scss">
+
+</style>

+ 11 - 0
src/views/sale/feature/index.vue

@@ -0,0 +1,11 @@
+<script setup>
+
+</script>
+
+<template>
+
+</template>
+
+<style scoped lang="scss">
+
+</style>

+ 707 - 0
src/views/sale/overview/index.vue

@@ -0,0 +1,707 @@
+<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>
+      <el-upload
+        ref="upload"
+        :limit="1"
+        accept=".xlsx,.xls,.csv"
+        :http-request="customUpload"
+        :disabled="upload.isUploading"
+        :on-progress="handleFileUploadProgress"
+        :before-upload="beforeUpload"
+        :auto-upload="false"
+        drag
+      >
+        <i class="el-icon-upload"></i>
+        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+        <div class="el-upload__tip" slot="tip">
+          <el-checkbox v-model="upload.updateSupport" /> 是否覆盖已上传的文件
+          <div>只能上传xlsx/xls/csv文件,且不超过20MB</div>
+        </div>
+      </el-upload>
+      <div style="margin-top: 15px">
+        <el-button :loading="upload.isUploading" type="primary" @click="submitUpload">立即上传并分析</el-button>
+        <el-button @click="resetUpload">重置</el-button>
+      </div>
+    </el-card>
+
+    <!-- 关键指标卡片 -->
+    <el-row :gutter="20" class="mb-20">
+      <el-col :xs="24" :sm="12" :md="8" :lg="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>
+</template>
+
+<script>
+import { uploadAndAnalyzeSales, getSalesResults } from '@/api/sales'
+import { getToken } from '@/utils/auth'
+import * as echarts from 'echarts'
+require('echarts/theme/macarons')
+
+export default {
+  name: 'SalesOverview',
+  data() {
+    return {
+      // 图表实例
+      priceTrendChart: null,
+      promotionTrendChart: null,
+      salesTrendChart: null,
+      anomalyDetectionChart: null,
+      // 数据
+      results: {},
+      // 计算属性数据
+      totalSales: 0,
+      salesGrowthRate: 0,
+      avgPrice: 0,
+      priceChange: 0,
+      avgPromotion: 0,
+      promotionChange: 0,
+      anomalyDetectionRate: 0,
+      // 趋势数据
+      timeSeries: [],
+      priceSeries: [],
+      promotionSeries: [],
+      salesSeries: [],
+      anomalySeries: [],
+      // 文件上传相关
+      upload: {
+        // 是否显示弹出层
+        open: false,
+        // 弹出层标题
+        title: '',
+        // 是否禁用上传
+        isUploading: false,
+        // 是否更新已经存在的文件
+        updateSupport: 0,
+        // 设置上传的请求头部
+        headers: { Authorization: 'Bearer ' + getToken() },
+        // 上传的地址
+        url: process.env.VUE_APP_BASE_API + '/statistics/sales/upload'
+      }
+    }
+  },
+  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()
+    }
+    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 isLt20M = file.size / 1024 / 1024 < 20
+
+      if (!isExcel) {
+        this.$modal.msgError('上传文件只能是 xlsx/xls/csv 格式!')
+        return false
+      }
+      if (!isLt20M) {
+        this.$modal.msgError('上传文件大小不能超过 20MB!')
+        return false
+      }
+      return true
+    },
+    /** 文件上传中处理 */
+    handleFileUploadProgress(event, file, fileList) {
+      this.upload.isUploading = true
+    },
+    /** 自定义上传方法 */
+    customUpload(options) {
+      const file = options.file
+      this.upload.isUploading = true
+      uploadAndAnalyzeSales(file).then(response => {
+        this.upload.isUploading = false
+        if (response.code === 200) {
+          this.$modal.msgSuccess('文件上传并分析成功')
+          // response.data 就是分析结果
+          this.results = response.data || {}
+          this.calculateMetrics()
+          this.$nextTick(() => {
+            this.renderCharts()
+          })
+          options.onSuccess(response)
+        } else {
+          this.$modal.msgError(response.msg || '分析失败')
+          options.onError(new Error(response.msg || '分析失败'))
+        }
+        // 重置上传组件
+        this.$refs.upload.clearFiles()
+      }).catch(error => {
+        this.upload.isUploading = false
+        const errorMsg = error.response?.data?.msg || error.message || '文件上传失败,请重试'
+        this.$modal.msgError(errorMsg)
+        options.onError(error)
+      })
+    },
+    /** 提交上传文件 */
+    submitUpload() {
+      const fileList = this.$refs.upload.uploadFiles
+      if (!fileList || fileList.length === 0) {
+        this.$modal.msgError('请选择要上传的文件')
+        return
+      }
+      this.$refs.upload.submit()
+    },
+    /** 重置上传 */
+    resetUpload() {
+      this.$refs.upload.clearFiles()
+    },
+    /** 获取销售分析结果 */
+    getList() {
+      getSalesResults().then(response => {
+        if (response.code === 200 && response.data) {
+          // response.data 就是分析结果
+          this.results = response.data || {}
+          this.calculateMetrics()
+          this.$nextTick(() => {
+            this.renderCharts()
+          })
+        }
+      }).catch(() => {
+        // 如果没有数据,不显示错误,只是不显示图表
+        this.results = {}
+      })
+    },
+    /** 计算关键指标 */
+    calculateMetrics() {
+      // 模拟数据,实际项目中应从results中计算
+      this.totalSales = 12580
+      this.salesGrowthRate = 12.5
+      this.avgPrice = 89.65
+      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]
+    },
+    /** 初始化图表 */
+    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'
+            }
+          }
+        ]
+      }
+
+      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)
+      }
+    },
+    /** 渲染销量趋势图表 */
+    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)
+      }
+    },
+    /** 渲染异常数据检测图表 */
+    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)
+      }
+    },
+    /** 窗口大小变化处理 */
+    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()
+      }
+    }
+  }
+}
+</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;
+  }
+}
+</style>

+ 852 - 0
src/views/sale/trendPred/index.vue

@@ -0,0 +1,852 @@
+<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">
+        <el-upload
+          ref="toolbarUpload"
+          class="toolbar-upload"
+          :limit="1"
+          accept=".xlsx,.xls,.csv"
+          :http-request="customUpload"
+          :disabled="upload.isUploading"
+          :on-change="handleFileChange"
+          :before-upload="beforeUpload"
+          :auto-upload="false"
+          :show-file-list="false"
+        >
+          <el-button plain>上传文件</el-button>
+        </el-upload>
+        <el-button :loading="upload.isUploading" type="primary" @click="submitUpload">开始预测</el-button>
+        <el-button type="success" :disabled="!hasResults" @click="exportResults">导出预测</el-button>
+      </div>
+      <div class="toolbar-status" v-if="upload.fileName">已上传:{{ upload.fileName }}</div>
+      <div class="toolbar-status" v-else-if="upload.pendingFileName">已选择:{{ upload.pendingFileName }}</div>
+      <div class="toolbar-status muted" v-else>未上传</div>
+    </div>
+
+    <!-- SKU选择 + 预测周期选择 + 基础信息板块 -->
+    <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">选择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 sku-select"
+                  v-model="selectedSku">
+            <option v-for="k in skuOptions" :key="k" :value="k">{{ k }}</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>
+      </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">SKU编码</p>
+          <p class="text-lg font-medium text-gray-800 truncate">{{ selectedSku }}</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>
+      </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>
+      <div class="h-96">
+        <canvas ref="salesTrendRef"></canvas>
+      </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>
+    </div>
+
+    <!-- 预测指标与详情分组 -->
+    <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-20">
+      <!-- 预测指标板块 -->
+      <div class="bg-white rounded-xl p-6 shadow-sm space-y-6">
+        <h3 class="text-lg font-semibold text-gray-800">预测指标</h3>
+        <div>
+          <div class="flex justify-between mb-2">
+            <span class="text-sm text-gray-500">预测期总销量</span>
+            <span class="text-sm font-medium text-gray-800">{{ 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>
+          </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>
+        </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>
+            </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>
+            </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </div>
+
+    <!-- 模型评估模块 -->
+    <div class="bg-white rounded-xl p-6 mb-20 shadow-sm">
+      <div class="flex justify-between items-center mb-6">
+        <h3 class="text-lg font-semibold text-gray-800">模型评估</h3>
+        <div class="text-sm" :class="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>
+        <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>
+        <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>
+        <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>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, beforeUnmount, watch } from 'vue'
+import { uploadAndAnalyzeSales, getSalesResults, predictSalesTrend } from '@/api/sales'
+import { getToken } from '@/utils/auth'
+import { Chart } from 'chart.js'
+import { formatDate } from '../../../utils/format'
+
+export default {
+  name: 'SalesTrendPrediction',
+  data() {
+    return {
+      salesTrendChart: null,
+      upload: {
+        isUploading: false,
+        fileName: '',
+        pendingFileName: ''
+      },
+      predictionPeriod: '7',
+      results: {}
+    }
+  },
+  computed: {
+    hasResults() {
+      return Object.keys(this.results || {}).length > 0
+    },
+    selectedSku: {
+      get() {
+        const state = this.$store && this.$store.state && this.$store.state.analysis
+        return (state && state.selectedSku) || ''
+      },
+      set(value) {
+        if (this.$store) {
+          this.$store.dispatch('analysis/selectSku', value)
+        }
+      }
+    },
+    skuOptions() {
+      return Object.keys(this.results || {}).filter(k => k !== '_analysis_summary_')
+    },
+    detail() {
+      return (this.results && this.selectedSku && this.results[this.selectedSku]) || null
+    },
+    totalHistoricalSales() {
+      const detail = this.detail || {}
+      return detail.total_historical_sales || 0
+    },
+    averageDailySales() {
+      const detail = this.detail || {}
+      return detail.average_daily_sales || 0
+    },
+    predictionAccuracy() {
+      const detail = this.detail || {}
+      return detail.prediction_accuracy || '0%'
+    },
+    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 || {}
+      return detail.prediction_details || []
+    },
+    mape() {
+      const detail = this.detail || {}
+      return detail.mape || '0%'
+    },
+    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 || 0
+    },
+    mae() {
+      const detail = this.detail || {}
+      return detail.mae || 0
+    },
+    rSquared() {
+      const detail = this.detail || {}
+      return detail.r_squared || 0
+    },
+    modelAccuracy() {
+      const detail = this.detail || {}
+      return detail.model_accuracy || 0
+    },
+    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'
+    },
+    modelAccuracyLevel() {
+      const accuracy = parseFloat(this.modelAccuracy) || 0
+      if (accuracy >= 80) return '(优秀)'
+      if (accuracy >= 60) return '(良好)'
+      return '(需改进)'
+    }
+  },
+  mounted() {
+    this.getList()
+  },
+  beforeUnmount() {
+    if (this.salesTrendChart) this.salesTrendChart.destroy()
+  },
+  watch: {
+    detail() {
+      this.renderSalesTrend()
+    },
+    results() {
+      if (!this.results || !this.results[this.selectedSku]) {
+        const first = this.skuOptions[0] || ''
+        if (first) this.$store.dispatch('analysis/selectSku', first)
+      }
+    },
+    predictionPeriod() {
+      if (this.selectedSku) {
+        this.predictSales()
+      }
+    }
+  },
+  methods: {
+    /** 获取销售分析结果 */
+    getList() {
+      getSalesResults().then(response => {
+        if (response && response.code === 200 && response.data) {
+          const results = response.data || {}
+          this.results = results
+          const firstSku = this.pickFirstSku(results)
+          if (firstSku) this.$store.dispatch('analysis/selectSku', firstSku)
+          this.$nextTick(() => {
+            this.renderSalesTrend()
+          })
+        }
+      }).catch(() => {
+        this.results = {}
+      })
+    },
+    /** 预测销量趋势 */
+    predictSales() {
+      if (!this.selectedSku) return
+      
+      const params = {
+        sku: this.selectedSku,
+        period: parseInt(this.predictionPeriod)
+      }
+      
+      predictSalesTrend(params).then(response => {
+        if (response && response.code === 200 && response.data) {
+          const results = response.data || {}
+          this.results = results
+          this.$nextTick(() => {
+            this.renderSalesTrend()
+          })
+        }
+      }).catch(() => {
+        this.$modal.msgError('预测失败,请重试')
+      })
+    },
+    /** 文件选择改变处理 */
+    handleFileChange(file, fileList) {
+      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 isLt20M = file.size / 1024 / 1024 < 20
+
+      if (!isExcel) {
+        this.$modal.msgError('上传文件只能是 xlsx/xls/csv 格式!')
+        return false
+      }
+      if (!isLt20M) {
+        this.$modal.msgError('上传文件大小不能超过 20MB!')
+        return false
+      }
+      return true
+    },
+    customUpload(options) {
+      const file = options.file
+      this.upload.isUploading = true
+      uploadAndAnalyzeSales(file).then(response => {
+        this.upload.isUploading = false
+        if (response && response.code === 200) {
+          const results = response.data || {}
+          this.results = results
+          const firstSku = this.pickFirstSku(results)
+          if (firstSku) this.$store.dispatch('analysis/selectSku', firstSku)
+          this.$modal.msgSuccess('文件上传并分析成功')
+          this.upload.fileName = this.upload.pendingFileName || file.name
+          this.upload.pendingFileName = ''
+          this.$nextTick(() => {
+            this.renderSalesTrend()
+          })
+          options.onSuccess(response)
+        } else {
+          this.$modal.msgError(response.msg || '分析失败')
+          options.onError(new Error(response.msg || '分析失败'))
+        }
+      }).catch(error => {
+        this.upload.isUploading = false
+        const msg = (error && error.message) || '文件上传失败,请重试'
+        this.$modal.msgError(msg)
+        options.onError(error)
+      }).finally(() => {
+        if (this.$refs.toolbarUpload) {
+          this.$refs.toolbarUpload.clearFiles()
+        }
+      })
+    },
+    submitUpload() {
+      const target = this.$refs.toolbarUpload
+      const fileList = target && target.uploadFiles ? target.uploadFiles : []
+      if (!fileList || fileList.length === 0) {
+        this.$modal.msgError('请选择要上传的文件')
+        return
+      }
+      target.submit()
+    },
+    /** 渲染销量趋势与预测图 */
+    renderSalesTrend() {
+      const canvas = this.$refs.salesTrendRef
+      if (!canvas) return
+      
+      const detail = this.detail || {}
+      const historicalSales = detail.historical_sales || []
+      const predictedSales = detail.predicted_sales || []
+      const dates = detail.date_series || []
+      const labels = dates.map(d => formatDate(d))
+      
+      // 计算趋势线数据
+      const trendLineData = this.calculateTrendLine(historicalSales)
+      
+      // 合并历史和预测数据
+      const combinedSales = [...historicalSales, ...predictedSales]
+      const combinedLabels = [...labels, ...this.generatePredictionDates(parseInt(this.predictionPeriod))]
+      
+      if (this.salesTrendChart) this.salesTrendChart.destroy()
+      
+      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: {
+            xAxes: [{
+              ticks: {
+                maxRotation: 0,
+                autoSkip: true,
+                maxTicksLimit: 12,
+                callback: function(value, index) {
+                  return combinedLabels[index]
+                }
+              }
+            }],
+            yAxes: [{
+              ticks: {
+                beginAtZero: true
+              }
+            }]
+          }
+        },
+        plugins: [{
+          id: 'prediction-area',
+          beforeDatasetsDraw(chart) {
+            const ctx = chart.ctx
+            const chartArea = chart.chartArea
+            const xScale = chart.scales['x-axis-0']
+            
+            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()
+          }
+        }]
+      })
+    },
+    /** 计算趋势线 */
+    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
+      }
+      
+      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'
+    },
+    /** 导出预测结果 */
+    exportResults() {
+      if (!this.hasResults) {
+        this.$modal.msgError('暂无可导出的预测结果')
+        return
+      }
+      
+      const payload = JSON.stringify(this.results || {}, null, 2)
+      const blob = new Blob([payload], { type: 'application/json;charset=utf-8' })
+      const url = URL.createObjectURL(blob)
+      const a = document.createElement('a')
+      a.href = url
+      a.download = `sales_trend_prediction_${this.formatUploadDate(new Date())}.json`
+      document.body.appendChild(a)
+      a.click()
+      document.body.removeChild(a)
+      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 || ''
+    }
+  }
+}
+</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>