|
|
@@ -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;
|