Gogs 3 nedēļas atpakaļ
vecāks
revīzija
089433b784

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

@@ -16,7 +16,7 @@
           <svg-icon v-if="item.meta && item.meta.icon" :icon-class="item.meta.icon" class="icon" />
           <i v-else class="el-icon-document icon"></i>
         </div>
-        <div class="submenu-title">{{ item.meta ? item.meta.title : item.name }}</div>
+        <div class="submenu-title">{{ formatMenuTitle(item.meta ? item.meta.title : item.name) }}</div>
       </div>
     </div>
   </div>
@@ -79,6 +79,9 @@ export default {
     },
     handleItemClick(item) {
       this.$router.push(item.path)
+    },
+    formatMenuTitle(title) {
+      return title === '半成品模块' ? '产品模块' : title
     }
   }
 }

+ 441 - 91
src/views/storage/accuracy/index.vue

@@ -63,18 +63,14 @@
           <el-select v-model="filterStatus" placeholder="状态筛选" clearable>
             <el-option label="全部" value="" />
             <el-option label="在库" value="in-stock" />
-            <el-option label="在制" value="in-production" />
-            <el-option label="待检" value="pending" />
-            <el-option label="已用" value="used" />
+            <el-option label="低库存" value="low-stock" />
+            <el-option label="已用完" value="used" />
           </el-select>
         </el-col>
         <el-col :span="4">
           <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="pcb" />
+            <el-option label="半成品" value="半成品" />
           </el-select>
         </el-col>
         <el-col :span="6">
@@ -85,11 +81,6 @@
             <i class="el-icon-refresh-left"></i> 重置
           </el-button>
         </el-col>
-        <el-col :span="4" style="text-align: right;">
-          <el-button type="success" @click="handleAdd">
-            <i class="el-icon-plus"></i> 新增半成品
-          </el-button>
-        </el-col>
       </el-row>
     </el-card>
 
@@ -100,7 +91,7 @@
           <el-button type="primary" size="small" @click="fetchAssemblyCapacities">刷新</el-button>
         </div>
       </template>
-      <el-table :data="assemblyCapacities" stripe style="width: 100%">
+      <el-table v-loading="assemblyLoading" :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" />
@@ -119,6 +110,189 @@
           </template>
         </el-table-column>
       </el-table>
+      <el-pagination
+        :current-page="assemblyCurrentPage"
+        :page-size="assemblyPageSize"
+        :total="assemblyTotal"
+        :page-sizes="[10, 20, 50, 100]"
+        layout="total, sizes, prev, pager, next, jumper"
+        style="margin-top: 16px; text-align: right;"
+        @current-change="handleAssemblyPageChange"
+        @size-change="handleAssemblySizeChange"
+      />
+    </el-card>
+
+    <el-card class="table-card">
+      <template slot="header">
+        <div style="display:flex; justify-content:space-between; align-items:center;">
+          <span>半成品明细</span>
+          <el-button type="primary" size="small" @click="fetchSemiProductList">刷新</el-button>
+        </div>
+      </template>
+      <el-table v-loading="semiListLoading" :data="semiProductList" stripe style="width: 100%">
+        <el-table-column prop="id" label="ID" width="80" />
+        <el-table-column prop="code" label="半成品编码" width="170" />
+        <el-table-column prop="name" label="半成品名称" min-width="180" />
+        <el-table-column prop="quantity" label="库存数量" width="110" align="right" />
+        <el-table-column prop="inboundQty" label="推导入库量" width="120" align="right" />
+        <el-table-column prop="consumedQty" label="组装消耗量" width="120" align="right" />
+        <el-table-column prop="safeStock" label="安全库存" width="100" align="right" />
+        <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 prop="unitPrice" label="单价(元)" width="100" align="right" />
+        <el-table-column prop="totalValue" label="库存价值(元)" width="130" align="right" />
+        <el-table-column label="关联成品" min-width="220">
+          <template slot-scope="scope">
+            <el-tag
+              v-for="name in scope.row.usedForProducts"
+              :key="scope.row.code + '-' + name"
+              size="mini"
+              style="margin-right: 6px; margin-bottom: 4px;"
+            >
+              {{ name }}
+            </el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        :current-page="currentPage"
+        :page-size="pageSize"
+        :total="totalItems"
+        :page-sizes="[10, 20, 50, 100]"
+        layout="total, sizes, prev, pager, next, jumper"
+        style="margin-top: 16px; text-align: right;"
+        @current-change="handlePageChange"
+        @size-change="handlePageSizeChange"
+      />
+    </el-card>
+
+    <el-card class="table-card">
+      <template slot="header">
+        <div style="display:flex; justify-content:space-between; align-items:center;">
+          <span>SKU指标汇总</span>
+          <el-button type="primary" size="small" @click="fetchSkuSummary">刷新</el-button>
+        </div>
+      </template>
+      <div class="sku-filters">
+        属性:
+        <el-select v-model="skuFilters.attribute" placeholder="属性" size="small" style="width: 160px">
+          <el-option label="全部" value="" />
+          <el-option v-for="item in skuAttributeOptions" :key="item" :label="item" :value="item" />
+        </el-select>
+        分类:
+        <el-select v-model="skuFilters.category" placeholder="分类" size="small" style="width: 160px">
+          <el-option label="全部" value="" />
+          <el-option v-for="item in skuCategoryOptions" :key="item" :label="item" :value="item" />
+        </el-select>
+        SPU:
+        <el-select v-model="skuFilters.spu" placeholder="SPU" size="small" style="width: 200px">
+          <el-option label="全部" value="" />
+          <el-option v-for="item in skuSpuOptions" :key="item" :label="item" :value="item" />
+        </el-select>
+        <el-button size="small" @click="resetSkuFilters">重置</el-button>
+      </div>
+      <el-table
+        :data="pagedSkuData"
+        stripe
+        style="width: 100%"
+        v-loading="skuLoading"
+        @sort-change="handleSkuSortChange"
+      >
+        <el-table-column prop="sku" label="SKU" width="200" fixed sortable="custom" />
+        <el-table-column prop="category" label="分类" width="140">
+          <template slot-scope="scope">{{ scope.row.category || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="attribute" label="属性" width="140">
+          <template slot-scope="scope">{{ scope.row.attribute || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="spuName" label="SPU" width="200">
+          <template slot-scope="scope">{{ scope.row.spuName || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="purchaseQty" label="入库数量" width="120" align="right" sortable="custom">
+          <template slot-scope="scope">{{ formatNumber(scope.row.purchaseQty) }}</template>
+        </el-table-column>
+        <el-table-column prop="salesQty" label="销售数量" width="120" align="right" sortable="custom">
+          <template slot-scope="scope">{{ formatNumber(scope.row.salesQty) }}</template>
+        </el-table-column>
+        <el-table-column prop="inventory" label="现有库存" width="120" align="right" sortable="custom">
+          <template slot-scope="scope">{{ formatNumber(scope.row.inventory) }}</template>
+        </el-table-column>
+        <el-table-column prop="purchaseAmount" label="入库总金额" width="150" align="right" sortable="custom">
+          <template slot-scope="scope">{{ formatNumber(scope.row.purchaseAmount) }}</template>
+        </el-table-column>
+        <el-table-column prop="amountRatio" label="入库资金占比(%)" width="150" align="right" sortable="custom">
+          <template slot-scope="scope">{{ scope.row.amountRatio }}%</template>
+        </el-table-column>
+        <el-table-column prop="turnoverRate" label="库存周转率" align="right" sortable="custom">
+          <template slot-scope="scope">
+            <el-tag :type="getTurnoverRateType(scope.row.turnoverRate)">{{ scope.row.turnoverRate }}</el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        :current-page="skuCurrentPage"
+        :page-size="skuPageSize"
+        :total="skuTotal"
+        :page-sizes="[10, 20, 50, 100]"
+        layout="total, sizes, prev, pager, next, jumper"
+        style="margin-top: 16px; text-align: right;"
+        @current-change="handleSkuPageChange"
+        @size-change="handleSkuSizeChange"
+      />
+    </el-card>
+
+    <el-card class="table-card">
+      <template slot="header">
+        <div style="display:flex; justify-content:space-between; align-items:center;">
+          <span>SPU指标汇总(成品)</span>
+          <el-button type="primary" size="small" @click="fetchSpuSummary">刷新</el-button>
+        </div>
+      </template>
+      <el-table
+        :data="pagedSpuData"
+        stripe
+        style="width: 100%"
+        v-loading="spuLoading"
+        @sort-change="handleSpuSortChange"
+      >
+        <el-table-column prop="spu" label="SPU" width="220" fixed sortable="custom" />
+        <el-table-column prop="skuCount" label="SKU数" width="90" align="right" sortable="custom" />
+        <el-table-column prop="purchaseQty" label="入库数量" width="120" align="right" sortable="custom">
+          <template slot-scope="scope">{{ formatNumber(scope.row.purchaseQty) }}</template>
+        </el-table-column>
+        <el-table-column prop="salesQty" label="销售数量" width="120" align="right" sortable="custom">
+          <template slot-scope="scope">{{ formatNumber(scope.row.salesQty) }}</template>
+        </el-table-column>
+        <el-table-column prop="inventory" label="现有库存" width="120" align="right" sortable="custom">
+          <template slot-scope="scope">{{ formatNumber(scope.row.inventory) }}</template>
+        </el-table-column>
+        <el-table-column prop="purchaseAmount" label="入库总金额" width="150" align="right" sortable="custom">
+          <template slot-scope="scope">{{ formatNumber(scope.row.purchaseAmount) }}</template>
+        </el-table-column>
+        <el-table-column prop="amountRatio" label="入库资金占比(%)" width="150" align="right" sortable="custom">
+          <template slot-scope="scope">{{ scope.row.amountRatio }}%</template>
+        </el-table-column>
+        <el-table-column prop="turnoverRate" label="库存周转率" align="right" sortable="custom">
+          <template slot-scope="scope">
+            <el-tag :type="getTurnoverRateType(scope.row.turnoverRate)">{{ scope.row.turnoverRate }}</el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        :current-page="spuCurrentPage"
+        :page-size="spuPageSize"
+        :total="spuTotal"
+        :page-sizes="[10, 20, 50, 100]"
+        layout="total, sizes, prev, pager, next, jumper"
+        style="margin-top: 16px; text-align: right;"
+        @current-change="handleSpuPageChange"
+        @size-change="handleSpuSizeChange"
+      />
     </el-card>
 
 <!--    <el-card class="table-card">-->
@@ -280,74 +454,35 @@ export default {
       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
-        }
-      ],
+      semiProductList: [],
+      semiListLoading: false,
+      totalItems: 0,
       assemblyCapacities: [],
+      assemblyLoading: false,
+      assemblyCurrentPage: 1,
+      assemblyPageSize: 10,
+      assemblyTotal: 0,
+      skuTableData: [],
+      spuTableData: [],
+      skuLoading: false,
+      spuLoading: false,
+      skuFilters: {
+        attribute: '',
+        category: '',
+        spu: ''
+      },
+      skuSort: {
+        prop: '',
+        order: ''
+      },
+      spuSort: {
+        prop: '',
+        order: ''
+      },
+      skuCurrentPage: 1,
+      skuPageSize: 10,
+      spuCurrentPage: 1,
+      spuPageSize: 10,
       selectedBomProduct: 'watch',
       turnoverAnalysis: [
         {
@@ -517,13 +652,66 @@ export default {
     }
   },
   computed: {
-    totalItems() {
-      return this.semiProductList.length
+    skuAttributeOptions() {
+      const values = this.skuTableData
+        .map(item => (item.attribute || '').trim())
+        .filter(item => item)
+      return Array.from(new Set(values)).sort()
+    },
+    skuCategoryOptions() {
+      const values = this.skuTableData
+        .map(item => (item.category || '').trim())
+        .filter(item => item)
+      return Array.from(new Set(values)).sort()
+    },
+    skuSpuOptions() {
+      const values = this.skuTableData
+        .map(item => (item.spuName || '').trim())
+        .filter(item => item)
+      return Array.from(new Set(values)).sort()
+    },
+    filteredSkuData() {
+      return this.skuTableData.filter(row => {
+        if (this.skuFilters.attribute && row.attribute !== this.skuFilters.attribute) return false
+        if (this.skuFilters.category && row.category !== this.skuFilters.category) return false
+        if (this.skuFilters.spu && row.spuName !== this.skuFilters.spu) return false
+        return true
+      })
+    },
+    sortedSkuData() {
+      const { prop, order } = this.skuSort
+      if (!prop || !order) return this.filteredSkuData
+      return this.sortTableData(this.filteredSkuData, prop, order)
+    },
+    pagedSkuData() {
+      const start = (this.skuCurrentPage - 1) * this.skuPageSize
+      return this.sortedSkuData.slice(start, start + this.skuPageSize)
+    },
+    skuTotal() {
+      return this.filteredSkuData.length
+    },
+    sortedSpuData() {
+      const { prop, order } = this.spuSort
+      if (!prop || !order) return this.spuTableData
+      return this.sortTableData(this.spuTableData, prop, order)
+    },
+    pagedSpuData() {
+      const start = (this.spuCurrentPage - 1) * this.spuPageSize
+      return this.sortedSpuData.slice(start, start + this.spuPageSize)
+    },
+    spuTotal() {
+      return this.sortedSpuData.length
     }
   },
   mounted() {
+    if (this.$route && this.$route.meta && this.$route.meta.title === '半成品模块') {
+      this.$route.meta.title = '产品模块'
+    }
     this.fetchStats()
     this.fetchAssemblyCapacities()
+    this.fetchSemiProductList()
+    this.fetchSkuSummary()
+    this.fetchSpuSummary()
     this.$nextTick(() => {
       this.initCharts()
       this.renderCharts()
@@ -539,6 +727,15 @@ export default {
   watch: {
     selectedBomProduct() {
       this.updateBomChart()
+    },
+    'skuFilters.attribute'() {
+      this.skuCurrentPage = 1
+    },
+    'skuFilters.category'() {
+      this.skuCurrentPage = 1
+    },
+    'skuFilters.spu'() {
+      this.skuCurrentPage = 1
     }
   },
   methods: {
@@ -549,12 +746,28 @@ export default {
       return null
     },
     async fetchAssemblyCapacities() {
+      this.assemblyLoading = true
       try {
-        const res = await request({ url: '/api/semi-product/assembly-capacity', method: 'get' })
+        const res = await request({
+          url: '/api/semi-product/assembly-capacity',
+          method: 'get',
+          params: {
+            page: this.assemblyCurrentPage,
+            pageSize: this.assemblyPageSize
+          }
+        })
         const data = this.normalizeResponse(res)
-        if (Array.isArray(data)) this.assemblyCapacities = data
+        if (data && Array.isArray(data.list)) {
+          this.assemblyCapacities = data.list
+          this.assemblyTotal = Number(data.total || 0)
+        } else if (Array.isArray(data)) {
+          this.assemblyCapacities = data
+          this.assemblyTotal = data.length
+        }
       } catch (e) {
         console.error('获取可组装能力失败:', e)
+      } finally {
+        this.assemblyLoading = false
       }
     },
     async fetchStats() {
@@ -566,6 +779,126 @@ export default {
         console.error('获取半成品统计失败:', e)
       }
     },
+    async fetchSemiProductList() {
+      this.semiListLoading = true
+      try {
+        const res = await request({
+          url: '/api/semi-product/list',
+          method: 'get',
+          params: {
+            page: this.currentPage,
+            pageSize: this.pageSize,
+            keyword: this.searchKeyword || undefined,
+            status: this.filterStatus || undefined,
+            category: this.filterCategory || undefined
+          }
+        })
+        const data = this.normalizeResponse(res)
+        if (data) {
+          this.semiProductList = Array.isArray(data.list) ? data.list : []
+          this.totalItems = Number(data.total || 0)
+        }
+      } catch (e) {
+        console.error('获取半成品列表失败:', e)
+      } finally {
+        this.semiListLoading = false
+      }
+    },
+    async fetchSkuSummary() {
+      this.skuLoading = 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
+          this.skuCurrentPage = 1
+        }
+      } catch (e) {
+        console.error('获取SKU汇总数据失败:', e)
+      } finally {
+        this.skuLoading = 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
+          this.spuCurrentPage = 1
+        }
+      } catch (e) {
+        console.error('获取SPU汇总数据失败:', e)
+      } finally {
+        this.spuLoading = false
+      }
+    },
+    sortTableData(source, prop, order) {
+      const direction = order === 'ascending' ? 1 : -1
+      const data = [...source]
+      data.sort((a, b) => {
+        const aRaw = a[prop]
+        const bRaw = b[prop]
+        const aNum = typeof aRaw === 'number' ? aRaw : Number(aRaw)
+        const bNum = typeof bRaw === 'number' ? bRaw : Number(bRaw)
+        const aIsNum = !Number.isNaN(aNum) && aRaw !== '' && aRaw !== null && aRaw !== undefined
+        const bIsNum = !Number.isNaN(bNum) && bRaw !== '' && bRaw !== null && bRaw !== undefined
+        if (aIsNum && bIsNum) {
+          return (aNum - bNum) * direction
+        }
+        return String(aRaw || '').localeCompare(String(bRaw || ''), 'zh-Hans-CN', { numeric: true }) * direction
+      })
+      return data
+    },
+    handleSkuSortChange({ prop, order }) {
+      this.skuSort = { prop, order }
+      this.skuCurrentPage = 1
+    },
+    handleSpuSortChange({ prop, order }) {
+      this.spuSort = { prop, order }
+      this.spuCurrentPage = 1
+    },
+    handleSkuPageChange(page) {
+      this.skuCurrentPage = page
+    },
+    handleSkuSizeChange(size) {
+      this.skuPageSize = size
+      this.skuCurrentPage = 1
+    },
+    handleSpuPageChange(page) {
+      this.spuCurrentPage = page
+    },
+    handleSpuSizeChange(size) {
+      this.spuPageSize = size
+      this.spuCurrentPage = 1
+    },
+    handleAssemblyPageChange(page) {
+      this.assemblyCurrentPage = page
+      this.fetchAssemblyCapacities()
+    },
+    handleAssemblySizeChange(size) {
+      this.assemblyPageSize = size
+      this.assemblyCurrentPage = 1
+      this.fetchAssemblyCapacities()
+    },
+    resetSkuFilters() {
+      this.skuFilters.attribute = ''
+      this.skuFilters.category = ''
+      this.skuFilters.spu = ''
+      this.skuSort = { prop: '', order: '' }
+      this.skuCurrentPage = 1
+    },
+    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()
+    },
     initCharts() {
       if (this.$refs.stockDistributionChart && !this.stockChart) {
         this.stockChart = echarts.init(this.$refs.stockDistributionChart, 'macarons')
@@ -626,8 +959,7 @@ export default {
     getStatusType(status) {
       const map = {
         'in-stock': 'success',
-        'in-production': 'warning',
-        pending: 'info',
+        'low-stock': 'warning',
         used: 'danger'
       }
       return map[status] || 'info'
@@ -635,9 +967,8 @@ export default {
     getStatusText(status) {
       const map = {
         'in-stock': '在库',
-        'in-production': '在制',
-        pending: '待检',
-        used: '已用'
+        'low-stock': '低库存',
+        used: '已用完'
       }
       return map[status] || '未知'
     },
@@ -661,13 +992,24 @@ export default {
       return map[status] || 'info'
     },
     handleSearch() {
-      Message.success('搜索成功')
+      this.currentPage = 1
+      this.fetchSemiProductList()
     },
     handleReset() {
       this.searchKeyword = ''
       this.filterStatus = ''
       this.filterCategory = ''
-      Message.info('已重置筛选条件')
+      this.currentPage = 1
+      this.fetchSemiProductList()
+    },
+    handlePageChange(page) {
+      this.currentPage = page
+      this.fetchSemiProductList()
+    },
+    handlePageSizeChange(size) {
+      this.pageSize = size
+      this.currentPage = 1
+      this.fetchSemiProductList()
     },
     handleAdd() {
       Message.info('打开新增半成品对话框')
@@ -760,6 +1102,14 @@ export default {
   margin-bottom: 20px;
 }
 
+.sku-filters {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 12px;
+  flex-wrap: wrap;
+}
+
 .charts-row {
   margin-bottom: 20px;
 }

+ 330 - 2
src/views/storage/agent/index.vue

@@ -4,13 +4,105 @@
     <div class="chat-header">
       <div class="header-title">
         <span class="bot-icon">AI</span>
-        <span>智能助手</span>
+        <span>智能决策</span>
       </div>
       <button class="refresh-btn" @click="refreshChat" :disabled="loading">
         <span class="refresh-icon">↻</span>
       </button>
     </div>
 
+    <div class="decision-board" v-loading="decisionLoading">
+      <el-row :gutter="16" class="decision-metrics">
+        <el-col :span="6">
+          <div class="decision-card">
+            <div class="decision-label">安全库存阈值</div>
+            <div class="decision-value">{{ safetyStockThreshold.days }}天</div>
+            <div class="decision-sub">覆盖天数下限,单品不少于 {{ safetyStockThreshold.minQty }} 件</div>
+          </div>
+        </el-col>
+        <el-col :span="6">
+          <div class="decision-card">
+            <div class="decision-label">补货建议</div>
+            <div class="decision-value">{{ replenishSuggestions.length }}</div>
+            <div class="decision-sub">低库存或覆盖不足SKU</div>
+          </div>
+        </el-col>
+        <el-col :span="6">
+          <div class="decision-card">
+            <div class="decision-label">清仓建议</div>
+            <div class="decision-value">{{ clearanceSuggestions.length }}</div>
+            <div class="decision-sub">滞销、超储或低周转SKU</div>
+          </div>
+        </el-col>
+        <el-col :span="6">
+          <div class="decision-card">
+            <div class="decision-label">库存健康报告</div>
+            <div class="decision-value" :class="healthReport.type">{{ healthReport.text }}</div>
+            <div class="decision-sub">综合风险与周转表现</div>
+          </div>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="16" class="decision-sections">
+        <el-col :span="12">
+          <div class="decision-panel">
+            <div class="panel-title">补货建议</div>
+            <div v-if="replenishSuggestions.length" class="suggestion-list">
+              <div v-for="item in replenishSuggestions" :key="item.productCode || item.sku" class="suggestion-item">
+                <div>
+                  <div class="suggestion-title">{{ item.productName || item.productCode || item.sku }}</div>
+                  <div class="suggestion-desc">现有库存 {{ formatInteger(item.inventory) }} 件,建议补至 {{ formatInteger(item.suggestQty) }} 件</div>
+                </div>
+                <el-tag type="danger" size="mini">补货</el-tag>
+              </div>
+            </div>
+            <el-empty v-else description="暂无补货建议" :image-size="80" />
+          </div>
+        </el-col>
+        <el-col :span="12">
+          <div class="decision-panel">
+            <div class="panel-title">清仓建议</div>
+            <div v-if="clearanceSuggestions.length" class="suggestion-list">
+              <div v-for="item in clearanceSuggestions" :key="item.productCode || item.sku" class="suggestion-item">
+                <div>
+                  <div class="suggestion-title">{{ item.productName || item.productCode || item.sku }}</div>
+                  <div class="suggestion-desc">库存 {{ formatInteger(item.inventory) }} 件,周转率 {{ formatDecimal(item.turnoverRate) }} 次</div>
+                </div>
+                <el-tag type="warning" size="mini">清仓</el-tag>
+              </div>
+            </div>
+            <el-empty v-else description="暂无清仓建议" :image-size="80" />
+          </div>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="16" class="decision-sections">
+        <el-col :span="12">
+          <div class="decision-panel">
+            <div class="panel-title">库存健康报告</div>
+            <div class="health-summary">
+              <div class="health-line"><span>高风险SKU</span><b>{{ riskStats.critical }}</b></div>
+              <div class="health-line"><span>中风险SKU</span><b>{{ riskStats.warning }}</b></div>
+              <div class="health-line"><span>低风险SKU</span><b>{{ riskStats.info }}</b></div>
+              <div class="health-line"><span>健康SKU</span><b>{{ riskStats.safe }}</b></div>
+            </div>
+          </div>
+        </el-col>
+        <el-col :span="12">
+          <div class="decision-panel">
+            <div class="panel-title">库存周转分析</div>
+            <div v-if="turnoverAnalysis.length" class="turnover-list">
+              <div v-for="item in turnoverAnalysis" :key="item.sku || item.productCode" class="turnover-item">
+                <span>{{ item.productName || item.sku || item.productCode }}</span>
+                <b>{{ formatDecimal(item.turnoverRate) }} 次</b>
+              </div>
+            </div>
+            <el-empty v-else description="暂无周转数据" :image-size="80" />
+          </div>
+        </el-col>
+      </el-row>
+    </div>
+
     <div class="chat-container">
       <div class="chatbox" ref="chatbox">
         <div class="message" v-for="(msg, index) in chatMessages" :key="index">
@@ -63,10 +155,130 @@ export default {
     return {
       userInput: '',
       loading: false,
+      decisionLoading: false,
+      riskStats: {
+        critical: 0,
+        warning: 0,
+        info: 0,
+        safe: 0
+      },
+      riskRows: [],
+      skuRows: [],
+      safetyStockThreshold: {
+        days: 15,
+        minQty: 30
+      },
       chatMessages: DEFAULT_MESSAGES.map(item => ({ ...item }))
     }
   },
+  computed: {
+    replenishSuggestions() {
+      const fromRisk = this.riskRows
+        .filter(item => item.riskType === 'stockout' || item.riskLevel === 'critical')
+        .slice(0, 5)
+        .map(item => ({
+          ...item,
+          inventory: this.getInventory(item),
+          suggestQty: Math.max(this.getInventory(item), Math.ceil(Number(item.avgDailySales || 0) * this.safetyStockThreshold.days), this.safetyStockThreshold.minQty)
+        }))
+      if (fromRisk.length) return fromRisk
+      return this.skuRows
+        .filter(item => this.getInventory(item) < this.safetyStockThreshold.minQty)
+        .slice(0, 5)
+        .map(item => ({
+          ...item,
+          inventory: this.getInventory(item),
+          suggestQty: this.safetyStockThreshold.minQty
+        }))
+    },
+    clearanceSuggestions() {
+      const fromRisk = this.riskRows
+        .filter(item => item.riskType === 'overstock' || item.riskType === 'slow-moving' || Number(item.turnoverRate || 0) < 0.8)
+        .slice(0, 5)
+        .map(item => ({ ...item, inventory: this.getInventory(item) }))
+      if (fromRisk.length) return fromRisk
+      return this.skuRows
+        .filter(item => this.getInventory(item) > 0 && Number(item.turnoverRate || 0) < 0.8)
+        .slice(0, 5)
+        .map(item => ({ ...item, inventory: this.getInventory(item) }))
+    },
+    healthReport() {
+      if (this.riskStats.critical > 0) return { text: '高风险', type: 'danger' }
+      if (this.riskStats.warning > 0) return { text: '需关注', type: 'warning' }
+      return { text: '健康', type: 'success' }
+    },
+    turnoverAnalysis() {
+      const source = this.skuRows.length ? this.skuRows : this.riskRows
+      return [...source]
+        .filter(item => item.turnoverRate !== undefined && item.turnoverRate !== null)
+        .sort((a, b) => Number(a.turnoverRate || 0) - Number(b.turnoverRate || 0))
+        .slice(0, 6)
+    }
+  },
+  mounted() {
+    this.loadDecisionData()
+  },
   methods: {
+    normalizeResponse(res) {
+      if (!res) return null
+      if (res.code === 200) return res.data
+      if (res.data) return res.data
+      return null
+    },
+    async loadDecisionData() {
+      this.decisionLoading = true
+      try {
+        await Promise.all([
+          this.fetchRiskStats(),
+          this.fetchRiskRows(),
+          this.fetchSkuRows()
+        ])
+      } finally {
+        this.decisionLoading = false
+      }
+    },
+    async fetchRiskStats() {
+      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 fetchRiskRows() {
+      try {
+        const res = await request({
+          url: '/api/risk/list',
+          method: 'get',
+          params: { page: 1, pageSize: 50 }
+        })
+        const data = this.normalizeResponse(res)
+        if (data && Array.isArray(data.list)) this.riskRows = data.list
+        else if (data && Array.isArray(data.rows)) this.riskRows = data.rows
+        else this.riskRows = Array.isArray(data) ? data : []
+      } catch (e) {
+        console.error('获取风险列表失败:', e)
+      }
+    },
+    async fetchSkuRows() {
+      try {
+        const res = await request({ url: '/api/inventory/sku-summary', method: 'get', timeout: 120000 })
+        const data = this.normalizeResponse(res)
+        this.skuRows = Array.isArray(data) ? data : []
+      } catch (e) {
+        console.error('获取SKU汇总失败:', e)
+      }
+    },
+    getInventory(item) {
+      return Number(item.inventory || item.currentInventory || item.remainingInventory || item.quantity || 0)
+    },
+    formatInteger(value) {
+      return Number(value || 0).toLocaleString()
+    },
+    formatDecimal(value) {
+      return Number(value || 0).toFixed(2)
+    },
     async sendMessage() {
       const content = this.userInput.trim()
       if (!content || this.loading) return
@@ -132,6 +344,7 @@ export default {
       this.chatMessages = DEFAULT_MESSAGES.map(item => ({ ...item }))
       this.userInput = ''
       this.loading = false
+      this.loadDecisionData()
       this.$nextTick(() => {
         const chatbox = this.$refs.chatbox
         if (chatbox) {
@@ -159,7 +372,7 @@ export default {
   color: #333;
   margin: 0;
   padding: 20px;
-  max-width: 900px;
+  max-width: 1180px;
   margin: 0 auto;
 }
 
@@ -218,6 +431,121 @@ export default {
   background: transparent;
 }
 
+.decision-board {
+  background: #ffffff;
+  border-bottom: 1px solid #e5e7eb;
+  padding: 16px 20px 20px;
+}
+
+.decision-metrics,
+.decision-sections {
+  margin-bottom: 16px;
+}
+
+.decision-sections:last-child {
+  margin-bottom: 0;
+}
+
+.decision-card,
+.decision-panel {
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  background: #fff;
+}
+
+.decision-card {
+  min-height: 118px;
+  padding: 16px;
+}
+
+.decision-label {
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 10px;
+}
+
+.decision-value {
+  font-size: 28px;
+  font-weight: 700;
+  color: #303133;
+  line-height: 1.1;
+}
+
+.decision-value.success {
+  color: #67c23a;
+}
+
+.decision-value.warning {
+  color: #e6a23c;
+}
+
+.decision-value.danger {
+  color: #f56c6c;
+}
+
+.decision-sub {
+  margin-top: 8px;
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.5;
+}
+
+.decision-panel {
+  min-height: 220px;
+  padding: 16px;
+}
+
+.panel-title {
+  font-size: 15px;
+  font-weight: 600;
+  color: #303133;
+  margin-bottom: 12px;
+}
+
+.suggestion-list,
+.turnover-list,
+.health-summary {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.suggestion-item,
+.turnover-item,
+.health-line {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  padding: 10px 12px;
+  background: #f7f8fa;
+  border-radius: 6px;
+}
+
+.suggestion-title {
+  color: #303133;
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.suggestion-desc {
+  color: #909399;
+  font-size: 12px;
+  margin-top: 4px;
+}
+
+.turnover-item span,
+.health-line span {
+  color: #606266;
+  font-size: 13px;
+}
+
+.turnover-item b,
+.health-line b {
+  color: #303133;
+  font-size: 14px;
+}
+
 /* 对话容器 */
 .chat-container {
   background: #ffffff;

+ 486 - 177
src/views/storage/efficiency/index.vue

@@ -1,31 +1,100 @@
 <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>
+    <section class="analysis-toolbar">
+      <div class="toolbar-title">
+        <h2>单品库存水位分析</h2>
+        <p>查看 SKU 的库存、周转、补货点和最高/最低水位线。</p>
+      </div>
+      <div class="toolbar-actions">
+        <el-input
+          v-model="skuInput"
+          placeholder="输入产品 SKU"
+          size="medium"
+          clearable
+          @keyup.enter.native="searchProduct"
+        >
+          <template slot="prefix">
+            <i class="el-icon-search"></i>
+          </template>
+        </el-input>
+        <el-button type="primary" size="medium" icon="el-icon-search" :loading="loading" @click="searchProduct">
+          查询
+        </el-button>
+        <el-button size="medium" icon="el-icon-edit-outline" :disabled="!productData" @click="settingsVisible = !settingsVisible">
+          SKU覆盖设置
+        </el-button>
+      </div>
+    </section>
+
+    <el-card v-show="settingsVisible && productData" class="sku-waterline-card">
+      <template slot="header">
+        <div class="card-header">
+          <div>
+            <span>当前 SKU 水位覆盖设置</span>
+            <el-tag size="mini" class="sku-tag">SKU: {{ currentSku }}</el-tag>
           </div>
-        </el-col>
-      </el-row>
+          <el-button type="primary" size="mini" @click="saveSkuSetting(skuDraft)">保存当前SKU覆盖</el-button>
+        </div>
+      </template>
+      <el-table :data="[skuDraft]" border size="mini" v-loading="waterlineLoading">
+        <el-table-column label="SKU" min-width="150">
+          <template slot-scope="scope">
+            <span class="readonly-cell">{{ scope.row.sku || currentSku }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="类型" width="120">
+          <template slot-scope="scope">
+            <el-select v-model="scope.row.productType" size="mini">
+              <el-option label="爆品" value="hot" />
+              <el-option label="常规品" value="normal" />
+              <el-option label="新品" value="new" />
+              <el-option label="长尾/滞销" value="slow" />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column label="手动安全库存" width="140">
+          <template slot-scope="scope">
+            <el-input-number v-model="scope.row.safetyStockOverride" :min="0" :step="1" size="mini" controls-position="right" />
+          </template>
+        </el-table-column>
+        <el-table-column label="安全库存天数" width="140">
+          <template slot-scope="scope">
+            <el-input-number v-model="scope.row.safetyDaysOverride" :min="0" :step="1" size="mini" controls-position="right" />
+          </template>
+        </el-table-column>
+        <el-table-column label="补货提前期" width="130">
+          <template slot-scope="scope">
+            <el-input-number v-model="scope.row.leadTimeOverride" :min="0" :step="1" size="mini" controls-position="right" />
+          </template>
+        </el-table-column>
+        <el-table-column label="最长补货周期" width="140">
+          <template slot-scope="scope">
+            <el-input-number v-model="scope.row.maxReplenishCycleOverride" :min="0" :step="1" size="mini" controls-position="right" />
+          </template>
+        </el-table-column>
+        <el-table-column label="月销目标" width="130">
+          <template slot-scope="scope">
+            <el-input-number v-model="scope.row.monthlyTargetOverride" :min="0" :step="1" size="mini" controls-position="right" />
+          </template>
+        </el-table-column>
+        <el-table-column label="不合格率(%)" width="130">
+          <template slot-scope="scope">
+            <el-input-number v-model="scope.row.supplierDefectRateOverride" :min="0" :max="100" :step="0.1" size="mini" controls-position="right" />
+          </template>
+        </el-table-column>
+        <el-table-column label="修改人" width="110">
+          <template slot-scope="scope">{{ scope.row.updatedBy || currentUserName || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="修改时间" width="160">
+          <template slot-scope="scope">{{ scope.row.updatedTime || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="操作" width="90" fixed="right">
+          <template slot-scope="scope">
+            <el-button type="primary" size="mini" @click="saveSkuSetting(scope.row)">保存</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="waterline-note">SKU 来自当前查询结果;手动安全库存为空时,系统按安全库存天数自动计算;修改人由后端按当前登录账号自动记录。</div>
     </el-card>
 
     <div v-if="productData">
@@ -51,8 +120,8 @@
         <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-label">期末库存</div>
+              <div class="metric-value">{{ formatInteger(endingInventory) }}</div>
               <div class="metric-unit">件</div>
             </div>
           </el-card>
@@ -60,14 +129,73 @@
         <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-label">商品周转率</div>
+              <div class="metric-value">{{ formatTurnover(productData.turnoverRate) }}</div>
               <div class="metric-unit">次</div>
             </div>
           </el-card>
         </el-col>
       </el-row>
 
+      <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(overstockData.quantity) }}</div>
+              <div class="metric-unit">{{ overstockData.level }}</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" :class="{ 'metric-positive': endingInventoryGrowthRate >= 0, 'metric-negative': endingInventoryGrowthRate < 0 }">
+                {{ formatPercent(endingInventoryGrowthRate) }}
+              </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">{{ coverageDaysText }}</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 metric-status">{{ inventoryHealthStatus }}</div>
+              <div class="metric-unit">基于周转与积压</div>
+            </div>
+          </el-card>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20" class="waterline-result-row">
+        <el-col :span="24">
+          <el-card>
+            <template slot="header">
+              <div class="card-header">
+                <span>当前 SKU 水位计算结果</span>
+                <el-tag size="mini">{{ typeLabel(productData.productType) }}</el-tag>
+              </div>
+            </template>
+            <el-table :data="waterlineResultRows" border size="small">
+              <el-table-column prop="name" label="项目" width="180" />
+              <el-table-column prop="formula" label="口径" min-width="360" />
+              <el-table-column prop="value" label="结果" width="160" align="right" />
+            </el-table>
+          </el-card>
+        </el-col>
+      </el-row>
+
       <el-row :gutter="20" class="turnover-row" v-if="turnoverBreakdown.length">
         <el-col :span="24">
           <el-card>
@@ -108,7 +236,11 @@
           <el-card>
             <template slot="header">
               <div class="card-header">
-                <span>单品库存变化趋势(入库/销售/库存)</span>
+                <div>
+                  <span>单品库存变化趋势</span>
+                  <el-tag v-if="productData.sku || skuInput" size="mini" class="sku-tag">SKU: {{ productData.sku || skuInput }}</el-tag>
+                </div>
+                <el-tag size="mini" :type="statusTagType">{{ inventoryHealthStatus }}</el-tag>
               </div>
             </template>
             <div ref="trendChart" class="trend-chart" />
@@ -116,45 +248,6 @@
         </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" />
@@ -175,34 +268,120 @@ export default {
       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)'
-      }
+      waterlineLoading: false,
+      skuSettings: [],
+      skuDraft: {
+        sku: '',
+        productType: 'normal',
+        safetyStockOverride: undefined,
+        safetyDaysOverride: undefined,
+        leadTimeOverride: undefined,
+        maxReplenishCycleOverride: undefined,
+        monthlyTargetOverride: undefined,
+        supplierDefectRateOverride: undefined,
+        updatedBy: ''
+      },
+      settingsVisible: false
     }
   },
   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]
+    endingInventory() {
+      if (!this.productData) return 0
+      const series = Array.isArray(this.productData.inventoryByDay) ? this.productData.inventoryByDay : []
+      if (series.length) return Number(series[series.length - 1] || 0)
+      return Number(this.productData.currentInventory || 0)
+    },
+    beginningInventory() {
+      if (!this.productData) return 0
+      const series = Array.isArray(this.productData.inventoryByDay) ? this.productData.inventoryByDay : []
+      if (series.length) return Number(series[0] || 0)
+      const purchase = Number(this.productData.purchaseQty || 0)
+      const sales = Number(this.productData.salesQty || 0)
+      return this.endingInventory - purchase + sales
     },
-    forecastDelta() {
-      if (this.forecastEnding === null) return null
-      const current = (this.productData && this.productData.currentInventory) || 0
-      return this.forecastEnding - current
+    endingInventoryGrowthRate() {
+      const base = Math.abs(this.beginningInventory)
+      if (base === 0) return this.endingInventory > 0 ? 100 : 0
+      return ((this.endingInventory - this.beginningInventory) / base) * 100
+    },
+    avgDailySales() {
+      if (!this.productData) return 0
+      const salesSeries = Array.isArray(this.productData.salesByDay) ? this.productData.salesByDay : []
+      if (salesSeries.length) {
+        const total = salesSeries.reduce((sum, value) => sum + Number(value || 0), 0)
+        return total / salesSeries.length
+      }
+      const days = Array.isArray(this.productData.dates) && this.productData.dates.length ? this.productData.dates.length : 30
+      return Number(this.productData.salesQty || 0) / days
+    },
+    overstockData() {
+      const stableSeries = this.productData && Array.isArray(this.productData.stableInventory) ? this.productData.stableInventory : []
+      const stable = stableSeries.length ? Number(stableSeries[stableSeries.length - 1] || 0) : Math.max(this.avgDailySales * 30, 0)
+      const quantity = Math.max(this.endingInventory - stable, 0)
+      const rate = stable > 0 ? quantity / stable : (quantity > 0 ? 1 : 0)
+      let level = '无明显积压'
+      if (rate >= 0.5) level = '严重积压'
+      else if (rate >= 0.2) level = '轻度积压'
+      return { quantity, level }
+    },
+    coverageDaysText() {
+      if (this.avgDailySales <= 0) return this.endingInventory > 0 ? '∞' : '0.0'
+      return (this.endingInventory / this.avgDailySales).toFixed(1)
+    },
+    inventoryHealthStatus() {
+      const turnover = Number(this.productData && this.productData.turnoverRate ? this.productData.turnoverRate : 0)
+      if (this.overstockData.level === '严重积压') return '积压'
+      if (this.endingInventory <= 0) return '缺货'
+      if (turnover >= 1.5) return '健康'
+      if (turnover >= 0.8) return '关注'
+      return '低周转'
+    },
+    statusTagType() {
+      if (this.inventoryHealthStatus === '健康') return 'success'
+      if (this.inventoryHealthStatus === '关注') return 'warning'
+      if (this.inventoryHealthStatus === '缺货' || this.inventoryHealthStatus === '积压') return 'danger'
+      return 'info'
+    },
+    currentSku() {
+      return String((this.productData && this.productData.sku) || this.skuInput || this.skuDraft.sku || '').trim().toUpperCase()
+    },
+    currentUserName() {
+      return this.$store && this.$store.getters ? (this.$store.getters.name || this.$store.getters.nickName || '') : ''
+    },
+    waterlineResultRows() {
+      if (!this.productData) return []
+      const setting = this.productData.waterlineSetting || {}
+      return [
+        {
+          name: 'W_MAX 最高水位',
+          formula: `MAX(日均销量 ${this.formatInventory(this.productData.dailySales)} x 最长补货周期 ${this.productData.maxReplenishCycle || 0} x 系数 ${setting.wmaxCycleFactor || 0}, 月销目标 ${this.formatInventory(this.productData.monthlyTarget)} x ${setting.monthlyTargetRatio || 0})`,
+          value: `${this.formatInventory(this.productData.waterlineMax)} 件`
+        },
+        {
+          name: 'ROP 补货点',
+          formula: `安全库存 ${this.formatInventory(this.productData.safetyStock)} + 日均销量 ${this.formatInventory(this.productData.dailySales)} x 补货提前期 ${this.productData.leadTime || 0}`,
+          value: `${this.formatInventory(this.productData.reorderPoint)} 件`
+        },
+        {
+          name: 'W_MIN / 有效库存',
+          formula: `当前库存 ${this.formatInventory(this.productData.currentInventory)} + 在途库存 ${this.formatInventory(this.productData.inTransit)} x (1 - 不合格率 ${this.formatInventory(this.productData.supplierDefectRate)}%)`,
+          value: `${this.formatInventory(this.productData.waterlineMin)} 件`
+        },
+        {
+          name: '安全库存 SS',
+          formula: setting.safetyStockOverride !== null && setting.safetyStockOverride !== undefined
+            ? 'SKU 手动安全库存覆盖'
+            : `日均销量 x 安全库存天数 ${setting.safetyDays || this.productData.safetyDays || 0}`,
+          value: `${this.formatInventory(this.productData.safetyStock)} 件`
+        }
+      ]
     }
   },
   mounted() {
+    this.fetchWaterlineSettings()
     window.addEventListener('resize', this.resizeChart)
   },
   activated() {
@@ -230,6 +409,90 @@ export default {
     formatTurnover(value) {
       return Number(value || 0).toFixed(2)
     },
+    formatPercent(value) {
+      const num = Number(value || 0)
+      return `${num >= 0 ? '+' : ''}${num.toFixed(1)}%`
+    },
+    typeLabel(type) {
+      const map = {
+        hot: '爆品',
+        normal: '常规品',
+        new: '新品',
+        slow: '长尾/滞销'
+      }
+      return map[type] || '常规品'
+    },
+    async fetchWaterlineSettings() {
+      this.waterlineLoading = true
+      try {
+        const res = await request({ url: '/api/inventory/waterline-settings', method: 'get' })
+        const data = this.normalizeResponse(res) || {}
+        this.skuSettings = Array.isArray(data.skuSettings) ? data.skuSettings : []
+        this.syncSkuDraftFromSettings()
+      } catch (error) {
+        console.error('获取水位设置失败:', error)
+      } finally {
+        this.waterlineLoading = false
+      }
+    },
+    async saveSkuSetting(row) {
+      const payload = { ...(row || {}) }
+      payload.sku = this.currentSku
+      delete payload.updatedBy
+      if (!payload.sku) {
+        Message.warning('请先查询 SKU')
+        return
+      }
+      try {
+        await request({
+          url: '/api/inventory/waterline-settings/sku',
+          method: 'post',
+          data: payload
+        })
+        Message.success('SKU 水位覆盖设置已保存')
+        await this.fetchWaterlineSettings()
+        if (this.skuInput && this.skuInput.trim().toUpperCase() === payload.sku) {
+          await this.searchProduct(true)
+        }
+      } catch (error) {
+        console.error('保存SKU水位覆盖设置失败:', error)
+        Message.error('保存失败')
+      }
+    },
+    syncSkuDraftFromSettings() {
+      const sku = String(this.skuInput || this.skuDraft.sku || '').trim().toUpperCase()
+      if (!sku) return
+      const existing = (this.skuSettings || []).find(row => String(row.sku || '').toUpperCase() === sku)
+      if (existing) {
+        this.skuDraft = { ...existing }
+        return
+      }
+      this.skuDraft = {
+        ...this.skuDraft,
+        sku,
+        productType: (this.productData && this.productData.productType) || this.skuDraft.productType || 'normal',
+        updatedBy: this.skuDraft.updatedBy || this.currentUserName || ''
+      }
+    },
+    applyProductToSkuDraft(data, sku) {
+      const code = String(sku || '').trim().toUpperCase()
+      const existing = (this.skuSettings || []).find(row => String(row.sku || '').toUpperCase() === code)
+      if (existing) {
+        this.skuDraft = { ...existing }
+        return
+      }
+      this.skuDraft = {
+        sku: code,
+        productType: (data && data.productType) || 'normal',
+        safetyStockOverride: undefined,
+        safetyDaysOverride: undefined,
+        leadTimeOverride: undefined,
+        maxReplenishCycleOverride: undefined,
+        monthlyTargetOverride: undefined,
+        supplierDefectRateOverride: undefined,
+        updatedBy: this.currentUserName || ''
+      }
+    },
     initTrendChart() {
       if (this.trendChartInstance || !this.$refs.trendChart) return
       this.trendChartInstance = echarts.init(this.$refs.trendChart, 'macarons')
@@ -237,30 +500,15 @@ export default {
     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天)']
+          data: ['入库', '销售', '当前库存', '稳定库存', 'W_MAX', 'ROP', 'W_MIN']
         },
         grid: {
           left: '3%',
@@ -289,13 +537,7 @@ export default {
             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)
-            }
+            smooth: true
           },
           {
             name: '稳定库存',
@@ -306,12 +548,28 @@ export default {
             smooth: true
           },
           {
-            name: '预测库存(30天)',
+            name: 'W_MAX',
             type: 'line',
-            data: this.productData.forecastInventory || [],
-            itemStyle: { color: '#ee6666' },
-            lineStyle: { width: 2 },
-            smooth: true
+            data: this.productData.waterlineMaxLine || [],
+            symbol: 'none',
+            itemStyle: { color: '#d48265' },
+            lineStyle: { width: 2, type: 'dashed' }
+          },
+          {
+            name: 'ROP',
+            type: 'line',
+            data: this.productData.reorderPointLine || [],
+            symbol: 'none',
+            itemStyle: { color: '#c23531' },
+            lineStyle: { width: 2, type: 'dotted' }
+          },
+          {
+            name: 'W_MIN',
+            type: 'line',
+            data: this.productData.waterlineMinLine || [],
+            symbol: 'none',
+            itemStyle: { color: '#2f4554' },
+            lineStyle: { width: 2, type: 'dashed' }
           }
         ]
       }
@@ -340,7 +598,7 @@ export default {
         }
       }
     },
-    async searchProduct() {
+    async searchProduct(keepSettingsVisible = false) {
       const sku = this.skuInput.trim()
       if (!sku) {
         Message.warning('请输入产品SKU')
@@ -352,6 +610,10 @@ export default {
         const data = await this.fetchProductTrend(sku)
         if (data) {
           this.productData = data
+          this.applyProductToSkuDraft(data, sku)
+          if (!keepSettingsVisible) {
+            this.settingsVisible = false
+          }
           this.$nextTick(() => {
             this.initTrendChart()
             this.updateTrendChart()
@@ -378,10 +640,54 @@ export default {
 <style scoped>
 .product-analysis {
   width: 100%;
+  padding: 4px 0 20px;
+}
+
+.analysis-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  margin-bottom: 16px;
+  padding: 14px 16px;
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 8px;
+}
+
+.toolbar-title h2 {
+  margin: 0 0 4px;
+  color: #1f2d3d;
+  font-size: 20px;
+  font-weight: 650;
+}
+
+.toolbar-title p {
+  margin: 0;
+  color: #909399;
+  font-size: 13px;
+}
+
+.toolbar-actions {
+  display: grid;
+  grid-template-columns: minmax(260px, 360px) auto auto;
+  gap: 10px;
+  align-items: center;
 }
 
-.search-card {
-  margin-bottom: 20px;
+.sku-waterline-card {
+  margin-bottom: 12px;
+  border-radius: 8px;
+}
+
+.waterline-note {
+  margin-top: 10px;
+  color: #909399;
+  font-size: 12px;
+}
+
+.waterline-result-row {
+  margin-bottom: 14px;
 }
 
 .product-info {
@@ -391,11 +697,11 @@ export default {
 }
 
 .metrics-row {
-  margin-bottom: 20px;
+  margin-bottom: 12px;
 }
 
 .turnover-row {
-  margin-bottom: 20px;
+  margin-bottom: 14px;
 }
 
 .turnover-period-label {
@@ -409,31 +715,45 @@ export default {
 }
 
 .metric-card {
-  text-align: center;
+  text-align: left;
   cursor: pointer;
-  transition: all 0.3s;
+  border-radius: 8px;
+  transition: border-color 0.2s, box-shadow 0.2s;
 }
 
 .metric-card:hover {
-  transform: translateY(-5px);
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  border-color: #b3d8ff;
+  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.08);
 }
 
 .metric-item {
-  padding: 10px;
+  min-height: 86px;
+  padding: 4px 2px;
 }
 
 .metric-label {
-  font-size: 14px;
+  font-size: 13px;
   color: #909399;
-  margin-bottom: 10px;
+  margin-bottom: 8px;
 }
 
 .metric-value {
-  font-size: 32px;
-  font-weight: bold;
+  font-size: 26px;
+  font-weight: 650;
   color: #303133;
-  margin-bottom: 5px;
+  margin-bottom: 4px;
+}
+
+.metric-positive {
+  color: #67c23a;
+}
+
+.metric-negative {
+  color: #f56c6c;
+}
+
+.metric-status {
+  font-size: 24px;
 }
 
 .metric-unit {
@@ -442,77 +762,66 @@ export default {
 }
 
 .charts-row {
-  margin-bottom: 20px;
+  margin-bottom: 14px;
 }
 
-.insights-row {
-  margin-bottom: 20px;
-}
-
-.forecast-meta {
+.card-header {
   display: flex;
-  flex-direction: column;
-  gap: 10px;
-  padding: 10px 0;
-}
-
-.forecast-value {
-  font-size: 34px;
+  justify-content: space-between;
+  align-items: center;
+  gap: 12px;
+  min-height: 24px;
   font-weight: 600;
-  color: #303133;
 }
 
-.forecast-delta {
-  font-size: 14px;
-  font-weight: 500;
-  color: #606266;
+.trend-chart {
+  height: 380px;
 }
 
-.forecast-delta.positive {
-  color: #67c23a;
+.sku-tag {
+  margin-left: 8px;
 }
 
-.forecast-delta.negative {
-  color: #f56c6c;
+.sku-waterline-card /deep/ .el-input-number {
+  width: 100%;
 }
 
-.segment-timeline {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 12px;
+.sku-waterline-card /deep/ .el-input-number--mini .el-input__inner {
+  padding-left: 6px;
+  padding-right: 30px;
 }
 
-.segment-chip {
-  padding: 12px 14px;
-  border-radius: 10px;
+.sku-waterline-card /deep/ .el-table {
   color: #303133;
-  min-width: 180px;
-  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.05);
 }
 
-.segment-name {
-  font-size: 16px;
+.readonly-cell {
+  color: #303133;
   font-weight: 600;
 }
 
-.segment-range {
-  font-size: 12px;
-  color: #606266;
-  margin: 6px 0;
+.analysis-empty {
+  margin-top: 32px;
+  padding: 44px 0;
+  background: #fff;
+  border: 1px dashed #dcdfe6;
+  border-radius: 8px;
 }
 
-.segment-score {
-  font-size: 12px;
-  color: #909399;
-}
+@media (max-width: 1180px) {
+  .analysis-toolbar {
+    align-items: stretch;
+    flex-direction: column;
+  }
 
-.card-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
+  .toolbar-actions {
+    grid-template-columns: 1fr auto auto;
+  }
 }
 
-.trend-chart {
-  height: 400px;
+@media (max-width: 760px) {
+  .toolbar-actions {
+    grid-template-columns: 1fr;
+  }
 }
 </style>

+ 470 - 166
src/views/storage/overview/index.vue

@@ -1,5 +1,27 @@
 <template>
   <div class="inventory-overview">
+    <el-card class="filter-card">
+      <div class="filter-bar">
+        <span class="filter-label">统计日期</span>
+        <el-date-picker
+          v-model="dateRange"
+          type="daterange"
+          size="small"
+          value-format="yyyy-MM-dd"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          clearable
+        />
+        <el-button type="primary" size="small" @click="refreshData" :disabled="uploadTaskPolling">
+          <i class="el-icon-search"></i> 查询
+        </el-button>
+        <el-button size="small" @click="resetDateRange" :disabled="uploadTaskPolling">
+          <i class="el-icon-refresh-left"></i> 重置
+        </el-button>
+      </div>
+    </el-card>
+
     <el-row :gutter="20" class="metrics-row">
       <el-col :span="6">
         <el-card class="metric-card">
@@ -50,15 +72,46 @@
               <i class="el-icon-truck"></i>
             </div>
             <div class="metric-info">
-              <div class="metric-label">在途库存占比</div>
-              <div class="metric-value">{{ overviewData.inTransitRatio }}</div>
-              <div class="metric-unit">%</div>
+              <div class="metric-label">库销比(成品)</div>
+              <div class="metric-value">{{ overviewData.stockSalesRatio }}</div>
+              <div class="metric-unit">库存/销量</div>
             </div>
           </div>
         </el-card>
       </el-col>
     </el-row>
 
+    <el-row :gutter="20" class="waterline-row">
+      <el-col :span="6">
+        <el-card class="waterline-card danger" @click.native="openRiskDialog">
+          <div class="waterline-label">低于补货点</div>
+          <div class="waterline-value">{{ overviewData.waterlineBelowReorder }}</div>
+          <div class="waterline-sub">需要补货判断</div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="waterline-card warning" @click.native="openRiskDialog">
+          <div class="waterline-label">超过最高水位</div>
+          <div class="waterline-value">{{ overviewData.waterlineOverMax }}</div>
+          <div class="waterline-sub">超储/资金占用</div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="waterline-card info" @click.native="openRiskDialog">
+          <div class="waterline-label">呆滞预警</div>
+          <div class="waterline-value">{{ overviewData.waterlineSlowMoving }}</div>
+          <div class="waterline-sub">低动销库存</div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="waterline-card success">
+          <div class="waterline-label">健康水位</div>
+          <div class="waterline-value">{{ overviewData.waterlineHealthy }}</div>
+          <div class="waterline-sub">处于水位区间</div>
+        </el-card>
+      </el-col>
+    </el-row>
+
     <el-row :gutter="20" class="upload-row">
       <el-col :span="24">
         <el-card class="upload-card">
@@ -103,167 +156,46 @@
     </el-row>
 
     <el-row :gutter="20" class="charts-row">
-      <el-col :span="24">
+      <el-col :span="12">
         <el-card>
           <template slot="header">
-            <div class="card-header">
-              <span>SKU指标汇总</span>
-              <el-button type="primary" size="small" @click="refreshData" :disabled="uploadTaskPolling">
-                <i class="el-icon-refresh"></i> 刷新
-              </el-button>
-            </div>
+            <span>库存数量按产品类型分布</span>
           </template>
-          <div class="sku-filters">
-            属性:
-            <el-select v-model="skuFilters.attribute" placeholder="属性" size="small" style="width: 160px">
-              <el-option label="全部" value="" />
-              <el-option v-for="item in skuAttributeOptions" :key="item" :label="item" :value="item" />
-            </el-select>
-            分类:
-            <el-select v-model="skuFilters.category" placeholder="分类" size="small" style="width: 160px">
-              <el-option label="全部" value="" />
-              <el-option v-for="item in skuCategoryOptions" :key="item" :label="item" :value="item" />
-            </el-select>
-            SPU:
-            <el-select v-model="skuFilters.spu" placeholder="SPU" size="small" style="width: 200px">
-              <el-option label="全部" value="" />
-              <el-option v-for="item in skuSpuOptions" :key="item" :label="item" :value="item" />
-            </el-select>
-            <el-button size="small" @click="resetSkuFilters">重置</el-button>
-          </div>
-          <el-table
-            :data="pagedSkuData"
-            stripe
-            style="width: 100%"
-            v-loading="loading"
-            @sort-change="handleSkuSortChange"
-          >
-            <el-table-column prop="sku" label="SKU" width="200" fixed sortable="custom" />
-            <el-table-column prop="category" label="分类" width="140">
-              <template slot-scope="scope">
-                {{ scope.row.category || '-' }}
-              </template>
-            </el-table-column>
-            <el-table-column prop="attribute" label="属性" width="140">
-              <template slot-scope="scope">
-                {{ scope.row.attribute || '-' }}
-              </template>
-            </el-table-column>
-            <el-table-column prop="spuName" label="SPU" width="200">
-              <template slot-scope="scope">
-                {{ scope.row.spuName || '-' }}
-              </template>
-            </el-table-column>
-            <el-table-column prop="purchaseQty" label="入库数量" width="120" align="right" sortable="custom">
-              <template slot-scope="scope">
-                {{ formatNumber(scope.row.purchaseQty) }}
-              </template>
-            </el-table-column>
-            <el-table-column prop="salesQty" label="销售数量" width="120" align="right" sortable="custom">
-              <template slot-scope="scope">
-                {{ formatNumber(scope.row.salesQty) }}
-              </template>
-            </el-table-column>
-            <el-table-column prop="inventory" label="现有库存" width="120" align="right" sortable="custom">
-              <template slot-scope="scope">
-                {{ formatNumber(scope.row.inventory) }}
-              </template>
-            </el-table-column>
-            <el-table-column prop="purchaseAmount" label="入库总金额" width="150" align="right" sortable="custom">
-              <template slot-scope="scope">
-                {{ formatNumber(scope.row.purchaseAmount) }}
-              </template>
-            </el-table-column>
-            <el-table-column prop="amountRatio" label="入库资金占比(%)" width="150" align="right" sortable="custom">
-              <template slot-scope="scope">
-                {{ scope.row.amountRatio }}%
-              </template>
-            </el-table-column>
-            <el-table-column prop="turnoverRate" label="库存周转率" align="right" sortable="custom">
-              <template slot-scope="scope">
-                <el-tag :type="getTurnoverRateType(scope.row.turnoverRate)">
-                  {{ scope.row.turnoverRate }}
-                </el-tag>
-              </template>
-            </el-table-column>
-          </el-table>
-          <el-pagination
-            :current-page="skuCurrentPage"
-            :page-size="skuPageSize"
-            :total="skuTotal"
-            :page-sizes="[10, 20, 50, 100]"
-            layout="total, sizes, prev, pager, next, jumper"
-            class="table-pagination"
-            @current-change="handleSkuPageChange"
-            @size-change="handleSkuSizeChange"
-          />
+          <div ref="inventoryValuePieChart" class="chart" v-loading="categoryValueLoading" />
+          <el-empty v-if="!categoryValueLoading && categoryValueDistribution.length === 0" description="暂无数据" />
+        </el-card>
+      </el-col>
+      <el-col :span="12">
+        <el-card>
+          <template slot="header">
+            <span>周转率 Top10</span>
+          </template>
+          <div ref="turnoverTopChart" class="chart" v-loading="turnoverTopLoading" />
+          <el-empty v-if="!turnoverTopLoading && turnoverTopList.length === 0" description="暂无数据" />
         </el-card>
       </el-col>
     </el-row>
 
     <el-row :gutter="20" class="charts-row">
       <el-col :span="24">
-        <el-card>
+        <el-card class="risk-entry-card">
           <template slot="header">
             <div class="card-header">
-              <span>SPU指标汇总(成品)</span>
-              <el-button type="primary" size="small" @click="refreshData" :disabled="uploadTaskPolling">
-                <i class="el-icon-refresh"></i> 刷新
-              </el-button>
+              <span>风险产品详情</span>
+              <div class="card-actions">
+                <el-tag v-if="warningTotal > 0" type="danger">{{ warningTotal }} 个风险 SKU</el-tag>
+                <el-button type="primary" size="small" @click="openRiskDialog">
+                  <i class="el-icon-view"></i> 查看详情
+                </el-button>
+              </div>
             </div>
           </template>
-          <el-table
-            :data="pagedSpuData"
-            stripe
-            style="width: 100%"
-            v-loading="spuLoading"
-            @sort-change="handleSpuSortChange"
-          >
-            <el-table-column prop="spu" label="SPU" width="220" fixed sortable="custom" />
-            <el-table-column prop="skuCount" label="SKU数" width="90" align="right" sortable="custom" />
-            <el-table-column prop="purchaseQty" label="入库数量" width="120" align="right" sortable="custom">
-              <template slot-scope="scope">
-                {{ formatNumber(scope.row.purchaseQty) }}
-              </template>
-            </el-table-column>
-            <el-table-column prop="salesQty" label="销售数量" width="120" align="right" sortable="custom">
-              <template slot-scope="scope">
-                {{ formatNumber(scope.row.salesQty) }}
-              </template>
-            </el-table-column>
-            <el-table-column prop="inventory" label="现有库存" width="120" align="right" sortable="custom">
-              <template slot-scope="scope">
-                {{ formatNumber(scope.row.inventory) }}
-              </template>
-            </el-table-column>
-            <el-table-column prop="purchaseAmount" label="入库总金额" width="150" align="right" sortable="custom">
-              <template slot-scope="scope">
-                {{ formatNumber(scope.row.purchaseAmount) }}
-              </template>
-            </el-table-column>
-            <el-table-column prop="amountRatio" label="入库资金占比(%)" width="150" align="right" sortable="custom">
-              <template slot-scope="scope">
-                {{ scope.row.amountRatio }}%
-              </template>
-            </el-table-column>
-            <el-table-column prop="turnoverRate" label="库存周转率" align="right" sortable="custom">
-              <template slot-scope="scope">
-                <el-tag :type="getTurnoverRateType(scope.row.turnoverRate)">
-                  {{ scope.row.turnoverRate }}
-                </el-tag>
-              </template>
-            </el-table-column>
-          </el-table>
-          <el-pagination
-            :current-page="spuCurrentPage"
-            :page-size="spuPageSize"
-            :total="spuTotal"
-            :page-sizes="[10, 20, 50, 100]"
-            layout="total, sizes, prev, pager, next, jumper"
-            class="table-pagination"
-            @current-change="handleSpuPageChange"
-            @size-change="handleSpuSizeChange"
-          />
+          <div class="risk-entry-content">
+            <span v-if="warningTotal > 0">
+              当前存在 {{ warningTotal }} 个水位异常 SKU,包含低于补货点、超过最高水位和呆滞预警。
+            </span>
+            <span v-else>当前没有检测到水位异常 SKU。</span>
+          </div>
         </el-card>
       </el-col>
     </el-row>
@@ -278,7 +210,7 @@
         type="info"
         show-icon
         :closable="false"
-        title="至少上传入库数据与销售数据,组装/产品资料/半成品映射可选。"
+        title="可上传Excel进行分析;若不上传文件则默认从数据库读取。入库/销售为常用必传项,其余可选。"
         class="upload-alert"
       />
       <el-form label-width="110px" class="upload-form">
@@ -325,9 +257,86 @@
       <input ref="mappingInput" class="hidden-file-input" type="file" accept=".xlsx,.xls" @change="handleFileChange('mapping', $event)">
       <span slot="footer" class="dialog-footer">
         <el-button @click="uploadDialogVisible = false" :disabled="uploadLoading">取消</el-button>
-        <el-button type="primary" :loading="uploadLoading" @click="submitUpload">上传并处理</el-button>
+        <el-button type="primary" :loading="uploadLoading" @click="submitUpload">上传/读取并处理</el-button>
       </span>
     </el-dialog>
+
+    <el-dialog
+      title="风险产品水位详情"
+      :visible.sync="riskDialogVisible"
+      width="1180px"
+      top="6vh"
+    >
+      <el-table
+        :data="warningList"
+        stripe
+        max-height="560"
+        style="width: 100%"
+        v-loading="warningLoading"
+      >
+        <el-table-column prop="sku" label="产品编码" width="170" fixed />
+        <el-table-column prop="productName" label="产品名称" min-width="180">
+          <template slot-scope="scope">{{ scope.row.productName || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="风险类型" width="130">
+          <template slot-scope="scope">
+            <el-tag :type="getWaterlineStatusType(scope.row.waterlineStatus)">
+              {{ getWaterlineStatusText(scope.row.waterlineStatus) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="effectiveInventory" label="有效库存" width="100" align="right">
+          <template slot-scope="scope">{{ formatNumber(scope.row.effectiveInventory) }}</template>
+        </el-table-column>
+        <el-table-column prop="remainingInventory" label="现有库存" width="100" align="right">
+          <template slot-scope="scope">{{ formatNumber(scope.row.remainingInventory) }}</template>
+        </el-table-column>
+        <el-table-column prop="inTransit" label="在途库存" width="100" align="right">
+          <template slot-scope="scope">{{ formatNumber(scope.row.inTransit) }}</template>
+        </el-table-column>
+        <el-table-column prop="qualifiedInTransit" label="折算在途" width="100" align="right">
+          <template slot-scope="scope">{{ formatNumber(scope.row.qualifiedInTransit) }}</template>
+        </el-table-column>
+        <el-table-column prop="supplierDefectRate" label="预测销量" width="100" align="right">
+          <template slot-scope="scope">{{ formatPercentPlain(scope.row.qualifiedInTransit) }}</template>
+        </el-table-column>
+        <el-table-column prop="reorderPoint" label="ROP" width="90" align="right">
+          <template slot-scope="scope">{{ formatNumber(scope.row.reorderPoint) }}</template>
+        </el-table-column>
+        <el-table-column prop="waterlineMax" label="W_MAX" width="100" align="right">
+          <template slot-scope="scope">{{ formatNumber(scope.row.waterlineMax) }}</template>
+        </el-table-column>
+        <el-table-column prop="safetyStock" label="SS" width="80" align="right">
+          <template slot-scope="scope">{{ formatNumber(scope.row.safetyStock) }}</template>
+        </el-table-column>
+        <el-table-column prop="leadTime" label="提前期" width="90" align="right">
+          <template slot-scope="scope">{{ scope.row.leadTime }} 天</template>
+        </el-table-column>
+        <el-table-column prop="dailySales" label="日销" width="80" align="right" />
+        <el-table-column prop="salableDays" label="可售天数" width="100" align="right">
+          <template slot-scope="scope">{{ formatSalableDays(scope.row.salableDays) }}</template>
+        </el-table-column>
+        <el-table-column prop="assemblyCapacity" label="可组" width="90" align="right">
+          <template slot-scope="scope">{{ formatNumber(scope.row.assemblyCapacity) }}</template>
+        </el-table-column>
+        <el-table-column prop="suggestedQty" label="建议量" width="90" align="right">
+          <template slot-scope="scope">{{ formatNumber(scope.row.suggestedQty) }}</template>
+        </el-table-column>
+        <el-table-column prop="riskReason" label="风险原因" min-width="220" />
+        <el-table-column prop="suggestedAction" label="建议动作" min-width="220" />
+      </el-table>
+      <el-empty v-if="!warningLoading && warningList.length === 0" description="暂无风险产品" />
+      <el-pagination
+        :current-page="warningCurrentPage"
+        :page-size="warningPageSize"
+        :total="warningTotal"
+        :page-sizes="[10, 20, 50, 100]"
+        layout="total, sizes, prev, pager, next, jumper"
+        class="table-pagination"
+        @current-change="handleWarningPageChange"
+        @size-change="handleWarningSizeChange"
+      />
+    </el-dialog>
   </div>
 </template>
 
@@ -347,11 +356,29 @@ export default {
         totalInventory: 0,
         totalValue: 0,
         turnoverRate: 0,
+        stockSalesRatio: 0,
         inTransitRatio: 0,
-        assemblyQty: 0
+        inTransitQty: 0,
+        qualifiedInTransitQty: 0,
+        assemblyQty: 0,
+        waterlineBelowReorder: 0,
+        waterlineOverMax: 0,
+        waterlineSlowMoving: 0,
+        waterlineHealthy: 0,
+        waterlineRiskTotal: 0
       },
+      dateRange: [],
       monthlyComparison: [],
       monthlyLoading: false,
+      categoryValueDistribution: [],
+      categoryValueLoading: false,
+      turnoverTopList: [],
+      turnoverTopLoading: false,
+      warningList: [],
+      warningLoading: false,
+      warningCurrentPage: 1,
+      warningPageSize: 10,
+      warningTotal: 0,
       skuTableData: [],
       spuTableData: [],
       loading: false,
@@ -374,6 +401,8 @@ export default {
       spuCurrentPage: 1,
       spuPageSize: 10,
       chartInstance: null,
+      valuePieChart: null,
+      turnoverTopChart: null,
       uploadDialogVisible: false,
       uploadLoading: false,
       uploadFiles: {
@@ -394,7 +423,8 @@ export default {
       uploadTaskId: '',
       uploadTaskStatus: '',
       uploadTaskStatusText: '',
-      uploadTaskPolling: false
+      uploadTaskPolling: false,
+      riskDialogVisible: false
     }
   },
   computed: {
@@ -490,11 +520,32 @@ export default {
       this.chartInstance.dispose()
       this.chartInstance = null
     }
+    if (this.valuePieChart) {
+      this.valuePieChart.dispose()
+      this.valuePieChart = null
+    }
+    if (this.turnoverTopChart) {
+      this.turnoverTopChart.dispose()
+      this.turnoverTopChart = null
+    }
   },
   methods: {
     inventoryRequest(config) {
       return request({ timeout: 120000, ...config })
     },
+    getDateParams() {
+      if (!Array.isArray(this.dateRange) || this.dateRange.length !== 2) {
+        return {}
+      }
+      return {
+        startDate: this.dateRange[0],
+        endDate: this.dateRange[1]
+      }
+    },
+    resetDateRange() {
+      this.dateRange = []
+      this.refreshData()
+    },
     normalizeResponse(res) {
       if (!res) return null
       if (res.code === 200) return res.data
@@ -502,12 +553,23 @@ export default {
       return null
     },
     initChart() {
-      if (this.chartInstance || !this.$refs.inventoryComparisonChart) return
-      this.chartInstance = echarts.init(this.$refs.inventoryComparisonChart, 'macarons')
+      if (!this.chartInstance && this.$refs.inventoryComparisonChart) {
+        this.chartInstance = echarts.init(this.$refs.inventoryComparisonChart, 'macarons')
+      }
+      if (!this.valuePieChart && this.$refs.inventoryValuePieChart) {
+        this.valuePieChart = echarts.init(this.$refs.inventoryValuePieChart, 'macarons')
+      }
+      if (!this.turnoverTopChart && this.$refs.turnoverTopChart) {
+        this.turnoverTopChart = echarts.init(this.$refs.turnoverTopChart, 'macarons')
+      }
       this.updateMonthlyChart()
+      this.updateCategoryValueChart()
+      this.updateTurnoverTopChart()
     },
     resizeChart() {
       if (this.chartInstance) this.chartInstance.resize()
+      if (this.valuePieChart) this.valuePieChart.resize()
+      if (this.turnoverTopChart) this.turnoverTopChart.resize()
     },
     updateMonthlyChart() {
       if (!this.chartInstance) return
@@ -542,9 +604,81 @@ export default {
       }
       this.chartInstance.setOption(option, true)
     },
+    updateCategoryValueChart() {
+      if (!this.valuePieChart) return
+      const data = Array.isArray(this.categoryValueDistribution) ? this.categoryValueDistribution : []
+      const option = {
+        tooltip: {
+          trigger: 'item',
+          formatter: params => {
+            const value = Number(params.value || 0).toLocaleString()
+            return `${params.name}<br/>库存数量: ${value} 件<br/>占比: ${params.percent}%`
+          }
+        },
+        legend: {
+          type: 'scroll',
+          orient: 'vertical',
+          right: 0,
+          top: 20,
+          bottom: 20
+        },
+        series: [
+          {
+            name: '库存数量',
+            type: 'pie',
+            radius: ['42%', '68%'],
+            center: ['38%', '50%'],
+            avoidLabelOverlap: true,
+            label: { formatter: '{b}: {d}%' },
+            data
+          }
+        ]
+      }
+      this.valuePieChart.setOption(option, true)
+    },
+    updateTurnoverTopChart() {
+      if (!this.turnoverTopChart) return
+      const rows = Array.isArray(this.turnoverTopList) ? this.turnoverTopList : []
+      const labels = rows.map(item => item.productName || item.spuName || item.sku)
+      const values = rows.map(item => item.turnoverRate || 0)
+      const option = {
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { type: 'shadow' },
+          formatter: params => {
+            const item = params && params.length ? params[0] : null
+            if (!item) return ''
+            const row = rows[item.dataIndex] || {}
+            return `${row.productName || row.spuName || row.sku}<br/>SKU: ${row.sku || '-'}<br/>周转率: ${item.value} 次`
+          }
+        },
+        grid: { left: '3%', right: '5%', bottom: '3%', containLabel: true },
+        xAxis: { type: 'value', name: '次' },
+        yAxis: {
+          type: 'category',
+          inverse: true,
+          data: labels,
+          axisLabel: {
+            width: 120,
+            overflow: 'truncate'
+          }
+        },
+        series: [
+          {
+            name: '周转率',
+            type: 'bar',
+            data: values,
+            barMaxWidth: 18,
+            itemStyle: { color: '#409eff' },
+            label: { show: true, position: 'right' }
+          }
+        ]
+      }
+      this.turnoverTopChart.setOption(option, true)
+    },
     async fetchOverviewData() {
       try {
-        const res = await this.inventoryRequest({ url: '/api/inventory/overview', method: 'get' })
+        const res = await this.inventoryRequest({ url: '/api/inventory/overview', method: 'get', params: this.getDateParams() })
         const data = this.normalizeResponse(res)
         if (data) this.overviewData = { ...this.overviewData, ...data }
       } catch (error) {
@@ -554,7 +688,7 @@ export default {
     async fetchMonthlyComparison() {
       this.monthlyLoading = true
       try {
-        const res = await this.inventoryRequest({ url: '/api/inventory/monthly-comparison', method: 'get' })
+        const res = await this.inventoryRequest({ url: '/api/inventory/monthly-comparison', method: 'get', params: this.getDateParams() })
         const data = this.normalizeResponse(res)
         if (data && Array.isArray(data.months)) {
           this.monthlyComparison = data.months.map((month, idx) => ({
@@ -575,6 +709,62 @@ export default {
         this.$nextTick(() => this.updateMonthlyChart())
       }
     },
+    async fetchCategoryValueDistribution() {
+      this.categoryValueLoading = true
+      try {
+        const res = await this.inventoryRequest({ url: '/api/inventory/category-value-distribution', method: 'get', params: this.getDateParams() })
+        const data = this.normalizeResponse(res)
+        this.categoryValueDistribution = Array.isArray(data) ? data : []
+      } catch (error) {
+        console.error('获取库存金额分类分布失败:', error)
+      } finally {
+        this.categoryValueLoading = false
+        this.$nextTick(() => this.updateCategoryValueChart())
+      }
+    },
+    async fetchTurnoverTop() {
+      this.turnoverTopLoading = true
+      try {
+        const res = await this.inventoryRequest({ url: '/api/inventory/turnover-top10', method: 'get', params: this.getDateParams() })
+        const data = this.normalizeResponse(res)
+        this.turnoverTopList = Array.isArray(data) ? data : []
+      } catch (error) {
+        console.error('获取周转率Top10失败:', error)
+      } finally {
+        this.turnoverTopLoading = false
+        this.$nextTick(() => this.updateTurnoverTopChart())
+      }
+    },
+    async fetchWarningList() {
+      this.warningLoading = true
+      try {
+        const res = await this.inventoryRequest({
+          url: '/api/inventory/low-salable-days',
+          method: 'get',
+          params: {
+            ...this.getDateParams(),
+            page: this.warningCurrentPage,
+            pageSize: this.warningPageSize
+          }
+        })
+        const data = this.normalizeResponse(res)
+        if (data && Array.isArray(data.list)) {
+          this.warningList = data.list
+          this.warningTotal = Number(data.total || 0)
+        } else {
+          this.warningList = Array.isArray(data) ? data : []
+          this.warningTotal = this.warningList.length
+        }
+      } catch (error) {
+        console.error('获取预警明细失败:', error)
+      } finally {
+        this.warningLoading = false
+      }
+    },
+    openRiskDialog() {
+      this.riskDialogVisible = true
+      this.fetchWarningList()
+    },
     async fetchSkuSummary() {
       this.loading = true
       try {
@@ -606,10 +796,12 @@ export default {
       }
     },
     async refreshData() {
+      this.warningCurrentPage = 1
       await this.fetchOverviewData()
       await this.fetchMonthlyComparison()
-      await this.fetchSkuSummary()
-      await this.fetchSpuSummary()
+      await this.fetchCategoryValueDistribution()
+      await this.fetchTurnoverTop()
+      await this.fetchWarningList()
     },
     openUploadDialog() {
       this.uploadDialogVisible = true
@@ -661,9 +853,9 @@ export default {
       return ['purchase', 'sales', 'assembly', 'product', 'mapping'].some(key => this.uploadFiles[key])
     },
     async submitUpload() {
-      if (!this.hasUploadFiles()) {
-        this.$message.error('请至少选择一个要上传的文件')
-        return
+      const useDb = !this.hasUploadFiles()
+      if (useDb) {
+        this.$message.info('未选择文件,将从数据库读取库存数据')
       }
       const formData = new FormData()
       if (this.uploadFiles.purchase) formData.append('purchaseFile', this.uploadFiles.purchase)
@@ -692,10 +884,13 @@ export default {
         this.uploadTaskId = res.data.taskId
         this.uploadTaskStatus = res.data.status || 'pending'
         this.uploadTaskStatusText = res.data.message || '任务已创建,等待后台处理'
-        this.lastUploadSummary = `已接收 ${res.data.count || 0} 个文件,任务ID:${this.uploadTaskId}`
+        const fileCount = res.data.count || 0
+        this.lastUploadSummary = fileCount > 0
+          ? `已接收 ${fileCount} 个文件,任务ID:${this.uploadTaskId}`
+          : `未上传文件,已切换为数据库读取,任务ID:${this.uploadTaskId}`
         this.uploadDialogVisible = false
         this.resetUploadForm()
-        this.$message.success('上传成功,后台正在处理库存数据')
+        this.$message.success(useDb ? '已提交,后台将从数据库读取并处理库存数据' : '上传成功,后台正在处理库存数据')
         await this.pollUploadTask(this.uploadTaskId)
       } catch (error) {
         console.error('上传库存数据失败:', error)
@@ -783,6 +978,15 @@ export default {
       this.spuSort = { prop, order }
       this.spuCurrentPage = 1
     },
+    handleWarningPageChange(page) {
+      this.warningCurrentPage = page
+      this.fetchWarningList()
+    },
+    handleWarningSizeChange(size) {
+      this.warningPageSize = size
+      this.warningCurrentPage = 1
+      this.fetchWarningList()
+    },
     resetSkuFilters() {
       this.skuFilters.attribute = ''
       this.skuFilters.category = ''
@@ -796,9 +1000,36 @@ export default {
       if (rate >= 0.5) return 'warning'
       return 'danger'
     },
+    getWaterlineStatusType(status) {
+      const map = {
+        below_reorder: 'danger',
+        over_max: 'warning',
+        slow_moving: 'info',
+        healthy: 'success'
+      }
+      return map[status] || 'info'
+    },
+    getWaterlineStatusText(status) {
+      const map = {
+        below_reorder: '低于补货点',
+        over_max: '超过最高水位',
+        slow_moving: '呆滞预警',
+        healthy: '健康'
+      }
+      return map[status] || '未知'
+    },
+    formatSalableDays(value) {
+      const num = Number(value || 0)
+      if (num >= 9999) return '无销量'
+      return num.toLocaleString()
+    },
     formatNumber(value) {
       const num = Number(value || 0)
       return num.toLocaleString()
+    },
+    formatPercentPlain(value) {
+      const num = Number(value || 0)
+      return `${num.toFixed(2)}%`
     }
   }
 }
@@ -809,10 +1040,71 @@ export default {
   width: 100%;
 }
 
+.filter-card {
+  margin-bottom: 20px;
+}
+
+.filter-bar {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  flex-wrap: wrap;
+}
+
+.filter-label {
+  color: #606266;
+  font-size: 14px;
+}
+
 .metrics-row {
   margin-bottom: 20px;
 }
 
+.waterline-row {
+  margin-bottom: 20px;
+}
+
+.waterline-card {
+  cursor: pointer;
+  border-left: 4px solid #dcdfe6;
+}
+
+.waterline-card.danger {
+  border-left-color: #f56c6c;
+}
+
+.waterline-card.warning {
+  border-left-color: #e6a23c;
+}
+
+.waterline-card.info {
+  border-left-color: #409eff;
+}
+
+.waterline-card.success {
+  cursor: default;
+  border-left-color: #67c23a;
+}
+
+.waterline-label {
+  color: #606266;
+  font-size: 14px;
+}
+
+.waterline-value {
+  margin-top: 8px;
+  color: #303133;
+  font-size: 30px;
+  font-weight: 700;
+  line-height: 1;
+}
+
+.waterline-sub {
+  margin-top: 8px;
+  color: #909399;
+  font-size: 12px;
+}
+
 .metric-card {
   cursor: pointer;
   transition: transform 0.3s;
@@ -872,6 +1164,18 @@ export default {
   margin-bottom: 20px;
 }
 
+.risk-entry-card .card-actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.risk-entry-content {
+  color: #606266;
+  font-size: 14px;
+  line-height: 1.7;
+}
+
 .upload-card .card-actions {
   display: flex;
   gap: 8px;

+ 475 - 0
src/views/storage/stock/index.vue

@@ -0,0 +1,475 @@
+<template>
+  <div class="capacity-management">
+    <el-card class="setting-card">
+      <div class="setting-bar">
+        <div class="setting-item">
+          <span class="setting-label">总库容边界</span>
+          <el-input-number v-model="capacityLimit" :min="1" :step="1000" size="small" />
+          <span class="setting-unit">件</span>
+        </div>
+        <div class="setting-item">
+          <span class="setting-label">爆仓预警线</span>
+          <el-input-number v-model="warningBoundary" :min="50" :max="100" :step="1" size="small" />
+          <span class="setting-unit">%</span>
+        </div>
+        <el-button type="primary" size="small" @click="refreshData" :loading="loading">
+          <i class="el-icon-refresh"></i> 刷新
+        </el-button>
+      </div>
+    </el-card>
+
+    <el-row :gutter="20" class="metrics-row">
+      <el-col :span="6">
+        <el-card class="metric-card">
+          <div class="metric-content">
+            <div class="metric-icon blue"><i class="el-icon-data-analysis"></i></div>
+            <div class="metric-info">
+              <div class="metric-label">库容利用率</div>
+              <div class="metric-value">{{ formatPercent(capacityUtilization) }}</div>
+              <div class="metric-unit">当前库存 / 总库容</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="metric-card">
+          <div class="metric-content">
+            <div class="metric-icon green"><i class="el-icon-box"></i></div>
+            <div class="metric-info">
+              <div class="metric-label">根据库容边界可存储量</div>
+              <div class="metric-value">{{ formatInteger(availableByBoundary) }}</div>
+              <div class="metric-unit">件</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="metric-card">
+          <div class="metric-content">
+            <div class="metric-icon orange"><i class="el-icon-warning-outline"></i></div>
+            <div class="metric-info">
+              <div class="metric-label">爆仓预警</div>
+              <div class="metric-value status-value" :class="overflowWarning.type">{{ overflowWarning.text }}</div>
+              <div class="metric-unit">预警线 {{ warningBoundary }}%</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card class="metric-card">
+          <div class="metric-content">
+            <div class="metric-icon red"><i class="el-icon-s-grid"></i></div>
+            <div class="metric-info">
+              <div class="metric-label">当前库存占用</div>
+              <div class="metric-value">{{ formatInteger(currentInventory) }}</div>
+              <div class="metric-unit">件</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="20" class="charts-row">
+      <el-col :span="12">
+        <el-card>
+          <template slot="header">
+            <span>各品类库存库容占比</span>
+          </template>
+          <div ref="categoryCapacityChart" class="chart" />
+          <el-empty v-if="!loading && categoryCapacityRows.length === 0" description="暂无品类库存数据" />
+        </el-card>
+      </el-col>
+      <el-col :span="12">
+        <el-card>
+          <template slot="header">
+            <span>库容边界结构</span>
+          </template>
+          <div ref="capacityGaugeChart" class="chart" />
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-card class="table-card">
+      <template slot="header">
+        <div class="card-header">
+          <span>品类库容明细</span>
+          <el-tag :type="overflowWarning.tagType">{{ overflowWarning.text }}</el-tag>
+        </div>
+      </template>
+      <el-table :data="categoryCapacityRows" stripe v-loading="loading">
+        <el-table-column prop="category" label="品类" min-width="160" />
+        <el-table-column label="库存量" width="140" align="right">
+          <template slot-scope="scope">{{ formatInteger(scope.row.inventory) }}</template>
+        </el-table-column>
+        <el-table-column label="占总库容" width="140" align="right">
+          <template slot-scope="scope">{{ formatPercent(scope.row.capacityRatio) }}</template>
+        </el-table-column>
+        <el-table-column label="占当前库存" width="140" align="right">
+          <template slot-scope="scope">{{ formatPercent(scope.row.inventoryRatio) }}</template>
+        </el-table-column>
+        <el-table-column label="边界剩余可存" width="160" align="right">
+          <template slot-scope="scope">{{ formatInteger(scope.row.availableByBoundary) }}</template>
+        </el-table-column>
+        <el-table-column label="状态" width="120">
+          <template slot-scope="scope">
+            <el-tag :type="getCategoryStatus(scope.row).type">{{ getCategoryStatus(scope.row).text }}</el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import request from '@/utils/request'
+import * as echarts from 'echarts'
+require('echarts/theme/macarons')
+
+export default {
+  name: 'StorageStock',
+  data() {
+    return {
+      loading: false,
+      capacityLimit: 100000,
+      warningBoundary: 85,
+      overviewData: {
+        totalInventory: 0
+      },
+      skuRows: [],
+      categoryCapacityChart: null,
+      capacityGaugeChart: null
+    }
+  },
+  computed: {
+    currentInventory() {
+      const totalFromSku = this.skuRows.reduce((sum, row) => sum + this.getRowInventory(row), 0)
+      return totalFromSku > 0 ? totalFromSku : Number(this.overviewData.totalInventory || 0)
+    },
+    boundaryCapacity() {
+      return Number(this.capacityLimit || 0) * Number(this.warningBoundary || 0) / 100
+    },
+    capacityUtilization() {
+      if (!this.capacityLimit) return 0
+      return this.currentInventory / this.capacityLimit * 100
+    },
+    availableByBoundary() {
+      return Math.max(this.boundaryCapacity - this.currentInventory, 0)
+    },
+    overflowWarning() {
+      if (this.currentInventory >= this.capacityLimit) {
+        return { text: '已爆仓', type: 'danger', tagType: 'danger' }
+      }
+      if (this.currentInventory >= this.boundaryCapacity) {
+        return { text: '接近爆仓', type: 'warning', tagType: 'warning' }
+      }
+      return { text: '库容正常', type: 'success', tagType: 'success' }
+    },
+    categoryCapacityRows() {
+      const map = {}
+      this.skuRows.forEach(row => {
+        const category = row.category || row.productType || row.attribute || '未分类'
+        if (!map[category]) map[category] = 0
+        map[category] += this.getRowInventory(row)
+      })
+      return Object.keys(map)
+        .map(category => {
+          const inventory = map[category]
+          return {
+            category,
+            inventory,
+            capacityRatio: this.capacityLimit ? inventory / this.capacityLimit * 100 : 0,
+            inventoryRatio: this.currentInventory ? inventory / this.currentInventory * 100 : 0,
+            availableByBoundary: Math.max(this.boundaryCapacity * (inventory / Math.max(this.currentInventory, 1)) - inventory, 0)
+          }
+        })
+        .sort((a, b) => b.inventory - a.inventory)
+    }
+  },
+  mounted() {
+    this.loadCapacitySettings()
+    this.refreshData()
+    window.addEventListener('resize', this.resizeCharts)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.resizeCharts)
+    if (this.categoryCapacityChart) this.categoryCapacityChart.dispose()
+    if (this.capacityGaugeChart) this.capacityGaugeChart.dispose()
+  },
+  watch: {
+    capacityLimit() {
+      this.saveCapacitySettings()
+      this.$nextTick(this.updateCharts)
+    },
+    warningBoundary() {
+      this.saveCapacitySettings()
+      this.$nextTick(this.updateCharts)
+    },
+    categoryCapacityRows() {
+      this.$nextTick(this.updateCharts)
+    }
+  },
+  methods: {
+    normalizeResponse(res) {
+      if (!res) return null
+      if (res.code === 200) return res.data
+      if (res.data) return res.data
+      return null
+    },
+    loadCapacitySettings() {
+      try {
+        const raw = localStorage.getItem('storage-capacity-settings')
+        if (!raw) return
+        const data = JSON.parse(raw)
+        if (data.capacityLimit) this.capacityLimit = Number(data.capacityLimit)
+        if (data.warningBoundary) this.warningBoundary = Number(data.warningBoundary)
+      } catch (e) {
+        console.error('读取库容设置失败:', e)
+      }
+    },
+    saveCapacitySettings() {
+      localStorage.setItem('storage-capacity-settings', JSON.stringify({
+        capacityLimit: this.capacityLimit,
+        warningBoundary: this.warningBoundary
+      }))
+    },
+    async refreshData() {
+      this.loading = true
+      try {
+        await Promise.all([this.fetchOverview(), this.fetchSkuSummary()])
+      } finally {
+        this.loading = false
+        this.$nextTick(() => {
+          this.initCharts()
+          this.updateCharts()
+        })
+      }
+    },
+    async fetchOverview() {
+      try {
+        const res = await request({ url: '/api/inventory/overview', method: 'get', timeout: 120000 })
+        const data = this.normalizeResponse(res)
+        if (data) this.overviewData = { ...this.overviewData, ...data }
+      } catch (e) {
+        console.error('获取库存概览失败:', e)
+      }
+    },
+    async fetchSkuSummary() {
+      try {
+        const res = await request({ url: '/api/inventory/sku-summary', method: 'get', timeout: 120000 })
+        const data = this.normalizeResponse(res)
+        this.skuRows = Array.isArray(data) ? data : []
+      } catch (e) {
+        console.error('获取SKU库存失败:', e)
+        this.skuRows = []
+      }
+    },
+    getRowInventory(row) {
+      return Number(row.inventory || row.currentInventory || row.remainingInventory || row.quantity || 0)
+    },
+    initCharts() {
+      if (!this.categoryCapacityChart && this.$refs.categoryCapacityChart) {
+        this.categoryCapacityChart = echarts.init(this.$refs.categoryCapacityChart, 'macarons')
+      }
+      if (!this.capacityGaugeChart && this.$refs.capacityGaugeChart) {
+        this.capacityGaugeChart = echarts.init(this.$refs.capacityGaugeChart, 'macarons')
+      }
+    },
+    updateCharts() {
+      this.updateCategoryChart()
+      this.updateGaugeChart()
+    },
+    updateCategoryChart() {
+      if (!this.categoryCapacityChart) return
+      const rows = this.categoryCapacityRows
+      this.categoryCapacityChart.setOption({
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { type: 'shadow' },
+          formatter: params => {
+            const item = params && params.length ? params[0] : null
+            const row = item ? rows[item.dataIndex] : null
+            if (!row) return ''
+            return `${row.category}<br/>库存: ${this.formatInteger(row.inventory)} 件<br/>占总库容: ${this.formatPercent(row.capacityRatio)}`
+          }
+        },
+        grid: { left: '3%', right: '5%', bottom: '3%', containLabel: true },
+        xAxis: { type: 'value', name: '%' },
+        yAxis: {
+          type: 'category',
+          inverse: true,
+          data: rows.map(item => item.category),
+          axisLabel: { width: 110, overflow: 'truncate' }
+        },
+        series: [{
+          name: '库容占比',
+          type: 'bar',
+          barMaxWidth: 18,
+          data: rows.map(item => Number(item.capacityRatio.toFixed(2))),
+          itemStyle: { color: '#409eff' },
+          label: { show: true, position: 'right', formatter: '{c}%' }
+        }]
+      }, true)
+    },
+    updateGaugeChart() {
+      if (!this.capacityGaugeChart) return
+      this.capacityGaugeChart.setOption({
+        tooltip: {
+          formatter: `库容利用率<br/>${this.formatPercent(this.capacityUtilization)}`
+        },
+        series: [{
+          name: '库容利用率',
+          type: 'gauge',
+          min: 0,
+          max: 100,
+          progress: { show: true, width: 14 },
+          axisLine: { lineStyle: { width: 14 } },
+          pointer: { width: 4 },
+          detail: {
+            formatter: value => `${Number(value || 0).toFixed(1)}%`,
+            fontSize: 24
+          },
+          data: [{ value: Number(this.capacityUtilization.toFixed(1)), name: `预警线 ${this.warningBoundary}%` }]
+        }]
+      }, true)
+    },
+    resizeCharts() {
+      if (this.categoryCapacityChart) this.categoryCapacityChart.resize()
+      if (this.capacityGaugeChart) this.capacityGaugeChart.resize()
+    },
+    getCategoryStatus(row) {
+      if (row.capacityRatio >= this.warningBoundary) return { text: '高占用', type: 'danger' }
+      if (row.capacityRatio >= this.warningBoundary * 0.7) return { text: '偏高', type: 'warning' }
+      return { text: '正常', type: 'success' }
+    },
+    formatInteger(value) {
+      return Number(value || 0).toLocaleString()
+    },
+    formatPercent(value) {
+      return `${Number(value || 0).toFixed(1)}%`
+    }
+  }
+}
+</script>
+
+<style scoped>
+.capacity-management {
+  width: 100%;
+}
+
+.setting-card,
+.metrics-row,
+.charts-row,
+.table-card {
+  margin-bottom: 20px;
+}
+
+.setting-bar {
+  display: flex;
+  align-items: center;
+  gap: 18px;
+  flex-wrap: wrap;
+}
+
+.setting-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.setting-label {
+  font-size: 14px;
+  color: #606266;
+}
+
+.setting-unit,
+.metric-unit {
+  font-size: 12px;
+  color: #909399;
+}
+
+.metric-card {
+  transition: transform 0.3s;
+}
+
+.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-icon {
+  width: 58px;
+  height: 58px;
+  border-radius: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #fff;
+  font-size: 28px;
+}
+
+.metric-icon.blue {
+  background: #409eff;
+}
+
+.metric-icon.green {
+  background: #67c23a;
+}
+
+.metric-icon.orange {
+  background: #e6a23c;
+}
+
+.metric-icon.red {
+  background: #f56c6c;
+}
+
+.metric-info {
+  min-width: 0;
+  flex: 1;
+}
+
+.metric-label {
+  font-size: 14px;
+  color: #909399;
+  margin-bottom: 8px;
+}
+
+.metric-value {
+  font-size: 28px;
+  font-weight: 700;
+  color: #303133;
+  line-height: 1.1;
+}
+
+.status-value {
+  font-size: 24px;
+}
+
+.status-value.success {
+  color: #67c23a;
+}
+
+.status-value.warning {
+  color: #e6a23c;
+}
+
+.status-value.danger {
+  color: #f56c6c;
+}
+
+.chart {
+  height: 320px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>

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

@@ -1,94 +1,99 @@
-<template>
-  <div class="settings">
-    <el-card>
+<template>
+  <div class="storage-settings">
+    <section class="settings-toolbar">
+      <div>
+        <h2>库存水位设置</h2>
+        <p>按产品类型维护默认库存水位参数,SKU 的单独覆盖在单品分析页设置。</p>
+      </div>
+      <el-button size="small" icon="el-icon-refresh" :loading="loading" @click="load">
+        刷新
+      </el-button>
+    </section>
+
+    <el-row :gutter="16" class="formula-row">
+      <el-col :span="8">
+        <el-card class="formula-card">
+          <div class="formula-title">W_MAX 最高水位</div>
+          <div class="formula-text">MAX(日均销量 x 最长补货周期 x W_MAX系数, 月销售目标 x 月销目标比例)</div>
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card class="formula-card">
+          <div class="formula-title">ROP 补货点</div>
+          <div class="formula-text">安全库存 SS + 日均销量 x 补货提前期 Lead Time</div>
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card class="formula-card">
+          <div class="formula-title">W_MIN 有效库存</div>
+          <div class="formula-text">当前库存 + 在途库存 x (1 - 供应商历史不合格率)</div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-card class="type-settings-card">
       <template slot="header">
         <div class="card-header">
-          <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>
+          <span>产品类型默认水位线</span>
+          <span class="header-note">修改人由系统按当前登录账号自动记录</span>
         </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-table :data="typeSettings" border size="small" v-loading="loading">
+        <el-table-column label="产品类型" width="120" fixed>
+          <template slot-scope="scope">
+            <el-tag size="mini">{{ typeLabel(scope.row.productType) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="默认安全库存天数" width="160">
+          <template slot-scope="scope">
+            <el-input-number v-model="scope.row.safetyDaysDefault" :min="0" :step="1" size="mini" controls-position="right" />
+          </template>
+        </el-table-column>
+        <el-table-column label="默认补货提前期" width="160">
+          <template slot-scope="scope">
+            <el-input-number v-model="scope.row.leadTimeDefault" :min="0" :step="1" size="mini" controls-position="right" />
+          </template>
+        </el-table-column>
+        <el-table-column label="最长补货周期" width="150">
+          <template slot-scope="scope">
+            <el-input-number v-model="scope.row.maxReplenishCycle" :min="0" :step="1" size="mini" controls-position="right" />
+          </template>
+        </el-table-column>
+        <el-table-column label="W_MAX系数" width="130">
+          <template slot-scope="scope">
+            <el-input-number v-model="scope.row.wmaxCycleFactor" :min="0" :step="0.1" size="mini" controls-position="right" />
+          </template>
+        </el-table-column>
+        <el-table-column label="月销目标比例" width="140">
+          <template slot-scope="scope">
+            <el-input-number v-model="scope.row.monthlyTargetRatio" :min="0" :step="0.05" size="mini" controls-position="right" />
+          </template>
+        </el-table-column>
+        <el-table-column label="目标覆盖天数" width="140">
+          <template slot-scope="scope">
+            <el-input-number v-model="scope.row.targetCoverageDays" :min="0" :step="1" size="mini" controls-position="right" />
+          </template>
+        </el-table-column>
+        <el-table-column label="目标周转率" width="130">
+          <template slot-scope="scope">
+            <el-input-number v-model="scope.row.targetTurnoverRate" :min="0" :step="0.1" size="mini" controls-position="right" />
+          </template>
+        </el-table-column>
+        <el-table-column label="修改人" width="120">
+          <template slot-scope="scope">{{ scope.row.updatedBy || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="修改时间" width="170">
+          <template slot-scope="scope">{{ scope.row.updatedTime || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="操作" width="100" fixed="right">
+          <template slot-scope="scope">
+            <el-button type="primary" size="mini" :loading="savingKey === scope.row.productType" @click="saveTypeSetting(scope.row)">
+              保存
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
     </el-card>
   </div>
 </template>
@@ -101,114 +106,148 @@ 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)
+      loading: false,
+      savingKey: '',
+      typeSettings: []
     }
   },
   mounted() {
     this.load()
   },
   methods: {
-    formatTooltip(value) {
-      return Number(value).toFixed(2)
+    typeLabel(type) {
+      const map = {
+        hot: '爆品',
+        normal: '常规品',
+        new: '新品',
+        slow: '长尾/滞销'
+      }
+      return map[type] || '常规品'
+    },
+    normalizeResponse(res) {
+      if (!res) return null
+      if (res.code === 200) return res.data
+      if (res.data) return res.data
+      return null
     },
     async load() {
+      this.loading = true
       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
-        }
+        const res = await request({ url: '/api/inventory/waterline-settings', method: 'get' })
+        const data = this.normalizeResponse(res) || {}
+        this.typeSettings = Array.isArray(data.typeSettings) ? data.typeSettings : []
       } catch (e) {
+        console.error('加载库存水位设置失败:', e)
         Message.error('加载设置失败')
+      } finally {
+        this.loading = false
       }
     },
-    async save() {
-      this.saving = true
+    async saveTypeSetting(row) {
+      if (!row) return
+      this.savingKey = row.productType
       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('设置已保存')
-        }
+        await request({
+          url: '/api/inventory/waterline-settings/type',
+          method: 'post',
+          data: row
+        })
+        Message.success('产品类型水位设置已保存')
+        await this.load()
       } catch (e) {
+        console.error('保存产品类型水位设置失败:', 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 }
+        this.savingKey = ''
       }
-      Message.success('已恢复默认权重')
     }
   }
 }
 </script>
 
 <style scoped>
-.settings {
+.storage-settings {
   width: 100%;
+  padding: 4px 0 20px;
 }
 
-.card-header {
+.settings-toolbar {
   display: flex;
-  justify-content: space-between;
   align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  margin-bottom: 16px;
+  padding: 14px 16px;
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 8px;
+}
+
+.settings-toolbar h2 {
+  margin: 0 0 4px;
+  color: #1f2d3d;
+  font-size: 20px;
+  font-weight: 600;
 }
 
-.header-actions {
+.settings-toolbar p {
+  margin: 0;
+  color: #909399;
+  font-size: 13px;
+}
+
+.formula-row {
+  margin-bottom: 12px;
+}
+
+.formula-card {
+  min-height: 112px;
+  border-radius: 8px;
+}
+
+.formula-title {
+  margin-bottom: 10px;
+  color: #303133;
+  font-weight: 600;
+}
+
+.formula-text {
+  color: #606266;
+  line-height: 1.6;
+  font-size: 13px;
+}
+
+.type-settings-card {
+  border-radius: 8px;
+}
+
+.card-header {
   display: flex;
-  gap: 10px;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  font-weight: 600;
+}
+
+.header-note {
+  color: #909399;
+  font-size: 12px;
+  font-weight: 400;
+}
+
+.type-settings-card /deep/ .el-input-number {
+  width: 100%;
+}
+
+.type-settings-card /deep/ .el-input-number--mini .el-input__inner {
+  padding-left: 6px;
+  padding-right: 30px;
+}
+
+@media (max-width: 900px) {
+  .settings-toolbar {
+    align-items: stretch;
+    flex-direction: column;
+  }
 }
 </style>