소스 검색

库存前端

Gogs 3 달 전
부모
커밋
a2438e6366
5개의 변경된 파일2017개의 추가작업 그리고 988개의 파일을 삭제
  1. 642 143
      src/views/storage/accuracy/index.vue
  2. 387 140
      src/views/storage/cost/index.vue
  3. 491 158
      src/views/storage/efficiency/index.vue
  4. 295 108
      src/views/storage/overview/index.vue
  5. 202 439
      src/views/storage/turnover/index.vue

+ 642 - 143
src/views/storage/accuracy/index.vue

@@ -1,17 +1,15 @@
-<template>
+<template>
   <div class="semi-product">
-    <!-- 半成品概览 -->
     <el-row :gutter="20" class="overview-row">
       <el-col :span="6">
         <el-card class="stat-card">
           <div class="stat-content">
             <div class="stat-icon" style="background-color: #409eff;">
-              <!-- Replace Box icon with text symbol -->
-              <span style="font-size:28px; color:#fff;">📦</span>
+              <i class="el-icon-box" style="font-size: 28px;"></i>
             </div>
             <div class="stat-info">
               <div class="stat-label">半成品总数</div>
-              <div class="stat-value">-</div>
+              <div class="stat-value">{{ stats.totalSemiProducts }}</div>
               <div class="stat-unit">种</div>
             </div>
           </div>
@@ -21,12 +19,11 @@
         <el-card class="stat-card">
           <div class="stat-content">
             <div class="stat-icon" style="background-color: #67c23a;">
-              <!-- Replace Goods icon with text symbol -->
-              <span style="font-size:28px; color:#fff;">📦</span>
+              <i class="el-icon-goods" style="font-size: 28px;"></i>
             </div>
             <div class="stat-info">
               <div class="stat-label">库存数量</div>
-              <div class="stat-value">-</div>
+              <div class="stat-value">{{ stats.totalQuantity }}</div>
               <div class="stat-unit">件</div>
             </div>
           </div>
@@ -36,12 +33,11 @@
         <el-card class="stat-card">
           <div class="stat-content">
             <div class="stat-icon" style="background-color: #909399;">
-              <!-- Replace Goods icon with text symbol -->
-              <span style="font-size:28px; color:#fff;">📦</span>
+              <i class="el-icon-s-promotion" style="font-size: 28px;"></i>
             </div>
             <div class="stat-info">
               <div class="stat-label">预计可生产成品</div>
-              <div class="stat-value">-</div>
+              <div class="stat-value">{{ stats.estimatedFinished }}</div>
               <div class="stat-unit">件</div>
             </div>
           </div>
@@ -49,22 +45,22 @@
       </el-col>
     </el-row>
 
-    <!-- 搜索与操作栏 -->
     <el-card class="search-card">
       <el-row :gutter="20" align="middle">
         <el-col :span="6">
           <el-input
+            v-model="searchKeyword"
             placeholder="搜索半成品名称/编码"
             clearable
+            @clear="handleSearch"
           >
-            <template #prefix>
-              <!-- Replace Search icon with text symbol -->
-              <span style="font-size:16px; color:#909399;">🔍</span>
+            <template slot="prefix">
+              <i class="el-icon-search"></i>
             </template>
           </el-input>
         </el-col>
         <el-col :span="4">
-          <el-select placeholder="状态筛选" clearable>
+          <el-select v-model="filterStatus" placeholder="状态筛选" clearable>
             <el-option label="全部" value="" />
             <el-option label="在库" value="in-stock" />
             <el-option label="在制" value="in-production" />
@@ -73,210 +69,713 @@
           </el-select>
         </el-col>
         <el-col :span="4">
-          <el-select placeholder="类别筛选" clearable>
+          <el-select v-model="filterCategory" placeholder="类别筛选" clearable>
             <el-option label="全部" value="" />
             <el-option label="电子元件" value="electronics" />
             <el-option label="机械部件" value="mechanics" />
-            <el-option label="外壳模" value="housing" />
+            <el-option label="外壳模" value="housing" />
             <el-option label="电路板" value="pcb" />
           </el-select>
         </el-col>
         <el-col :span="6">
-          <el-button type="primary">
-            <!-- Replace Search icon with text -->
-            🔍 搜索
+          <el-button type="primary" @click="handleSearch">
+            <i class="el-icon-search"></i> 搜索
           </el-button>
-          <el-button>
-            <!-- Replace RefreshLeft icon with text -->
-            🔄 重置
+          <el-button @click="handleReset">
+            <i class="el-icon-refresh-left"></i> 重置
           </el-button>
         </el-col>
         <el-col :span="4" style="text-align: right;">
-          <el-button type="success">
-            <!-- Replace Plus icon with text -->
-            ➕ 新增半成品
+          <el-button type="success" @click="handleAdd">
+            <i class="el-icon-plus"></i> 新增半成品
           </el-button>
         </el-col>
       </el-row>
     </el-card>
 
-    <!-- 可组装成品能力 -->
     <el-card class="assembly-card" style="margin-bottom: 20px;">
-      <template #header>
+      <template slot="header">
         <div style="display:flex; justify-content:space-between; align-items:center;">
           <span>可组装成品能力(基于入库库存)</span>
-          <el-button type="primary" size="small">刷新</el-button>
+          <el-button type="primary" size="small" @click="fetchAssemblyCapacities">刷新</el-button>
         </div>
       </template>
-      <el-table stripe style="width: 100%">
-        <el-table-column label="成品编码" width="180" />
-        <el-table-column label="名称" width="220" />
-        <el-table-column label="可组装数量" width="140" />
-        <el-table-column label="所需半成品及库存" />
+      <el-table :data="assemblyCapacities" stripe style="width: 100%">
+        <el-table-column prop="product_code" label="成品编码" width="180" />
+        <el-table-column prop="product_name" label="名称" width="220" />
+        <el-table-column prop="capacity" label="可组装数量" width="140" />
+        <el-table-column label="所需半成品及库存">
+          <template slot-scope="scope">
+            <div>
+              <el-tag
+                v-for="c in scope.row.components"
+                :key="scope.row.product_code + c.code"
+                size="small"
+                style="margin: 2px 4px;"
+              >
+                {{ c.code }}: {{ c.available }}
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
       </el-table>
     </el-card>
 
-    <!-- 半成品列表 -->
     <el-card class="table-card">
-      <el-table stripe style="width: 100%">
+      <el-table :data="semiProductList" stripe style="width: 100%">
         <el-table-column type="selection" width="55" />
-        <el-table-column label="编码" width="120" fixed />
-        <el-table-column label="半成品名称" width="200" />
-        <el-table-column label="类别" width="120" />
-        <el-table-column label="库存数量" width="100" />
-        <el-table-column label="安全库存" width="100" />
-        <el-table-column label="状态" width="100" />
-        <el-table-column label="可用于组装" width="150" />
-        <el-table-column label="供应商" width="150" />
-        <el-table-column label="采购周期" width="100" />
-        <el-table-column label="单价(元)" width="100" />
-        <el-table-column label="库存价值(元)" width="120" />
+        <el-table-column prop="code" label="编码" width="120" fixed />
+        <el-table-column prop="name" label="半成品名称" width="200" />
+        <el-table-column label="类别" width="120">
+          <template slot-scope="scope">
+            <el-tag>{{ getCategoryText(scope.row.category) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="quantity" label="库存数量" width="100" sortable />
+        <el-table-column prop="safeStock" label="安全库存" width="100" />
+        <el-table-column label="状态" width="100">
+          <template slot-scope="scope">
+            <el-tag :type="getStatusType(scope.row.status)">
+              {{ getStatusText(scope.row.status) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="可用于组装" width="150">
+          <template slot-scope="scope">
+            <el-popover placement="top" :width="300" trigger="hover">
+              <template slot="reference">
+                <el-tag type="info" style="cursor: pointer;">
+                  {{ scope.row.usedForProducts.length }} 种成品
+                </el-tag>
+              </template>
+              <div>
+                <p style="font-weight: bold; margin-bottom: 8px;">可组装成品</p>
+                <el-tag
+                  v-for="product in scope.row.usedForProducts"
+                  :key="product"
+                  size="small"
+                  style="margin: 2px 4px;"
+                >
+                  {{ product }}
+                </el-tag>
+              </div>
+            </el-popover>
+          </template>
+        </el-table-column>
+        <el-table-column prop="supplier" label="供应商" width="150" />
+        <el-table-column prop="leadTime" label="采购周期" width="100">
+          <template slot-scope="scope">
+            {{ scope.row.leadTime }} 天
+          </template>
+        </el-table-column>
+        <el-table-column prop="unitPrice" label="单价(元)" width="100" />
+        <el-table-column prop="totalValue" label="库存价值(元)" width="120" sortable />
         <el-table-column label="操作" width="240" fixed="right">
-          <template #default>
-            <el-button type="primary" size="small">详情</el-button>
-            <el-button type="success" size="small">编辑</el-button>
-            <el-button type="warning" size="small">组装</el-button>
-            <el-button type="danger" size="small">删除</el-button>
+          <template slot-scope="scope">
+            <el-button type="primary" size="small" @click="handleDetail(scope.row)">详情</el-button>
+            <el-button type="success" size="small" @click="handleEdit(scope.row)">编辑</el-button>
+            <el-button type="warning" size="small" @click="handleAssemble(scope.row)">组装</el-button>
+            <el-button type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
 
       <el-pagination
-        :current-page="1"
-        :page-size="10"
-        :total="0"
+        :current-page="currentPage"
+        :page-size="pageSize"
+        :total="totalItems"
         :page-sizes="[10, 20, 50, 100]"
         layout="total, sizes, prev, pager, next, jumper"
         style="margin-top: 20px; justify-content: flex-end;"
       />
     </el-card>
 
-    <!-- 半成品分析图表 -->
     <el-row :gutter="20" class="charts-row">
       <el-col :span="12">
         <el-card>
-          <template #header>
+          <template slot="header">
             <span>半成品库存分布</span>
           </template>
-          <div style="height: 300px; display: flex; align-items: center; justify-content: center; color: #909399;">
-            暂无图表数据
-          </div>
+          <div ref="stockDistributionChart" class="chart" />
         </el-card>
       </el-col>
       <el-col :span="12">
         <el-card>
-          <template #header>
+          <template slot="header">
             <span>半成品使用趋势</span>
           </template>
-          <div style="height: 300px; display: flex; align-items: center; justify-content: center; color: #909399;">
-            暂无图表数据
-          </div>
+          <div ref="usageTrendChart" class="chart" />
         </el-card>
       </el-col>
     </el-row>
 
+    <el-card class="bom-card">
+      <template slot="header">
+        <div class="card-header">
+          <span>半成品BOM关系图</span>
+          <el-select
+            v-model="selectedBomProduct"
+            placeholder="选择成品查看BOM"
+            style="width: 250px;"
+          >
+            <el-option label="智能手表 Pro X1" value="watch" />
+            <el-option label="无线耳机 Elite" value="earphone" />
+            <el-option label="智能音箱" value="speaker" />
+          </el-select>
+        </div>
+      </template>
+      <div ref="bomRelationChart" class="chart" style="height: 400px;"></div>
+    </el-card>
 
-
-    <!-- 半成品周转分析 -->
     <el-card class="turnover-card">
-      <template #header>
+      <template slot="header">
         <span>半成品周转效率分析</span>
       </template>
-      <el-table stripe>
-        <el-table-column label="半成品名称" width="200" />
-        <el-table-column label="平均周转天数" width="150" />
-        <el-table-column label="使用率" width="120" />
-        <el-table-column label="月消耗量" width="120" />
-        <el-table-column label="再订货点" width="120" />
-        <el-table-column label="库存状态" width="120" />
-        <el-table-column label="优化建议" min-width="250" />
+      <el-table :data="turnoverAnalysis" stripe>
+        <el-table-column prop="name" label="半成品名称" width="200" />
+        <el-table-column prop="avgTurnover" label="平均周转天数" width="150" sortable>
+          <template slot-scope="scope">
+            <span :style="{ color: getTurnoverColor(scope.row.avgTurnover) }">
+              {{ scope.row.avgTurnover }} 天
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="usageRate" label="使用率" width="120" sortable>
+          <template slot-scope="scope">
+            <el-progress :percentage="scope.row.usageRate" :color="getProgressColor(scope.row.usageRate)" />
+          </template>
+        </el-table-column>
+        <el-table-column prop="monthlyConsumption" label="月消耗量" width="120" />
+        <el-table-column prop="reorderPoint" label="再订货点" width="120" />
+        <el-table-column label="库存状态" width="120">
+          <template slot-scope="scope">
+            <el-tag :type="getStockStatusType(scope.row.stockStatus)">
+              {{ scope.row.stockStatus }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="suggestion" label="优化建议" min-width="250" />
       </el-table>
     </el-card>
   </div>
 </template>
 
-<script setup>
-  // REMOVED ALL ICON IMPORTS (no @element-plus/icons-vue dependency)
+<script>
+import request from '@/utils/request'
+import { Message, MessageBox } from 'element-ui'
+import * as echarts from 'echarts'
+require('echarts/theme/macarons')
+
+export default {
+  name: 'StorageSemiProduct',
+  data() {
+    return {
+      stats: {
+        totalSemiProducts: 0,
+        totalQuantity: 0,
+        estimatedFinished: 0
+      },
+      searchKeyword: '',
+      filterStatus: '',
+      filterCategory: '',
+      currentPage: 1,
+      pageSize: 10,
+      semiProductList: [
+        {
+          code: 'SEM-101',
+          name: '电路板 PCB-A',
+          category: 'pcb',
+          quantity: 3200,
+          safeStock: 2000,
+          status: 'in-stock',
+          usedForProducts: ['智能手表 Pro X1', '智能手环'],
+          supplier: '深圳电子有限公司',
+          leadTime: 15,
+          unitPrice: 45.8,
+          totalValue: 146560
+        },
+        {
+          code: 'SEM-102',
+          name: '显示屏模块',
+          category: 'electronics',
+          quantity: 2800,
+          safeStock: 1500,
+          status: 'in-stock',
+          usedForProducts: ['智能手表 Pro X1'],
+          supplier: '京东方科技',
+          leadTime: 20,
+          unitPrice: 128.5,
+          totalValue: 359800
+        },
+        {
+          code: 'SEM-103',
+          name: '锂电池组',
+          category: 'electronics',
+          quantity: 1200,
+          safeStock: 1800,
+          status: 'pending',
+          usedForProducts: ['智能手表 Pro X1', '无线耳机 Elite'],
+          supplier: '宁德时代',
+          leadTime: 25,
+          unitPrice: 68.0,
+          totalValue: 81600
+        },
+        {
+          code: 'SEM-104',
+          name: '金属外壳',
+          category: 'housing',
+          quantity: 4500,
+          safeStock: 3000,
+          status: 'in-stock',
+          usedForProducts: ['智能手表 Pro X1', '智能音箱'],
+          supplier: '精密制造厂',
+          leadTime: 12,
+          unitPrice: 35.2,
+          totalValue: 158400
+        },
+        {
+          code: 'SEM-105',
+          name: '蓝牙模块',
+          category: 'electronics',
+          quantity: 580,
+          safeStock: 1000,
+          status: 'in-production',
+          usedForProducts: ['无线耳机 Elite', '智能音箱', '智能手表 Pro X1'],
+          supplier: '高通技术',
+          leadTime: 30,
+          unitPrice: 85.5,
+          totalValue: 49590
+        }
+      ],
+      assemblyCapacities: [],
+      selectedBomProduct: 'watch',
+      turnoverAnalysis: [
+        {
+          name: '电路板 PCB-A',
+          avgTurnover: 18,
+          usageRate: 85,
+          monthlyConsumption: 850,
+          reorderPoint: 1200,
+          stockStatus: '正常',
+          suggestion: '库存充足,周转良好'
+        },
+        {
+          name: '显示屏模块',
+          avgTurnover: 22,
+          usageRate: 78,
+          monthlyConsumption: 650,
+          reorderPoint: 1000,
+          stockStatus: '正常',
+          suggestion: '建议保持当前库存水平'
+        },
+        {
+          name: '锂电池组',
+          avgTurnover: 35,
+          usageRate: 92,
+          monthlyConsumption: 720,
+          reorderPoint: 1500,
+          stockStatus: '偏低',
+          suggestion: '建议立即补货,避免缺货风险'
+        },
+        {
+          name: '金属外壳',
+          avgTurnover: 15,
+          usageRate: 65,
+          monthlyConsumption: 580,
+          reorderPoint: 2000,
+          stockStatus: '充足',
+          suggestion: '库存充裕,可适当减少采购量'
+        },
+        {
+          name: '蓝牙模块',
+          avgTurnover: 45,
+          usageRate: 95,
+          monthlyConsumption: 480,
+          reorderPoint: 800,
+          stockStatus: '紧张',
+          suggestion: '使用率高,建议增加安全库存'
+        }
+      ],
+      stockDistributionOption: {
+        tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
+        legend: { orient: 'vertical', left: 'left' },
+        series: [
+          {
+            name: '半成品类别',
+            type: 'pie',
+            radius: ['40%', '70%'],
+            avoidLabelOverlap: false,
+            itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
+            label: { show: true, formatter: '{b}: {d}%' },
+            data: [
+              { value: 6800, name: '电子元件', itemStyle: { color: '#5470c6' } },
+              { value: 4500, name: '机械部件', itemStyle: { color: '#91cc75' } },
+              { value: 3200, name: '外壳模块', itemStyle: { color: '#fac858' } },
+              { value: 2620, name: '电路板', itemStyle: { color: '#ee6666' } }
+            ]
+          }
+        ]
+      },
+      usageTrendOption: {
+        tooltip: { trigger: 'axis' },
+        legend: { data: ['入库', '出库', '组装消耗'] },
+        grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
+        xAxis: { type: 'category', boundaryGap: false, data: ['1月', '2月', '3月', '4月', '5月', '6月'] },
+        yAxis: { type: 'value', name: '数量' },
+        series: [
+          { name: '入库', type: 'line', data: [1200, 1350, 1180, 1450, 1600, 1520], smooth: true, itemStyle: { color: '#67c23a' } },
+          { name: '出库', type: 'line', data: [980, 1120, 1050, 1280, 1380, 1450], smooth: true, itemStyle: { color: '#e6a23c' } },
+          { name: '组装消耗', type: 'line', data: [850, 980, 920, 1150, 1250, 1320], smooth: true, itemStyle: { color: '#409eff' } }
+        ]
+      },
+      bomDataMap: {
+        watch: {
+          name: '智能手表 Pro X1',
+          children: [
+            {
+              name: '电子模块',
+              children: [
+                { name: '电路板 PCB-A\n库存: 3200' },
+                { name: '显示屏模块\n库存: 2800' },
+                { name: '锂电池组\n库存: 1200' },
+                { name: '蓝牙模块\n库存: 580' }
+              ]
+            },
+            {
+              name: '结构件',
+              children: [
+                { name: '金属外壳\n库存: 4500' },
+                { name: '表带\n库存: 5200' },
+                { name: '按键组件\n库存: 6800' }
+              ]
+            },
+            {
+              name: '包装材料',
+              children: [
+                { name: '包装盒\n库存: 8500' },
+                { name: '说明书\n库存: 9200' }
+              ]
+            }
+          ]
+        },
+        earphone: {
+          name: '无线耳机 Elite',
+          children: [
+            {
+              name: '电子模块',
+              children: [
+                { name: '蓝牙模块\n库存: 580' },
+                { name: '锂电池组\n库存: 1200' }
+              ]
+            },
+            {
+              name: '结构件',
+              children: [
+                { name: '耳机壳体\n库存: 2600' },
+                { name: '充电仓\n库存: 1800' }
+              ]
+            },
+            {
+              name: '包装材料',
+              children: [
+                { name: '包装盒\n库存: 4200' },
+                { name: '说明书\n库存: 5100' }
+              ]
+            }
+          ]
+        },
+        speaker: {
+          name: '智能音箱',
+          children: [
+            {
+              name: '电子模块',
+              children: [
+                { name: '主控板\n库存: 900' },
+                { name: '蓝牙模块\n库存: 580' }
+              ]
+            },
+            {
+              name: '结构件',
+              children: [
+                { name: '金属外壳\n库存: 4500' },
+                { name: '喇叭组件\n库存: 1200' }
+              ]
+            },
+            {
+              name: '包装材料',
+              children: [
+                { name: '包装盒\n库存: 6000' },
+                { name: '说明书\n库存: 9200' }
+              ]
+            }
+          ]
+        }
+      },
+      stockChart: null,
+      usageChart: null,
+      bomChart: null
+    }
+  },
+  computed: {
+    totalItems() {
+      return this.semiProductList.length
+    }
+  },
+  mounted() {
+    this.fetchStats()
+    this.fetchAssemblyCapacities()
+    this.$nextTick(() => {
+      this.initCharts()
+      this.renderCharts()
+    })
+    window.addEventListener('resize', this.resizeCharts)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.resizeCharts)
+    if (this.stockChart) this.stockChart.dispose()
+    if (this.usageChart) this.usageChart.dispose()
+    if (this.bomChart) this.bomChart.dispose()
+  },
+  watch: {
+    selectedBomProduct() {
+      this.updateBomChart()
+    }
+  },
+  methods: {
+    normalizeResponse(res) {
+      if (!res) return null
+      if (res.code === 200) return res.data
+      if (res.data) return res.data
+      return null
+    },
+    async fetchAssemblyCapacities() {
+      try {
+        const res = await request({ url: '/api/semi-product/assembly-capacity', method: 'get' })
+        const data = this.normalizeResponse(res)
+        if (Array.isArray(data)) this.assemblyCapacities = data
+      } catch (e) {
+        console.error('获取可组装能力失败:', e)
+      }
+    },
+    async fetchStats() {
+      try {
+        const res = await request({ url: '/api/semi-product/statistics', method: 'get' })
+        const data = this.normalizeResponse(res)
+        if (data) this.stats = { ...this.stats, ...data }
+      } catch (e) {
+        console.error('获取半成品统计失败:', e)
+      }
+    },
+    initCharts() {
+      if (this.$refs.stockDistributionChart && !this.stockChart) {
+        this.stockChart = echarts.init(this.$refs.stockDistributionChart, 'macarons')
+      }
+      if (this.$refs.usageTrendChart && !this.usageChart) {
+        this.usageChart = echarts.init(this.$refs.usageTrendChart, 'macarons')
+      }
+      if (this.$refs.bomRelationChart && !this.bomChart) {
+        this.bomChart = echarts.init(this.$refs.bomRelationChart, 'macarons')
+      }
+    },
+    renderCharts() {
+      if (this.stockChart) this.stockChart.setOption(this.stockDistributionOption, true)
+      if (this.usageChart) this.usageChart.setOption(this.usageTrendOption, true)
+      this.updateBomChart()
+    },
+    buildBomOption() {
+      const data = this.bomDataMap[this.selectedBomProduct] || this.bomDataMap.watch
+      return {
+        tooltip: { trigger: 'item', triggerOn: 'mousemove' },
+        series: [
+          {
+            type: 'tree',
+            data: [data],
+            top: '5%',
+            left: '10%',
+            bottom: '5%',
+            right: '20%',
+            symbolSize: 10,
+            label: { position: 'left', verticalAlign: 'middle', align: 'right', fontSize: 12 },
+            leaves: { label: { position: 'right', verticalAlign: 'middle', align: 'left' } },
+            emphasis: { focus: 'descendant' },
+            expandAndCollapse: true,
+            animationDuration: 550,
+            animationDurationUpdate: 750
+          }
+        ]
+      }
+    },
+    updateBomChart() {
+      if (!this.bomChart) return
+      this.bomChart.setOption(this.buildBomOption(), true)
+    },
+    resizeCharts() {
+      if (this.stockChart) this.stockChart.resize()
+      if (this.usageChart) this.usageChart.resize()
+      if (this.bomChart) this.bomChart.resize()
+    },
+    getCategoryText(category) {
+      const map = {
+        electronics: '电子元件',
+        mechanics: '机械部件',
+        housing: '外壳模块',
+        pcb: '电路板'
+      }
+      return map[category] || '其他'
+    },
+    getStatusType(status) {
+      const map = {
+        'in-stock': 'success',
+        'in-production': 'warning',
+        pending: 'info',
+        used: 'danger'
+      }
+      return map[status] || 'info'
+    },
+    getStatusText(status) {
+      const map = {
+        'in-stock': '在库',
+        'in-production': '在制',
+        pending: '待检',
+        used: '已用'
+      }
+      return map[status] || '未知'
+    },
+    getTurnoverColor(days) {
+      if (days <= 20) return '#67c23a'
+      if (days <= 35) return '#e6a23c'
+      return '#f56c6c'
+    },
+    getProgressColor(rate) {
+      if (rate >= 80) return '#67c23a'
+      if (rate >= 60) return '#e6a23c'
+      return '#f56c6c'
+    },
+    getStockStatusType(status) {
+      const map = {
+        正常: 'success',
+        充足: 'success',
+        偏低: 'warning',
+        紧张: 'danger'
+      }
+      return map[status] || 'info'
+    },
+    handleSearch() {
+      Message.success('搜索成功')
+    },
+    handleReset() {
+      this.searchKeyword = ''
+      this.filterStatus = ''
+      this.filterCategory = ''
+      Message.info('已重置筛选条件')
+    },
+    handleAdd() {
+      Message.info('打开新增半成品对话框')
+    },
+    handleDetail(row) {
+      Message.info(`查看详情: ${row.name}`)
+    },
+    handleEdit(row) {
+      Message.info(`编辑: ${row.name}`)
+    },
+    handleAssemble(row) {
+      Message.info(`进入组装流程: ${row.name}`)
+    },
+    handleDelete(row) {
+      MessageBox.confirm(`确认删除半成品"${row.name}"?`, '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        Message.success('删除成功')
+      }).catch(() => {})
+    }
+  }
+}
 </script>
 
 <style scoped>
-  .semi-product {
-    width: 100%;
-  }
+.semi-product {
+  width: 100%;
+}
 
-  .overview-row {
-    margin-bottom: 20px;
-  }
+.overview-row {
+  margin-bottom: 20px;
+}
 
-  .stat-card {
-    cursor: pointer;
-    transition: all 0.3s;
-  }
+.stat-card {
+  cursor: pointer;
+  transition: all 0.3s;
+}
 
-  .stat-card:hover {
-    transform: translateY(-5px);
-    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-  }
+.stat-card:hover {
+  transform: translateY(-5px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
 
-  .stat-content {
-    display: flex;
-    align-items: center;
-    gap: 15px;
-  }
+.stat-content {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+}
 
-  .stat-icon {
-    width: 56px;
-    height: 56px;
-    border-radius: 12px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    color: #fff;
-  }
+.stat-icon {
+  width: 56px;
+  height: 56px;
+  border-radius: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #fff;
+}
 
-  .stat-info {
-    flex: 1;
-  }
+.stat-info {
+  flex: 1;
+}
 
-  .stat-label {
-    font-size: 14px;
-    color: #909399;
-    margin-bottom: 8px;
-  }
+.stat-label {
+  font-size: 14px;
+  color: #909399;
+  margin-bottom: 8px;
+}
 
-  .stat-value {
-    font-size: 28px;
-    font-weight: bold;
-    color: #303133;
-    line-height: 1;
-  }
+.stat-value {
+  font-size: 28px;
+  font-weight: bold;
+  color: #303133;
+  line-height: 1;
+}
 
-  .stat-unit {
-    font-size: 12px;
-    color: #909399;
-    margin-top: 4px;
-  }
+.stat-unit {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
 
-  .search-card {
-    margin-bottom: 20px;
-  }
+.search-card {
+  margin-bottom: 20px;
+}
 
-  .table-card {
-    margin-bottom: 20px;
-  }
+.table-card {
+  margin-bottom: 20px;
+}
 
-  .charts-row {
-    margin-bottom: 20px;
-  }
+.charts-row {
+  margin-bottom: 20px;
+}
 
-  .card-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-  }
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
 
-  .bom-card,
-  .turnover-card {
-    margin-bottom: 20px;
-  }
+.bom-card,
+.turnover-card {
+  margin-bottom: 20px;
+}
+
+.chart {
+  height: 300px;
+}
 </style>

+ 387 - 140
src/views/storage/cost/index.vue

@@ -1,15 +1,13 @@
-<template>
+<template>
   <div class="risk-warning">
-    <!-- 风险总览卡片 -->
     <el-row :gutter="20" class="risk-overview">
       <el-col :span="6">
         <el-card class="risk-card critical">
           <div class="risk-header">
-            <!-- Replace WarningFilled icon with symbol -->
-            <span style="font-size:32px; color:#f56c6c;">⚠️</span>
+            <i class="el-icon-warning" style="font-size: 32px; color: #f56c6c;"></i>
             <div class="risk-info">
               <div class="risk-label">高风险</div>
-              <div class="risk-count">-</div>
+              <div class="risk-count">{{ riskStats.critical }}</div>
             </div>
           </div>
         </el-card>
@@ -17,11 +15,10 @@
       <el-col :span="6">
         <el-card class="risk-card warning">
           <div class="risk-header">
-            <!-- Replace Warning icon with symbol -->
-            <span style="font-size:32px; color:#e6a23c;">⚠️</span>
+            <i class="el-icon-warning" style="font-size: 32px; color: #e6a23c;"></i>
             <div class="risk-info">
               <div class="risk-label">中风险</div>
-              <div class="risk-count">-</div>
+              <div class="risk-count">{{ riskStats.warning }}</div>
             </div>
           </div>
         </el-card>
@@ -29,11 +26,10 @@
       <el-col :span="6">
         <el-card class="risk-card info">
           <div class="risk-header">
-            <!-- Replace InfoFilled icon with symbol -->
-            <span style="font-size:32px; color:#409eff;">ℹ️</span>
+            <i class="el-icon-info" style="font-size: 32px; color: #409eff;"></i>
             <div class="risk-info">
               <div class="risk-label">低风险</div>
-              <div class="risk-count">-</div>
+              <div class="risk-count">{{ riskStats.info }}</div>
             </div>
           </div>
         </el-card>
@@ -41,95 +37,165 @@
       <el-col :span="6">
         <el-card class="risk-card safe">
           <div class="risk-header">
-            <!-- Replace SuccessFilled icon with symbol -->
-            <span style="font-size:32px; color:#67c23a;">✅</span>
+            <i class="el-icon-success" style="font-size: 32px; color: #67c23a;"></i>
             <div class="risk-info">
               <div class="risk-label">正常</div>
-              <div class="risk-count">-</div>
+              <div class="risk-count">{{ riskStats.safe }}</div>
             </div>
           </div>
         </el-card>
       </el-col>
     </el-row>
 
-    <!-- 风险列表与筛选 -->
     <el-card class="risk-list-card">
-      <template #header>
+      <template slot="header">
         <div class="card-header">
           <span>风险预警列表</span>
           <div class="filter-group">
-            <el-select placeholder="风险等级" size="default" style="width: 120px">
+            <el-select v-model="filterRiskLevel" placeholder="风险等级" size="default" style="width: 120px">
               <el-option label="全部" value="" />
               <el-option label="高风险" value="critical" />
               <el-option label="中风险" value="warning" />
               <el-option label="低风险" value="info" />
             </el-select>
-            <el-select placeholder="风险类型" size="default" style="width: 140px">
+            <el-select v-model="filterRiskType" placeholder="风险类型" size="default" style="width: 140px">
               <el-option label="全部" value="" />
               <el-option label="缺货风险" value="stockout" />
               <el-option label="超储风险" value="overstock" />
               <el-option label="滞销风险" value="slow-moving" />
               <el-option label="周转风险" value="turnover" />
             </el-select>
-            <el-button type="primary">
-              <!-- Replace Refresh icon with symbol -->
-              🔄 刷新
+            <el-button type="primary" @click="handleRefresh">
+              <i class="el-icon-refresh"></i> 刷新
             </el-button>
           </div>
         </div>
       </template>
 
-      <el-table stripe style="width: 100%" :loading="false">
-        <el-table-column label="编号" width="80" />
-        <el-table-column label="产品名称" width="200" />
-        <el-table-column label="产品编码" width="120" />
-        <el-table-column label="风险等级" width="120" />
-        <el-table-column label="风险类型" width="140" />
-        <el-table-column label="健康评分" width="150" />
-        <el-table-column label="周转率" width="100" />
-        <el-table-column label="风险评分" width="100" />
-        <el-table-column label="覆盖天数" width="110" />
-        <el-table-column label="当前库存" width="110" />
-        <el-table-column label="日均销量" width="110" />
-        <el-table-column label="风险描述" min-width="200" />
-        <el-table-column label="建议措施" min-width="200" />
-        <el-table-column label="检测时间" width="180" />
+      <el-table :data="riskTableData" stripe style="width: 100%" :loading="riskTableLoading">
+        <el-table-column prop="id" label="编号" width="80" />
+        <el-table-column prop="productName" label="产品名称" width="200" />
+        <el-table-column prop="productCode" label="产品编码" width="120" />
+        <el-table-column label="风险等级" width="120">
+          <template slot-scope="scope">
+            <el-tag :type="getRiskLevelType(scope.row.riskLevel)" effect="dark">
+              {{ getRiskLevelText(scope.row.riskLevel) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="风险类型" width="140">
+          <template slot-scope="scope">
+            <el-tag :color="getRiskTypeColor(scope.row.riskType)" effect="dark">
+              {{ getRiskTypeText(scope.row.riskType) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="健康评分" width="150">
+          <template slot-scope="scope">
+            <el-popover placement="top" trigger="hover" width="260">
+              <template slot="reference">
+                <div class="health-cell">
+                  <span class="health-value">{{ scope.row.healthScore !== undefined ? scope.row.healthScore : '-' }}</span>
+                  <el-progress
+                    v-if="scope.row.healthScore !== undefined"
+                    :percentage="scope.row.healthScore"
+                    :stroke-width="6"
+                    :color="getHealthColor(scope.row.healthScore)"
+                  />
+                </div>
+              </template>
+              <div v-if="scope.row.scoreBreakdown" class="score-breakdown">
+                <div class="score-line">
+                  <span>覆盖天数</span>
+                  <b>{{ scope.row.scoreBreakdown.coverage }}</b>
+                </div>
+                <div class="score-line">
+                  <span>周转贡献</span>
+                  <b>{{ scope.row.scoreBreakdown.turnover }}</b>
+                </div>
+                <div class="score-line">
+                  <span>销量趋势</span>
+                  <b>{{ scope.row.scoreBreakdown.trend }}</b>
+                </div>
+                <div class="score-line">
+                  <span>资金占比</span>
+                  <b>{{ scope.row.scoreBreakdown.capital }}</b>
+                </div>
+              </div>
+            </el-popover>
+          </template>
+        </el-table-column>
+        <el-table-column label="周转率" width="100">
+          <template slot-scope="scope">
+            {{ scope.row.turnoverRate !== undefined ? scope.row.turnoverRate.toFixed(2) : '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="riskScore" label="风险评分" width="100" sortable>
+          <template slot-scope="scope">
+            <span :style="{ color: getRiskScoreColor(scope.row.riskScore) }">
+              {{ scope.row.riskScore }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="覆盖天数" width="110">
+          <template slot-scope="scope">
+            {{ formatCoverage(scope.row.coverageDays) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="inventory" label="当前库存" width="110" />
+        <el-table-column label="日均销量" width="110">
+          <template slot-scope="scope">
+            {{ scope.row.avgDailySales !== undefined ? scope.row.avgDailySales.toFixed(1) : '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="description" label="风险描述" min-width="200" />
+        <el-table-column prop="suggestion" label="建议措施" min-width="200" />
+        <el-table-column prop="detectedTime" label="检测时间" width="180" />
       </el-table>
 
       <el-pagination
-        :current-page="1"
-        :page-size="10"
-        :total="0"
+        :current-page="currentPage"
+        :page-size="pageSize"
+        :total="totalRisks"
         :page-sizes="[10, 20, 50, 100]"
         layout="total, sizes, prev, pager, next, jumper"
         style="margin-top: 20px; justify-content: flex-end;"
+        @current-change="handleCurrentChange"
+        @size-change="handleSizeChange"
       />
     </el-card>
 
-    <!-- 智能预警规则配置 -->
     <el-card class="rule-card">
-      <template #header>
+      <template slot="header">
         <div class="card-header">
           <span>智能预警规则配置</span>
-          <el-button type="primary" size="small">
-            <!-- Replace Plus icon with symbol -->
-            ➕ 添加规则
+          <el-button type="primary" size="small" @click="handleAddRule">
+            <i class="el-icon-plus"></i> 添加规则
           </el-button>
         </div>
       </template>
 
-      <el-table stripe>
-        <el-table-column label="规则名称" width="200" />
-        <el-table-column label="触发条件" min-width="300" />
-        <el-table-column label="风险等级" width="120" />
-        <el-table-column label="状态" width="100" />
-        <el-table-column label="触发次数" width="100" />
+      <el-table :data="warningRules" stripe>
+        <el-table-column prop="name" label="规则名称" width="200" />
+        <el-table-column prop="condition" label="触发条件" min-width="300" />
+        <el-table-column label="风险等级" width="120">
+          <template slot-scope="scope">
+            <el-tag :type="getRiskLevelType(scope.row.level)">
+              {{ getRiskLevelText(scope.row.level) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" width="100">
+          <template slot-scope="scope">
+            <el-switch v-model="scope.row.enabled" @change="handleRuleToggle(scope.row)" />
+          </template>
+        </el-table-column>
+        <el-table-column prop="triggerCount" label="触发次数" width="100" />
       </el-table>
     </el-card>
 
-    <!-- 反馈优化建议 -->
     <el-card class="feedback-card">
-      <template #header>
+      <template slot="header">
         <div class="card-header">
           <span>系统优化建议与反馈</span>
           <el-tag type="success">智能分析</el-tag>
@@ -138,13 +204,19 @@
 
       <el-timeline>
         <el-timeline-item
-          timestamp="-"
+          v-for="(feedback, index) in optimizationFeedback"
+          :key="index"
+          :timestamp="feedback.time"
+          :type="feedback.type"
           placement="top"
         >
           <el-card>
-            <h4>暂无数据</h4>
-            <p class="feedback-content">暂无系统优化建议与反馈信息</p>
+            <h4>{{ feedback.title }}</h4>
+            <p class="feedback-content">{{ feedback.content }}</p>
             <div class="feedback-actions">
+              <el-tag v-for="tag in feedback.tags" :key="tag" size="small" style="margin-right: 8px">
+                {{ tag }}
+              </el-tag>
               <el-button type="text" size="small">查看详情</el-button>
             </div>
           </el-card>
@@ -154,109 +226,284 @@
   </div>
 </template>
 
-<script setup>
-  // REMOVED ALL ICON IMPORTS (no @element-plus/icons-vue dependency)
+<script>
+import request from '@/utils/request'
+import { Message, MessageBox } from 'element-ui'
+
+export default {
+  name: 'StorageRiskWarning',
+  data() {
+    return {
+      filterRiskLevel: '',
+      filterRiskType: '',
+      currentPage: 1,
+      pageSize: 10,
+      riskStats: {
+        critical: 0,
+        warning: 0,
+        info: 0,
+        safe: 0
+      },
+      riskTableData: [],
+      totalRisks: 0,
+      riskTableLoading: false,
+      warningRules: [],
+      optimizationFeedback: []
+    }
+  },
+  mounted() {
+    this.loadAll()
+  },
+  watch: {
+    filterRiskLevel() {
+      this.refreshList()
+    },
+    filterRiskType() {
+      this.refreshList()
+    }
+  },
+  methods: {
+    normalizeResponse(res) {
+      if (!res) return null
+      if (res.code === 200) return res.data
+      if (res.data) return res.data
+      return null
+    },
+    async loadAll() {
+      await Promise.all([
+        this.fetchStats(),
+        this.refreshList(),
+        this.fetchWarningRules(),
+        this.fetchFeedback()
+      ])
+    },
+    async fetchStats() {
+      try {
+        const res = await request({ url: '/api/risk/statistics', method: 'get' })
+        const data = this.normalizeResponse(res)
+        if (data) this.riskStats = { ...this.riskStats, ...data }
+      } catch (e) {
+        console.error('获取风险统计失败:', e)
+      }
+    },
+    async refreshList() {
+      this.riskTableLoading = true
+      try {
+        const res = await request({
+          url: '/api/risk/list',
+          method: 'get',
+          params: {
+            page: this.currentPage,
+            pageSize: this.pageSize,
+            riskLevel: this.filterRiskLevel,
+            riskType: this.filterRiskType
+          }
+        })
+        const data = this.normalizeResponse(res)
+        if (data) {
+          this.riskTableData = data.list || data.rows || []
+          this.totalRisks = data.total || this.riskTableData.length
+        }
+      } catch (e) {
+        console.error('获取风险列表失败:', e)
+      } finally {
+        this.riskTableLoading = false
+      }
+    },
+    async fetchWarningRules() {
+      try {
+        const res = await request({ url: '/api/risk/rules', method: 'get' })
+        const data = this.normalizeResponse(res)
+        if (Array.isArray(data)) this.warningRules = data
+      } catch (e) {
+        console.error('获取预警规则失败:', e)
+      }
+    },
+    async fetchFeedback() {
+      try {
+        const res = await request({ url: '/api/risk/feedback', method: 'get' })
+        const data = this.normalizeResponse(res)
+        if (Array.isArray(data)) this.optimizationFeedback = data
+      } catch (e) {
+        console.error('获取反馈失败:', e)
+      }
+    },
+    handleRefresh() {
+      this.currentPage = 1
+      this.loadAll()
+      Message.success('数据已刷新')
+    },
+    handleCurrentChange(page) {
+      this.currentPage = page
+      this.refreshList()
+    },
+    handleSizeChange(size) {
+      this.pageSize = size
+      this.currentPage = 1
+      this.refreshList()
+    },
+    getRiskLevelType(level) {
+      const map = { critical: 'danger', warning: 'warning', info: 'info', safe: 'success' }
+      return map[level] || 'info'
+    },
+    getRiskLevelText(level) {
+      const map = { critical: '高风险', warning: '中风险', info: '低风险', safe: '正常' }
+      return map[level] || '未知'
+    },
+    getRiskTypeColor(type) {
+      const map = {
+        stockout: '#f56c6c',
+        overstock: '#e6a23c',
+        'slow-moving': '#909399',
+        turnover: '#67c23a',
+        balanced: '#409eff'
+      }
+      return map[type] || '#909399'
+    },
+    getRiskTypeText(type) {
+      const map = {
+        stockout: '缺货风险',
+        overstock: '超储风险',
+        'slow-moving': '滞销风险',
+        turnover: '周转风险',
+        balanced: '均衡'
+      }
+      return map[type] || '未知'
+    },
+    getRiskScoreColor(score) {
+      if (score >= 80) return '#f56c6c'
+      if (score >= 60) return '#e6a23c'
+      return '#409eff'
+    },
+    getHealthColor(score) {
+      if (score >= 80) return '#67c23a'
+      if (score >= 60) return '#e6a23c'
+      return '#f56c6c'
+    },
+    formatCoverage(value) {
+      if (value === undefined || value === null) return '-'
+      if (value >= 9000) return '∞'
+      return `${Number(value).toFixed(1)} 天`
+    },
+    handleAddRule() {
+      Message.info('添加预警规则')
+    },
+    handleRuleToggle(row) {
+      const status = row.enabled ? '启用' : '禁用'
+      Message.success(`规则已${status}`)
+    },
+    handleDetail(row) {
+      Message.info(`查看风险详情: ${row.productCode}`)
+    },
+    handleResolve(row) {
+      MessageBox.confirm('确认已处理该风险?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        Message.success('已标记为已处理')
+      }).catch(() => {})
+    }
+  }
+}
 </script>
 
 <style scoped>
-  .risk-warning {
-    width: 100%;
-  }
+.risk-warning {
+  width: 100%;
+}
 
-  .risk-overview {
-    margin-bottom: 20px;
-  }
+.risk-overview {
+  margin-bottom: 20px;
+}
 
-  .risk-card {
-    cursor: pointer;
-    transition: all 0.3s;
-  }
+.risk-card {
+  cursor: pointer;
+  transition: all 0.3s;
+}
 
-  .risk-card:hover {
-    transform: translateY(-5px);
-    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-  }
+.risk-card:hover {
+  transform: translateY(-5px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
 
-  .risk-header {
-    display: flex;
-    align-items: center;
-    gap: 15px;
-  }
+.risk-header {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+}
 
-  .risk-info {
-    flex: 1;
-  }
+.risk-info {
+  flex: 1;
+}
 
-  .risk-label {
-    font-size: 14px;
-    color: #909399;
-    margin-bottom: 8px;
-  }
+.risk-label {
+  font-size: 14px;
+  color: #909399;
+  margin-bottom: 8px;
+}
 
-  .risk-count {
-    font-size: 32px;
-    font-weight: bold;
-    color: #303133;
-  }
+.risk-count {
+  font-size: 32px;
+  font-weight: bold;
+  color: #303133;
+}
 
-  .risk-list-card {
-    margin-bottom: 20px;
-  }
+.risk-list-card {
+  margin-bottom: 20px;
+}
 
-  .card-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-  }
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
 
-  .filter-group {
-    display: flex;
-    gap: 10px;
-  }
+.filter-group {
+  display: flex;
+  gap: 10px;
+}
 
-  .charts-row {
-    margin-bottom: 20px;
-  }
+.rule-card,
+.feedback-card {
+  margin-bottom: 20px;
+}
 
-  .rule-card,
-  .feedback-card {
-    margin-bottom: 20px;
-  }
+.feedback-content {
+  color: #606266;
+  font-size: 14px;
+  margin: 10px 0;
+  line-height: 1.6;
+}
 
-  .feedback-content {
-    color: #606266;
-    font-size: 14px;
-    margin: 10px 0;
-    line-height: 1.6;
-  }
+.feedback-actions {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-top: 10px;
+}
 
-  .feedback-actions {
-    display: flex;
-    align-items: center;
-    gap: 10px;
-    margin-top: 10px;
-  }
+.health-cell {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
 
-  .health-cell {
-    display: flex;
-    flex-direction: column;
-    gap: 4px;
-  }
+.health-value {
+  font-weight: 600;
+  color: #303133;
+}
 
-  .health-value {
-    font-weight: 600;
-    color: #303133;
-  }
+.score-breakdown {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
 
-  .score-breakdown {
-    display: flex;
-    flex-direction: column;
-    gap: 6px;
-  }
-  .score-line {
-    display: flex;
-    justify-content: space-between;
-    font-size: 12px;
-    color: #606266;
-  }
+.score-line {
+  display: flex;
+  justify-content: space-between;
+  font-size: 12px;
+  color: #606266;
+}
 </style>

+ 491 - 158
src/views/storage/efficiency/index.vue

@@ -1,185 +1,518 @@
-<template>
-  <div class="inventory-overview">
-    <div class="product-analysis">
-      <!-- 搜索框 -->
-      <div class="search-bar">
-        <input
-          v-model="sku"
-          placeholder="输入SKU查询"
-          @keyup.enter="search"
-        >
-        <button @click="search" :disabled="loading">
-          {{ loading ? '查询中...' : '查询' }}
-        </button>
-      </div>
-    </div>
+<template>
+  <div class="product-analysis">
+    <el-card class="search-card">
+      <el-row :gutter="20" align="middle">
+        <el-col :span="12">
+          <el-input
+            v-model="skuInput"
+            placeholder="请输入产品SKU(如:E06D01AS1)"
+            size="medium"
+            clearable
+            @keyup.enter.native="searchProduct"
+          >
+            <template slot="prepend">
+              <i class="el-icon-search"></i>
+            </template>
+          </el-input>
+        </el-col>
+        <el-col :span="4">
+          <el-button type="primary" size="medium" @click="searchProduct" :loading="loading">
+            查询分析
+          </el-button>
+        </el-col>
+        <el-col :span="8">
+          <div class="product-info" v-if="productData">
+            <el-tag>SKU: {{ skuInput }}</el-tag>
+          </div>
+        </el-col>
+      </el-row>
+    </el-card>
 
-    <!-- SKU指标汇总表格 - 空表格 -->
-    <el-row :gutter="20" class="charts-row">
-      <el-col :span="24">
-        <el-card>
-          <template #header>
-            <div class="card-header">
-              <span>SKU指标汇总</span>
-              <el-button type="primary" size="small" @click="handleRefresh">
-                <span style="margin-right: 4px;">🔄</span> 刷新
-              </el-button>
+    <div v-if="productData">
+      <el-row :gutter="20" class="metrics-row">
+        <el-col :span="6">
+          <el-card class="metric-card">
+            <div class="metric-item">
+              <div class="metric-label">入库总量</div>
+              <div class="metric-value">{{ formatInteger(productData.purchaseQty) }}</div>
+              <div class="metric-unit">件</div>
+            </div>
+          </el-card>
+        </el-col>
+        <el-col :span="6">
+          <el-card class="metric-card">
+            <div class="metric-item">
+              <div class="metric-label">销售总量</div>
+              <div class="metric-value">{{ formatInteger(productData.salesQty) }}</div>
+              <div class="metric-unit">件</div>
+            </div>
+          </el-card>
+        </el-col>
+        <el-col :span="6">
+          <el-card class="metric-card">
+            <div class="metric-item">
+              <div class="metric-label">当前库存</div>
+              <div class="metric-value">{{ formatInteger(productData.currentInventory) }}</div>
+              <div class="metric-unit">件</div>
             </div>
-          </template>
-          <el-table :data="[]" stripe style="width: 100%" v-loading="false">
-            <el-table-column prop="sku" label="SKU" width="200" fixed />
-            <el-table-column prop="attribute" label="属性" width="140">
-              <template #default>—</template>
-            </el-table-column>
-            <el-table-column prop="spuName" label="SPU" width="200">
-              <template #default>—</template>
-            </el-table-column>
-            <el-table-column prop="purchaseQty" label="入库数量" width="120" align="right">
-              <template #default>—</template>
-            </el-table-column>
-            <el-table-column prop="salesQty" label="销售数量" width="120" align="right">
-              <template #default>—</template>
-            </el-table-column>
-            <el-table-column prop="inventory" label="现有库存" width="120" align="right">
-              <template #default>—</template>
-            </el-table-column>
-            <el-table-column prop="purchaseAmount" label="入库总资金" width="150" align="right">
-              <template #default>—</template>
-            </el-table-column>
-            <el-table-column prop="amountRatio" label="入库资金占比(%)" width="150" align="right">
-              <template #default>—</template>
-            </el-table-column>
-            <el-table-column prop="turnoverRate" label="库存周转率" align="right">
-              <template #default>
-                <el-tag type="info">—</el-tag>
-              </template>
-            </el-table-column>
-          </el-table>
-        </el-card>
-      </el-col>
-    </el-row>
+          </el-card>
+        </el-col>
+        <el-col :span="6">
+          <el-card class="metric-card">
+            <div class="metric-item">
+              <div class="metric-label">周转率</div>
+              <div class="metric-value">{{ productData.turnoverRate }}</div>
+              <div class="metric-unit">次</div>
+            </div>
+          </el-card>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20" class="turnover-row" v-if="turnoverBreakdown.length">
+        <el-col :span="24">
+          <el-card>
+            <template slot="header">
+              <div class="card-header">
+                <span>分段周转率</span>
+              </div>
+            </template>
+            <el-table :data="turnoverBreakdown" border size="small">
+              <el-table-column label="统计区间" width="220">
+                <template slot-scope="scope">
+                  <div class="turnover-period-label">近{{ scope.row.windowDays }}天</div>
+                  <div class="turnover-period-range">{{ scope.row.start }} ~ {{ scope.row.end }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="销售出库(件)" width="160">
+                <template slot-scope="scope">
+                  {{ formatInteger(scope.row.salesSum) }}
+                </template>
+              </el-table-column>
+              <el-table-column label="时间加权平均库存(件)" width="220">
+                <template slot-scope="scope">
+                  {{ formatInventory(scope.row.avgInventory) }}
+                </template>
+              </el-table-column>
+              <el-table-column label="周转率(次)">
+                <template slot-scope="scope">
+                  {{ formatTurnover(scope.row.turnover) }}
+                </template>
+              </el-table-column>
+            </el-table>
+          </el-card>
+        </el-col>
+      </el-row>
 
+      <el-row :gutter="20" class="charts-row">
+        <el-col :span="24">
+          <el-card>
+            <template slot="header">
+              <div class="card-header">
+                <span>单品库存变化趋势(入库/销售/库存)</span>
+              </div>
+            </template>
+            <div ref="trendChart" class="trend-chart" />
+          </el-card>
+        </el-col>
+      </el-row>
 
+      <el-row :gutter="20" class="insights-row">
+        <el-col :span="12">
+          <el-card>
+            <template slot="header">
+              <div class="card-header">
+                <span>30天预测摘要</span>
+              </div>
+            </template>
+            <div v-if="forecastEnding !== null" class="forecast-meta">
+              <div class="forecast-value">{{ Math.round(forecastEnding).toLocaleString() }} 件</div>
+              <div class="forecast-delta" :class="{ positive: (forecastDelta || 0) >= 0, negative: (forecastDelta || 0) < 0 }">
+                {{ forecastDelta >= 0 ? '+' : '' }}{{ Math.round(forecastDelta || 0).toLocaleString() }} 件 vs 当前库存
+              </div>
+            </div>
+            <el-empty v-else description="暂无预测数据" :image-size="120" />
+          </el-card>
+        </el-col>
+        <el-col :span="12" v-if="lifecycleSegments.length">
+          <el-card>
+            <template slot="header">
+              <div class="card-header">
+                <span>生命周期分段</span>
+              </div>
+            </template>
+            <div class="segment-timeline">
+              <div
+                v-for="seg in lifecycleSegments"
+                :key="seg.start + seg.end"
+                class="segment-chip"
+                :style="{ background: seg.color || stageColorMap[seg.name] || 'rgba(160,160,160,0.18)' }"
+              >
+                <div class="segment-name">{{ seg.name }}</div>
+                <div class="segment-range">{{ seg.start }} ~ {{ seg.end }}</div>
+                <div class="segment-score">阶段指数 {{ formatTurnover(seg.score) }}</div>
+              </div>
+            </div>
+          </el-card>
+        </el-col>
+      </el-row>
+    </div>
+
+    <el-empty v-else description="请输入产品SKU进行查询分析" :image-size="200" />
   </div>
 </template>
 
-<script setup>
-  // 完全移除所有图标依赖,仅保留空的刷新方法
-  const handleRefresh = () => {
-    // 后续可添加数据加载逻辑
+<script>
+import request from '@/utils/request'
+import { Message } from 'element-ui'
+import * as echarts from 'echarts'
+require('echarts/theme/macarons')
+
+export default {
+  name: 'StorageEfficiency',
+  data() {
+    return {
+      skuInput: '',
+      productData: null,
+      loading: false,
+      trendChartInstance: null,
+      stageColorMap: {
+        '引入期': 'rgba(64, 158, 255, 0.12)',
+        '成长期': 'rgba(103, 194, 58, 0.18)',
+        '成熟期': 'rgba(250, 200, 88, 0.18)',
+        '衰退期': 'rgba(245, 108, 108, 0.18)'
+      }
+    }
+  },
+  computed: {
+    lifecycleSegments() {
+      return (this.productData && this.productData.lifecycleSegments) || []
+    },
+    turnoverBreakdown() {
+      return (this.productData && this.productData.turnoverBreakdown) || []
+    },
+    forecastEnding() {
+      if (!this.productData || !Array.isArray(this.productData.forecastInventory)) return null
+      const future = this.productData.forecastInventory.filter(v => v !== null && v !== undefined)
+      if (!future.length) return null
+      return future[future.length - 1]
+    },
+    forecastDelta() {
+      if (this.forecastEnding === null) return null
+      const current = (this.productData && this.productData.currentInventory) || 0
+      return this.forecastEnding - current
+    }
+  },
+  mounted() {
+    window.addEventListener('resize', this.resizeChart)
+  },
+  activated() {
+    if (this.productData) {
+      this.$nextTick(() => {
+        this.initTrendChart()
+        this.updateTrendChart()
+      })
+    }
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.resizeChart)
+    if (this.trendChartInstance) {
+      this.trendChartInstance.dispose()
+      this.trendChartInstance = null
+    }
+  },
+  methods: {
+    formatInteger(value) {
+      return Number(value || 0).toLocaleString()
+    },
+    formatInventory(value) {
+      return Number(value || 0).toLocaleString(undefined, { maximumFractionDigits: 1 })
+    },
+    formatTurnover(value) {
+      return Number(value || 0).toFixed(2)
+    },
+    initTrendChart() {
+      if (this.trendChartInstance || !this.$refs.trendChart) return
+      this.trendChartInstance = echarts.init(this.$refs.trendChart, 'macarons')
+    },
+    resizeChart() {
+      if (this.trendChartInstance) this.trendChartInstance.resize()
+    },
+    buildMarkAreas(segments) {
+      return segments.map(seg => {
+        const label = seg.score !== undefined ? `${seg.name} (${Number(seg.score).toFixed(2)})` : seg.name
+        return [
+          {
+            xAxis: seg.start,
+            name: label,
+            itemStyle: { color: seg.color || this.stageColorMap[seg.name] || 'rgba(160,160,160,0.25)' },
+            label: { color: '#606266' }
+          },
+          { xAxis: seg.end || seg.start }
+        ]
+      })
+    },
+    updateTrendChart() {
+      if (!this.trendChartInstance || !this.productData) return
+      const segments = this.lifecycleSegments
+      const option = {
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { type: 'cross' }
+        },
+        legend: {
+          data: ['入库', '销售', '当前库存', '稳定库存', '预测库存(30天)']
+        },
+        grid: {
+          left: '3%',
+          right: '4%',
+          bottom: '3%',
+          containLabel: true
+        },
+        xAxis: { type: 'category', data: this.productData.dates || [] },
+        yAxis: { type: 'value', name: '数量 (件)' },
+        series: [
+          {
+            name: '入库',
+            type: 'bar',
+            data: this.productData.purchaseByDay || [],
+            itemStyle: { color: '#5470c6' }
+          },
+          {
+            name: '销售',
+            type: 'bar',
+            data: this.productData.salesByDay || [],
+            itemStyle: { color: '#91cc75' }
+          },
+          {
+            name: '当前库存',
+            type: 'line',
+            data: this.productData.inventoryByDay || [],
+            itemStyle: { color: '#fac858' },
+            lineStyle: { width: 3 },
+            smooth: true,
+            markArea: {
+              itemStyle: { color: 'rgba(160,160,160,0.25)' },
+              label: { show: true, color: '#606266' },
+              silent: false,
+              data: this.buildMarkAreas(segments)
+            }
+          },
+          {
+            name: '稳定库存',
+            type: 'line',
+            data: this.productData.stableInventory || [],
+            itemStyle: { color: '#73c0de' },
+            lineStyle: { width: 2, type: 'dashed' },
+            smooth: true
+          },
+          {
+            name: '预测库存(30天)',
+            type: 'line',
+            data: this.productData.forecastInventory || [],
+            itemStyle: { color: '#ee6666' },
+            lineStyle: { width: 2 },
+            smooth: true
+          }
+        ]
+      }
+      this.trendChartInstance.setOption(option, true)
+    },
+    normalizeResponse(res) {
+      if (!res) return null
+      if (res.code === 200) return res.data
+      if (res.data) return res.data
+      return null
+    },
+    async fetchProductTrend(sku) {
+      try {
+        const res = await request({ url: `/api/product/trend/${sku}`, method: 'get' })
+        return this.normalizeResponse(res)
+      } catch (e) {
+        try {
+          const res = await request({
+            url: '/api/inventory/product-trend',
+            method: 'get',
+            params: { sku }
+          })
+          return this.normalizeResponse(res)
+        } catch (err) {
+          throw err
+        }
+      }
+    },
+    async searchProduct() {
+      const sku = this.skuInput.trim()
+      if (!sku) {
+        Message.warning('请输入产品SKU')
+        return
+      }
+
+      this.loading = true
+      try {
+        const data = await this.fetchProductTrend(sku)
+        if (data) {
+          this.productData = data
+          this.$nextTick(() => {
+            this.initTrendChart()
+            this.updateTrendChart()
+            this.resizeChart()
+          })
+          if (data.purchaseQty === 0 && data.salesQty === 0) {
+            Message.info(`SKU "${sku}" 没有找到相关数据`)
+          }
+        } else {
+          this.productData = null
+        }
+      } catch (error) {
+        console.error('查询产品失败:', error)
+        Message.error('查询失败,请稍后重试')
+        this.productData = null
+      } finally {
+        this.loading = false
+      }
+    }
   }
+}
 </script>
 
 <style scoped>
-  .product-analysis {
-    width: 90%;
-    max-width: 1200px;
-    margin: 20px auto;
-    font-family: Arial, sans-serif;
-  }
+.product-analysis {
+  width: 100%;
+}
 
-  /* 搜索栏 */
-  .search-bar {
-    margin-bottom: 20px;
-    display: flex;
-    gap: 10px;
-  }
-  .search-bar input {
-    flex: 1;
-    padding: 8px 12px;
-    border: 1px solid #ddd;
-    border-radius: 4px;
-  }
-  .search-bar button {
-    padding: 8px 20px;
-    border: none;
-    background: #409eff;
-    color: #fff;
-    border-radius: 4px;
-    cursor: pointer;
-  }
-  .search-bar button:disabled {
-    background: #a0cfff;
-    cursor: not-allowed;
-  }
+.search-card {
+  margin-bottom: 20px;
+}
 
-  /* 空状态 */
-  .empty {
-    text-align: center;
-    padding: 50px 0;
-    color: #999;
-  }
+.product-info {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+}
 
-  /* 分析内容 */
-  .analysis-content {
-    display: grid;
-    gap: 20px;
-  }
+.metrics-row {
+  margin-bottom: 20px;
+}
 
-  .inventory-overview {
-    width: 100%;
-  }
+.turnover-row {
+  margin-bottom: 20px;
+}
 
-  .metrics-row {
-    margin-bottom: 20px;
-  }
+.turnover-period-label {
+  font-weight: 600;
+  color: #303133;
+}
 
-  .metric-card {
-    cursor: pointer;
-    transition: transform 0.3s;
-  }
+.turnover-period-range {
+  font-size: 12px;
+  color: #909399;
+}
 
-  .metric-card:hover {
-    transform: translateY(-5px);
-    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-  }
+.metric-card {
+  text-align: center;
+  cursor: pointer;
+  transition: all 0.3s;
+}
 
-  .metric-content {
-    display: flex;
-    align-items: center;
-    gap: 15px;
-  }
+.metric-card:hover {
+  transform: translateY(-5px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
 
-  .metric-icon {
-    width: 60px;
-    height: 60px;
-    border-radius: 12px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    color: #fff;
-  }
+.metric-item {
+  padding: 10px;
+}
 
-  .metric-info {
-    flex: 1;
-  }
+.metric-label {
+  font-size: 14px;
+  color: #909399;
+  margin-bottom: 10px;
+}
 
-  .metric-label {
-    font-size: 14px;
-    color: #909399;
-    margin-bottom: 8px;
-  }
+.metric-value {
+  font-size: 32px;
+  font-weight: bold;
+  color: #303133;
+  margin-bottom: 5px;
+}
 
-  .metric-value {
-    font-size: 28px;
-    font-weight: bold;
-    color: #303133;
-    line-height: 1;
-  }
+.metric-unit {
+  font-size: 12px;
+  color: #909399;
+}
 
-  .metric-unit {
-    font-size: 12px;
-    color: #909399;
-    margin-top: 4px;
-  }
+.charts-row {
+  margin-bottom: 20px;
+}
 
-  .charts-row {
-    margin-bottom: 20px;
-  }
+.insights-row {
+  margin-bottom: 20px;
+}
 
-  .card-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-  }
+.forecast-meta {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  padding: 10px 0;
+}
+
+.forecast-value {
+  font-size: 34px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.forecast-delta {
+  font-size: 14px;
+  font-weight: 500;
+  color: #606266;
+}
+
+.forecast-delta.positive {
+  color: #67c23a;
+}
+
+.forecast-delta.negative {
+  color: #f56c6c;
+}
+
+.segment-timeline {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+}
+
+.segment-chip {
+  padding: 12px 14px;
+  border-radius: 10px;
+  color: #303133;
+  min-width: 180px;
+  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.05);
+}
+
+.segment-name {
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.segment-range {
+  font-size: 12px;
+  color: #606266;
+  margin: 6px 0;
+}
+
+.segment-score {
+  font-size: 12px;
+  color: #909399;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.trend-chart {
+  height: 400px;
+}
 </style>

+ 295 - 108
src/views/storage/overview/index.vue

@@ -1,17 +1,17 @@
-<template>
+<template>
   <div class="inventory-overview">
-    <!-- 关键指标卡片 - 纯文本替代图标 -->
     <el-row :gutter="20" class="metrics-row">
       <el-col :span="6">
         <el-card class="metric-card">
           <div class="metric-content">
             <div class="metric-icon" style="background-color: #409eff;">
-              <span style="font-size: 30px; color: #fff;">📦</span>
+              <i class="el-icon-box"></i>
             </div>
             <div class="metric-info">
               <div class="metric-label">总体库存量</div>
-              <div class="metric-value">-</div>
+              <div class="metric-value">{{ overviewData.totalInventory }}</div>
               <div class="metric-unit">件</div>
+              <div class="metric-sub">含组装转入 {{ overviewData.assemblyQty || 0 }} 件</div>
             </div>
           </div>
         </el-card>
@@ -20,11 +20,11 @@
         <el-card class="metric-card">
           <div class="metric-content">
             <div class="metric-icon" style="background-color: #67c23a;">
-              <span style="font-size: 30px; color: #fff;">💰</span>
+              <i class="el-icon-money"></i>
             </div>
             <div class="metric-info">
               <div class="metric-label">库存价值</div>
-              <div class="metric-value">-</div>
+              <div class="metric-value">{{ overviewData.totalValue }}</div>
               <div class="metric-unit">万元</div>
             </div>
           </div>
@@ -34,12 +34,12 @@
         <el-card class="metric-card">
           <div class="metric-content">
             <div class="metric-icon" style="background-color: #e6a23c;">
-              <span style="font-size: 30px; color: #fff;">🔄</span>
+              <i class="el-icon-refresh"></i>
             </div>
             <div class="metric-info">
               <div class="metric-label">库存周转率</div>
-              <div class="metric-value">-</div>
-              <div class="metric-unit"></div>
+              <div class="metric-value">{{ overviewData.turnoverRate }}</div>
+              <div class="metric-unit"></div>
             </div>
           </div>
         </el-card>
@@ -48,11 +48,11 @@
         <el-card class="metric-card">
           <div class="metric-content">
             <div class="metric-icon" style="background-color: #f56c6c;">
-              <span style="font-size: 30px; color: #fff;">🚚</span>
+              <i class="el-icon-truck"></i>
             </div>
             <div class="metric-info">
-              <div class="metric-label">在途库存比</div>
-              <div class="metric-value">-</div>
+              <div class="metric-label">在途库存比</div>
+              <div class="metric-value">{{ overviewData.inTransitRatio }}</div>
               <div class="metric-unit">%</div>
             </div>
           </div>
@@ -60,58 +60,71 @@
       </el-col>
     </el-row>
 
-    <!-- 每月入库/销售/库存对比图 - 空容器 -->
     <el-row :gutter="20" class="charts-row">
       <el-col :span="24">
         <el-card>
-          <template #header>
+          <template slot="header">
             <span>入库/销售/库存对比</span>
           </template>
-          <div style="height: 300px; display: flex; align-items: center; justify-content: center; color: #909399;">
-            <div>暂无数据</div>
-          </div>
+          <div ref="inventoryComparisonChart" class="chart" />
+          <el-empty v-if="!monthlyLoading && monthlyComparison.length === 0" description="暂无数据" />
         </el-card>
       </el-col>
     </el-row>
 
-    <!-- SKU指标汇总表格 - 空表格 -->
     <el-row :gutter="20" class="charts-row">
       <el-col :span="24">
         <el-card>
-          <template #header>
+          <template slot="header">
             <div class="card-header">
               <span>SKU指标汇总</span>
-              <el-button type="primary" size="small" @click="handleRefresh">
-                <span style="margin-right: 4px;">🔄</span> 刷新
+              <el-button type="primary" size="small" @click="refreshData">
+                <i class="el-icon-refresh"></i> 刷新
               </el-button>
             </div>
           </template>
-          <el-table :data="[]" stripe style="width: 100%" v-loading="false">
+          <el-table :data="skuTableData" stripe style="width: 100%" v-loading="loading">
             <el-table-column prop="sku" label="SKU" width="200" fixed />
             <el-table-column prop="attribute" label="属性" width="140">
-              <template #default>—</template>
+              <template slot-scope="scope">
+                {{ scope.row.attribute || '-' }}
+              </template>
             </el-table-column>
             <el-table-column prop="spuName" label="SPU" width="200">
-              <template #default>—</template>
+              <template slot-scope="scope">
+                {{ scope.row.spuName || '-' }}
+              </template>
             </el-table-column>
             <el-table-column prop="purchaseQty" label="入库数量" width="120" align="right">
-              <template #default>—</template>
+              <template slot-scope="scope">
+                {{ formatNumber(scope.row.purchaseQty) }}
+              </template>
             </el-table-column>
             <el-table-column prop="salesQty" label="销售数量" width="120" align="right">
-              <template #default>—</template>
+              <template slot-scope="scope">
+                {{ formatNumber(scope.row.salesQty) }}
+              </template>
             </el-table-column>
             <el-table-column prop="inventory" label="现有库存" width="120" align="right">
-              <template #default>—</template>
+              <template slot-scope="scope">
+                {{ formatNumber(scope.row.inventory) }}
+              </template>
             </el-table-column>
-            <el-table-column prop="purchaseAmount" label="入库总资金" width="150" align="right">
-              <template #default>—</template>
+            <el-table-column prop="purchaseAmount" label="入库总金额" width="150" align="right">
+              <template slot-scope="scope">
+                {{ formatNumber(scope.row.purchaseAmount) }}
+              </template>
             </el-table-column>
             <el-table-column prop="amountRatio" label="入库资金占比(%)" width="150" align="right">
-              <template #default>—</template>
+              <template slot-scope="scope">
+                {{ scope.row.amountRatio }}%
+              </template>
             </el-table-column>
             <el-table-column prop="turnoverRate" label="库存周转率" align="right">
-              <template #default>
-                <el-tag type="info">—</el-tag>
+              <template slot-scope="scope">
+                <el-tag :type="getTurnoverRateType(scope.row.turnoverRate)">
+                  {{ scope.row.turnoverRate }}
+                </el-tag>
               </template>
             </el-table-column>
           </el-table>
@@ -119,44 +132,55 @@
       </el-col>
     </el-row>
 
-    <!-- SPU指标汇总表格 - 空表格 -->
     <el-row :gutter="20" class="charts-row">
       <el-col :span="24">
         <el-card>
-          <template #header>
+          <template slot="header">
             <div class="card-header">
               <span>SPU指标汇总(成品)</span>
-              <el-button type="primary" size="small" @click="handleRefresh">
-                <span style="margin-right: 4px;">🔄</span> 刷新
+              <el-button type="primary" size="small" @click="refreshData">
+                <i class="el-icon-refresh"></i> 刷新
               </el-button>
             </div>
           </template>
-          <el-table :data="[]" stripe style="width: 100%" v-loading="false">
+          <el-table :data="spuTableData" stripe style="width: 100%" v-loading="spuLoading">
             <el-table-column prop="spu" label="SPU" width="220" fixed />
             <el-table-column prop="attribute" label="属性" width="140">
-              <template #default>—</template>
-            </el-table-column>
-            <el-table-column prop="skuCount" label="SKU数" width="90" align="right">
-              <template #default>—</template>
+              <template slot-scope="scope">
+                {{ scope.row.attribute || '-' }}
+              </template>
             </el-table-column>
+            <el-table-column prop="skuCount" label="SKU数" width="90" align="right" />
             <el-table-column prop="purchaseQty" label="入库数量" width="120" align="right">
-              <template #default>—</template>
+              <template slot-scope="scope">
+                {{ formatNumber(scope.row.purchaseQty) }}
+              </template>
             </el-table-column>
             <el-table-column prop="salesQty" label="销售数量" width="120" align="right">
-              <template #default>—</template>
+              <template slot-scope="scope">
+                {{ formatNumber(scope.row.salesQty) }}
+              </template>
             </el-table-column>
             <el-table-column prop="inventory" label="现有库存" width="120" align="right">
-              <template #default>—</template>
+              <template slot-scope="scope">
+                {{ formatNumber(scope.row.inventory) }}
+              </template>
             </el-table-column>
-            <el-table-column prop="purchaseAmount" label="入库总资金" width="150" align="right">
-              <template #default>—</template>
+            <el-table-column prop="purchaseAmount" label="入库总金额" width="150" align="right">
+              <template slot-scope="scope">
+                {{ formatNumber(scope.row.purchaseAmount) }}
+              </template>
             </el-table-column>
             <el-table-column prop="amountRatio" label="入库资金占比(%)" width="150" align="right">
-              <template #default>—</template>
+              <template slot-scope="scope">
+                {{ scope.row.amountRatio }}%
+              </template>
             </el-table-column>
             <el-table-column prop="turnoverRate" label="库存周转率" align="right">
-              <template #default>
-                <el-tag type="info">—</el-tag>
+              <template slot-scope="scope">
+                <el-tag :type="getTurnoverRateType(scope.row.turnoverRate)">
+                  {{ scope.row.turnoverRate }}
+                </el-tag>
               </template>
             </el-table-column>
           </el-table>
@@ -166,78 +190,241 @@
   </div>
 </template>
 
-<script setup>
-  // 完全移除所有图标依赖,仅保留空的刷新方法
-  const handleRefresh = () => {
-    // 后续可添加数据加载逻辑
+<script>
+import request from '@/utils/request'
+import * as echarts from 'echarts'
+require('echarts/theme/macarons')
+
+export default {
+  name: 'StorageOverview',
+  data() {
+    return {
+      overviewData: {
+        totalInventory: 0,
+        totalValue: 0,
+        turnoverRate: 0,
+        inTransitRatio: 0,
+        assemblyQty: 0
+      },
+      monthlyComparison: [],
+      monthlyLoading: false,
+      skuTableData: [],
+      spuTableData: [],
+      loading: false,
+      spuLoading: false,
+      chartInstance: null
+    }
+  },
+  mounted() {
+    this.refreshData()
+    this.$nextTick(() => {
+      this.initChart()
+    })
+    window.addEventListener('resize', this.resizeChart)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.resizeChart)
+    if (this.chartInstance) {
+      this.chartInstance.dispose()
+      this.chartInstance = null
+    }
+  },
+  methods: {
+    normalizeResponse(res) {
+      if (!res) return null
+      if (res.code === 200) return res.data
+      if (res.data) return res.data
+      return null
+    },
+    initChart() {
+      if (this.chartInstance || !this.$refs.inventoryComparisonChart) return
+      this.chartInstance = echarts.init(this.$refs.inventoryComparisonChart, 'macarons')
+      this.updateMonthlyChart()
+    },
+    resizeChart() {
+      if (this.chartInstance) this.chartInstance.resize()
+    },
+    updateMonthlyChart() {
+      if (!this.chartInstance) return
+      const months = []
+      const purchase = []
+      const sales = []
+      const inventory = []
+
+      if (Array.isArray(this.monthlyComparison) && this.monthlyComparison.length) {
+        this.monthlyComparison.forEach(item => {
+          months.push(item.month)
+          purchase.push(item.purchase || 0)
+          sales.push(item.sales || 0)
+          inventory.push(item.inventory || 0)
+        })
+      }
+
+      const option = {
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { type: 'shadow' }
+        },
+        legend: { data: ['入库', '销售', '当前库存'] },
+        grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
+        xAxis: { type: 'category', data: months },
+        yAxis: { type: 'value', name: '数量 (件)' },
+        series: [
+          { name: '入库', type: 'bar', data: purchase, itemStyle: { color: '#5470c6' } },
+          { name: '销售', type: 'bar', data: sales, itemStyle: { color: '#91cc75' } },
+          { name: '当前库存', type: 'line', data: inventory, itemStyle: { color: '#fac858' }, lineStyle: { width: 3 }, smooth: true }
+        ]
+      }
+      this.chartInstance.setOption(option, true)
+    },
+    async fetchOverviewData() {
+      try {
+        const res = await request({ url: '/api/inventory/overview', method: 'get' })
+        const data = this.normalizeResponse(res)
+        if (data) this.overviewData = { ...this.overviewData, ...data }
+      } catch (error) {
+        console.error('获取总览数据失败:', error)
+      }
+    },
+    async fetchMonthlyComparison() {
+      this.monthlyLoading = true
+      try {
+        const res = await request({ url: '/api/inventory/monthly-comparison', method: 'get' })
+        const data = this.normalizeResponse(res)
+        if (data && Array.isArray(data.months)) {
+          this.monthlyComparison = data.months.map((month, idx) => ({
+            month,
+            purchase: data.purchase ? data.purchase[idx] : 0,
+            sales: data.sales ? data.sales[idx] : 0,
+            inventory: data.inventory ? data.inventory[idx] : 0
+          }))
+        } else if (Array.isArray(data)) {
+          this.monthlyComparison = data
+        } else {
+          this.monthlyComparison = []
+        }
+      } catch (error) {
+        console.error('获取月度对比数据失败:', error)
+      } finally {
+        this.monthlyLoading = false
+        this.$nextTick(() => this.updateMonthlyChart())
+      }
+    },
+    async fetchSkuSummary() {
+      this.loading = true
+      try {
+        const res = await request({ url: '/api/inventory/sku-summary', method: 'get' })
+        const data = this.normalizeResponse(res)
+        if (Array.isArray(data)) this.skuTableData = data
+      } catch (error) {
+        console.error('获取SKU汇总数据失败:', error)
+      } finally {
+        this.loading = false
+      }
+    },
+    async fetchSpuSummary() {
+      this.spuLoading = true
+      try {
+        const res = await request({ url: '/api/inventory/spu-summary', method: 'get' })
+        const data = this.normalizeResponse(res)
+        if (Array.isArray(data)) this.spuTableData = data
+      } catch (error) {
+        console.error('获取SPU汇总数据失败:', error)
+      } finally {
+        this.spuLoading = false
+      }
+    },
+    async refreshData() {
+      await this.fetchOverviewData()
+      this.fetchMonthlyComparison()
+      this.fetchSkuSummary()
+      this.fetchSpuSummary()
+    },
+    getTurnoverRateType(rate) {
+      if (rate === 0) return 'info'
+      if (rate >= 1) return 'success'
+      if (rate >= 0.5) return 'warning'
+      return 'danger'
+    },
+    formatNumber(value) {
+      const num = Number(value || 0)
+      return num.toLocaleString()
+    }
   }
+}
 </script>
 
 <style scoped>
-  .inventory-overview {
-    width: 100%;
-  }
+.inventory-overview {
+  width: 100%;
+}
 
-  .metrics-row {
-    margin-bottom: 20px;
-  }
+.metrics-row {
+  margin-bottom: 20px;
+}
 
-  .metric-card {
-    cursor: pointer;
-    transition: transform 0.3s;
-  }
+.metric-card {
+  cursor: pointer;
+  transition: transform 0.3s;
+}
 
-  .metric-card:hover {
-    transform: translateY(-5px);
-    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-  }
+.metric-card:hover {
+  transform: translateY(-5px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
 
-  .metric-content {
-    display: flex;
-    align-items: center;
-    gap: 15px;
-  }
+.metric-content {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+}
 
-  .metric-icon {
-    width: 60px;
-    height: 60px;
-    border-radius: 12px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    color: #fff;
-  }
+.metric-icon {
+  width: 60px;
+  height: 60px;
+  border-radius: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #fff;
+  font-size: 28px;
+}
 
-  .metric-info {
-    flex: 1;
-  }
+.metric-info {
+  flex: 1;
+}
 
-  .metric-label {
-    font-size: 14px;
-    color: #909399;
-    margin-bottom: 8px;
-  }
+.metric-label {
+  font-size: 14px;
+  color: #909399;
+  margin-bottom: 8px;
+}
 
-  .metric-value {
-    font-size: 28px;
-    font-weight: bold;
-    color: #303133;
-    line-height: 1;
-  }
+.metric-value {
+  font-size: 28px;
+  font-weight: bold;
+  color: #303133;
+  line-height: 1;
+}
 
-  .metric-unit {
-    font-size: 12px;
-    color: #909399;
-    margin-top: 4px;
-  }
+.metric-unit,
+.metric-sub {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
 
-  .charts-row {
-    margin-bottom: 20px;
-  }
+.charts-row {
+  margin-bottom: 20px;
+}
 
-  .card-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-  }
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.chart {
+  height: 300px;
+}
 </style>

+ 202 - 439
src/views/storage/turnover/index.vue

@@ -1,451 +1,214 @@
-<template>
-  <div class="product-analysis">
-    <!-- 产品SKU搜索区域 -->
-    <div class="search-card">
-      <div class="search-row">
-        <div class="search-col search-input-col">
-          <div class="input-wrapper">
-            <span class="input-icon">🔍</span>
-            <input
-              type="text"
-              v-model="skuInput"
-              placeholder="请输入产品SKU(如:J06D01AS1)"
-              class="search-input"
-              @keyup.enter="searchProduct"
-            >
-            <button
-              class="clear-btn"
-              v-if="skuInput"
-              @click="clearInput"
-            >×</button>
-          </div>
-        </div>
-        <div class="search-col search-btn-col">
-          <button
-            class="search-btn"
-            :disabled="loading"
-            @click="searchProduct"
-          >
-            <span v-if="loading" class="loading-spinner"></span>
-            <span v-else>查询分析</span>
-          </button>
-        </div>
-        <div class="search-col search-info-col">
-          <span class="sku-tag" v-if="showProductArea">SKU: {{ skuInput }}</span>
-        </div>
-      </div>
-    </div>
-
-    <!-- 初始空状态 -->
-    <div class="empty-init" v-if="!showProductArea">
-      <div class="empty-icon">📦</div>
-      <div class="empty-text">请输入产品SKU进行查询分析</div>
-    </div>
-
-    <!-- 产品分析空框架 -->
-    <div class="analysis-content" v-else>
-      <!-- 关键指标卡片 -->
-      <div class="metrics-row">
-        <div class="metric-card">
-          <div class="metric-item">
-            <div class="metric-label">入库总量</div>
-            <div class="metric-value">-</div>
-            <div class="metric-unit">件</div>
-          </div>
-        </div>
-        <div class="metric-card">
-          <div class="metric-item">
-            <div class="metric-label">销售总量</div>
-            <div class="metric-value">-</div>
-            <div class="metric-unit">件</div>
-          </div>
-        </div>
-        <div class="metric-card">
-          <div class="metric-item">
-            <div class="metric-label">当前库存</div>
-            <div class="metric-value">-</div>
-            <div class="metric-unit">件</div>
-          </div>
-        </div>
-        <div class="metric-card">
-          <div class="metric-item">
-            <div class="metric-label">周转率</div>
-            <div class="metric-value">-</div>
-            <div class="metric-unit">次</div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 分段周转率表格 -->
-      <div class="card-container">
+<template>
+  <div class="settings">
+    <el-card>
+      <template slot="header">
         <div class="card-header">
-          <span>分段周转率</span>
-        </div>
-        <div class="table-container">
-          <table class="turnover-table">
-            <thead>
-            <tr>
-              <th width="220">统计区间</th>
-              <th width="160">销售出库 (件)</th>
-              <th width="220">时间加权平均库存 (件)</th>
-              <th>周转率 (次)</th>
-            </tr>
-            </thead>
-            <tbody>
-            <tr class="empty-row">
-              <td colspan="4">暂无数据</td>
-            </tr>
-            </tbody>
-          </table>
-        </div>
-      </div>
-
-      <!-- 库存趋势图占位 -->
-      <div class="card-container">
-        <div class="card-header">
-          <span>单品库存变化趋势(入库/销售/库存)</span>
-        </div>
-        <div class="chart-placeholder">
-          暂无图表数据
-        </div>
-      </div>
-
-      <!-- 预测与生命周期 -->
-      <div class="insights-row">
-        <div class="insight-card">
-          <div class="card-header">
-            <span>30天预测摘要</span>
-          </div>
-          <div class="empty-placeholder">
-            暂无预测数据
+          <span>分析设置(生命周期/LSTM权重)</span>
+          <div class="header-actions">
+            <el-button size="small" @click="resetToDefault" :disabled="!hasDefaults">恢复默认</el-button>
+            <el-button type="primary" size="small" @click="save" :loading="saving">保存</el-button>
           </div>
         </div>
-        <div class="insight-card">
-          <div class="card-header">
-            <span>生命周期分段</span>
-          </div>
-          <div class="empty-placeholder">
-            暂无生命周期数据
-          </div>
-        </div>
-      </div>
-    </div>
+      </template>
+
+      <el-form label-width="220px">
+        <el-form-item label="生命周期-销售增长权重">
+          <el-slider v-model="weights.sales_growth_weight" :min="0" :max="1" :step="0.05" :format-tooltip="formatTooltip" />
+        </el-form-item>
+        <el-form-item label="生命周期-库存波动权重">
+          <el-slider v-model="weights.inventory_variance_weight" :min="0" :max="1" :step="0.05" :format-tooltip="formatTooltip" />
+        </el-form-item>
+        <el-form-item label="生命周期-周转率权重">
+          <el-slider v-model="weights.turnover_weight" :min="0" :max="1" :step="0.05" :format-tooltip="formatTooltip" />
+        </el-form-item>
+        <el-form-item label="生命周期-状态信号权重">
+          <el-slider v-model="weights.state_signal_weight" :min="0" :max="1" :step="0.05" :format-tooltip="formatTooltip" />
+        </el-form-item>
+        <el-form-item label="生命周期滑动窗口(天)">
+          <el-input-number v-model="weights.lifecycle_window_days" :min="7" :max="60" />
+        </el-form-item>
+
+        <el-divider />
+
+        <el-form-item label="LSTM特征-库存权重">
+          <el-slider v-model="weights.feature_inventory_weight" :min="0" :max="1" :step="0.05" :format-tooltip="formatTooltip" />
+        </el-form-item>
+        <el-form-item label="LSTM特征-销量权重">
+          <el-slider v-model="weights.feature_sales_weight" :min="0" :max="1" :step="0.05" :format-tooltip="formatTooltip" />
+        </el-form-item>
+        <el-form-item label="LSTM特征-入库权重">
+          <el-slider v-model="weights.feature_purchase_weight" :min="0" :max="1" :step="0.05" :format-tooltip="formatTooltip" />
+        </el-form-item>
+        <el-form-item label="LSTM特征-净流量权重">
+          <el-slider v-model="weights.feature_net_flow_weight" :min="0" :max="1" :step="0.05" :format-tooltip="formatTooltip" />
+        </el-form-item>
+        <el-form-item label="LSTM特征-稳定偏差权重">
+          <el-slider v-model="weights.feature_stable_gap_weight" :min="0" :max="1" :step="0.05" :format-tooltip="formatTooltip" />
+        </el-form-item>
+
+        <el-divider />
+
+        <el-form-item label="库存状态-低库存权重">
+          <el-slider v-model="weights.state_low_weight" :min="0" :max="1" :step="0.05" :format-tooltip="formatTooltip" />
+        </el-form-item>
+        <el-form-item label="库存状态-超储权重">
+          <el-slider v-model="weights.state_overstock_weight" :min="0" :max="1" :step="0.05" :format-tooltip="formatTooltip" />
+        </el-form-item>
+        <el-form-item label="库存状态-慢动/滞销权重">
+          <el-slider v-model="weights.state_slow_weight" :min="0" :max="1" :step="0.05" :format-tooltip="formatTooltip" />
+        </el-form-item>
+        <el-form-item label="库存状态-需求峰值权重">
+          <el-slider v-model="weights.state_spike_weight" :min="0" :max="1" :step="0.05" :format-tooltip="formatTooltip" />
+        </el-form-item>
+        <el-form-item label="库存状态-正常权重">
+          <el-slider v-model="weights.state_normal_weight" :min="0" :max="1" :step="0.05" :format-tooltip="formatTooltip" />
+        </el-form-item>
+
+        <el-divider />
+
+        <el-form-item label="稳定库存窗口天数">
+          <el-input-number v-model="weights.stable_window_days" :min="7" :max="180" />
+        </el-form-item>
+        <el-form-item label="预测净流趋势权重">
+          <el-slider v-model="weights.forecast_drift_weight" :min="0" :max="0.5" :step="0.01" :format-tooltip="formatTooltip" />
+        </el-form-item>
+
+        <el-divider />
+        <h4 style="margin: 10px 0;">风险评分权重(总计100)</h4>
+        <el-form-item label="覆盖评分权重">
+          <el-input-number v-model="riskWeights.coverageWeight" :min="0" :max="100" />
+        </el-form-item>
+        <el-form-item label="周转评分权重">
+          <el-input-number v-model="riskWeights.turnoverWeight" :min="0" :max="100" />
+        </el-form-item>
+        <el-form-item label="趋势评分权重">
+          <el-input-number v-model="riskWeights.trendWeight" :min="0" :max="100" />
+        </el-form-item>
+        <el-form-item label="资金评分权重">
+          <el-input-number v-model="riskWeights.capitalWeight" :min="0" :max="100" />
+        </el-form-item>
+        <div style="text-align:right;color:#909399">当前合计:{{ riskTotalWeight }} / 100</div>
+      </el-form>
+    </el-card>
   </div>
 </template>
 
-<script setup>
-  import { ref } from 'vue'
-
-  // 核心响应式状态
-  const skuInput = ref('')       // SKU输入值
-  const loading = ref(false)     // 加载状态
-  const showProductArea = ref(false) // 是否显示分析区域
-
-  // 清空输入框
-  const clearInput = () => {
-    skuInput.value = ''
-    showProductArea.value = false
-  }
-
-  // 搜索产品(仅UI状态切换,无数据处理)
-  const searchProduct = () => {
-    const sku = skuInput.value.trim()
-    if (!sku) {
-      alert('请输入产品SKU')
-      return
+<script>
+import request from '@/utils/request'
+import { Message } from 'element-ui'
+
+export default {
+  name: 'StorageSettings',
+  data() {
+    return {
+      weights: {
+        sales_growth_weight: 0.35,
+        inventory_variance_weight: 0.25,
+        turnover_weight: 0.2,
+        state_signal_weight: 0.3,
+        lifecycle_window_days: 14,
+        feature_inventory_weight: 0.4,
+        feature_sales_weight: 0.25,
+        feature_purchase_weight: 0.15,
+        feature_net_flow_weight: 0.1,
+        feature_stable_gap_weight: 0.1,
+        state_low_weight: 0.3,
+        state_overstock_weight: 0.3,
+        state_slow_weight: 0.2,
+        state_spike_weight: 0.15,
+        state_normal_weight: 0.1,
+        stable_window_days: 60,
+        forecast_drift_weight: 0.15
+      },
+      saving: false,
+      riskWeights: {
+        coverageWeight: 40,
+        turnoverWeight: 30,
+        trendWeight: 20,
+        capitalWeight: 10
+      },
+      defaultWeights: {},
+      defaultRiskWeights: {}
+    }
+  },
+  computed: {
+    hasDefaults() {
+      return Object.keys(this.defaultWeights).length > 0 || Object.keys(this.defaultRiskWeights).length > 0
+    },
+    riskTotalWeight() {
+      return ['coverageWeight', 'turnoverWeight', 'trendWeight', 'capitalWeight']
+        .map(k => Number(this.riskWeights[k] || 0))
+        .reduce((a, b) => a + b, 0)
+    }
+  },
+  mounted() {
+    this.load()
+  },
+  methods: {
+    formatTooltip(value) {
+      return Number(value).toFixed(2)
+    },
+    async load() {
+      try {
+        const [invPayload, riskPayload] = await Promise.all([
+          request({ url: '/api/inventory/settings', method: 'get' }),
+          request({ url: '/api/risk/settings', method: 'get' })
+        ])
+        if (invPayload && invPayload.code === 200) {
+          if (invPayload.data) this.weights = { ...this.weights, ...invPayload.data }
+          if (invPayload.defaults) this.defaultWeights = invPayload.defaults
+        }
+        if (riskPayload && riskPayload.code === 200) {
+          if (riskPayload.data) this.riskWeights = { ...this.riskWeights, ...riskPayload.data }
+          if (riskPayload.defaults) this.defaultRiskWeights = riskPayload.defaults
+        }
+      } catch (e) {
+        Message.error('加载设置失败')
+      }
+    },
+    async save() {
+      this.saving = true
+      try {
+        const [invUpdated, riskUpdated] = await Promise.all([
+          request({ url: '/api/inventory/settings', method: 'post', data: this.weights }),
+          request({ url: '/api/risk/settings', method: 'post', data: this.riskWeights })
+        ])
+        if ((invUpdated && invUpdated.code === 200) || (riskUpdated && riskUpdated.code === 200)) {
+          Message.success('设置已保存')
+        }
+      } catch (e) {
+        Message.error('保存失败')
+      } finally {
+        this.saving = false
+      }
+    },
+    resetToDefault() {
+      if (!this.hasDefaults) return
+      if (Object.keys(this.defaultWeights).length) {
+        this.weights = { ...this.weights, ...this.defaultWeights }
+      }
+      if (Object.keys(this.defaultRiskWeights).length) {
+        this.riskWeights = { ...this.riskWeights, ...this.defaultRiskWeights }
+      }
+      Message.success('已恢复默认权重')
     }
-
-    // 模拟加载状态
-    loading.value = true
-
-    // 仅延迟切换UI状态,无实际数据请求/处理
-    setTimeout(() => {
-      loading.value = false
-      showProductArea.value = true
-      alert(`已触发SKU "${sku}" 查询,当前为Vue空框架版本,无实际数据展示`)
-    }, 800)
   }
+}
 </script>
 
 <style scoped>
-  /* 全局基础样式 */
-  .product-analysis {
-    width: 100%;
-    padding: 20px;
-    box-sizing: border-box;
-    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
-    background-color: #f8f9fa;
-    min-height: 100vh;
-  }
-
-  /* 搜索区域样式 */
-  .search-card {
-    background: #ffffff;
-    border-radius: 8px;
-    padding: 20px;
-    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
-    margin-bottom: 20px;
-  }
-
-  .search-row {
-    display: flex;
-    align-items: center;
-    gap: 20px;
-    flex-wrap: wrap;
-  }
-
-  .search-col {
-    flex: 1;
-  }
-
-  .search-input-col {
-    flex: 2;
-    min-width: 300px;
-  }
-
-  .search-btn-col {
-    flex: none;
-    width: 140px;
-  }
-
-  .search-info-col {
-    flex: 1;
-    min-width: 200px;
-  }
-
-  .input-wrapper {
-    position: relative;
-    display: flex;
-    align-items: center;
-  }
-
-  .search-input {
-    width: 100%;
-    height: 40px;
-    padding: 0 40px 0 36px;
-    border: 1px solid #dcdfe6;
-    border-radius: 4px;
-    font-size: 14px;
-    outline: none;
-    transition: border-color 0.3s;
-  }
-
-  .search-input:focus {
-    border-color: #409eff;
-  }
-
-  .input-icon {
-    position: absolute;
-    left: 12px;
-    font-size: 16px;
-    color: #909399;
-  }
-
-  .clear-btn {
-    position: absolute;
-    right: 12px;
-    width: 20px;
-    height: 20px;
-    border: none;
-    background: transparent;
-    font-size: 16px;
-    color: #909399;
-    cursor: pointer;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-
-  .search-btn {
-    width: 100%;
-    height: 40px;
-    background: #409eff;
-    color: #fff;
-    border: none;
-    border-radius: 4px;
-    font-size: 14px;
-    cursor: pointer;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    gap: 8px;
-    transition: background-color 0.3s;
-  }
-
-  .search-btn:hover:not(:disabled) {
-    background: #337ecc;
-  }
-
-  .search-btn:disabled {
-    background: #a0cfff;
-    cursor: not-allowed;
-  }
-
-  .loading-spinner {
-    width: 16px;
-    height: 16px;
-    border: 2px solid #fff;
-    border-top: 2px solid transparent;
-    border-radius: 50%;
-    animation: spin 1s linear infinite;
-  }
-
-  @keyframes spin {
-    to {
-      transform: rotate(360deg);
-    }
-  }
-
-  .sku-tag {
-    display: inline-block;
-    height: 28px;
-    line-height: 28px;
-    padding: 0 10px;
-    background: #ecf5ff;
-    color: #409eff;
-    border-radius: 4px;
-    font-size: 12px;
-  }
-
-  /* 初始空状态 */
-  .empty-init {
-    padding: 100px 0;
-    text-align: center;
-  }
-
-  .empty-icon {
-    font-size: 64px;
-    color: #dcdfe6;
-    margin-bottom: 20px;
-  }
-
-  .empty-text {
-    font-size: 16px;
-    color: #909399;
-  }
-
-  /* 分析区域样式 */
-  .analysis-content {
-    display: flex;
-    flex-direction: column;
-    gap: 20px;
-  }
-
-  /* 关键指标卡片 */
-  .metrics-row {
-    display: flex;
-    gap: 20px;
-    flex-wrap: wrap;
-  }
-
-  .metric-card {
-    flex: 1;
-    min-width: 200px;
-    background: #ffffff;
-    border-radius: 8px;
-    padding: 20px;
-    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
-    text-align: center;
-    cursor: pointer;
-    transition: transform 0.3s, box-shadow 0.3s;
-  }
-
-  .metric-card:hover {
-    transform: translateY(-5px);
-    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-  }
-
-  .metric-label {
-    font-size: 14px;
-    color: #909399;
-    margin-bottom: 10px;
-  }
-
-  .metric-value {
-    font-size: 32px;
-    font-weight: bold;
-    color: #303133;
-    margin-bottom: 5px;
-  }
-
-  .metric-unit {
-    font-size: 12px;
-    color: #909399;
-  }
-
-  /* 通用卡片容器 */
-  .card-container, .insight-card {
-    background: #ffffff;
-    border-radius: 8px;
-    padding: 20px;
-    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
-  }
-
-  .card-header {
-    font-size: 16px;
-    font-weight: 500;
-    color: #303133;
-    margin-bottom: 16px;
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-  }
-
-  /* 表格样式 */
-  .table-container {
-    width: 100%;
-    overflow-x: auto;
-  }
-
-  .turnover-table {
-    width: 100%;
-    border-collapse: collapse;
-    text-align: left;
-  }
-
-  .turnover-table th,
-  .turnover-table td {
-    padding: 12px;
-    border: 1px solid #ebeef5;
-  }
-
-  .turnover-table th {
-    background: #f5f7fa;
-    color: #606266;
-    font-weight: 500;
-  }
-
-  .empty-row td {
-    text-align: center;
-    color: #909399;
-  }
-
-  /* 图表占位符 */
-  .chart-placeholder {
-    height: 400px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    color: #909399;
-    border: 1px dashed #dcdfe6;
-    border-radius: 4px;
-  }
-
-  /* 洞察区域 */
-  .insights-row {
-    display: flex;
-    gap: 20px;
-    flex-wrap: wrap;
-  }
-
-  .insight-card {
-    flex: 1;
-    min-width: 300px;
-  }
-
-  .empty-placeholder {
-    padding: 40px 0;
-    text-align: center;
-    color: #909399;
-  }
+.settings {
+  width: 100%;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.header-actions {
+  display: flex;
+  gap: 10px;
+}
 </style>