|
|
@@ -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">
|
|
|
@@ -8,10 +30,9 @@
|
|
|
<i class="el-icon-box"></i>
|
|
|
</div>
|
|
|
<div class="metric-info">
|
|
|
- <div class="metric-label">总体库存量</div>
|
|
|
+ <div class="metric-label">总体库存量(成品)</div>
|
|
|
<div class="metric-value">{{ overviewData.totalInventory }}</div>
|
|
|
<div class="metric-unit">件</div>
|
|
|
- <div class="metric-sub">含组装转入 {{ overviewData.assemblyQty || 0 }} 件</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</el-card>
|
|
|
@@ -23,7 +44,7 @@
|
|
|
<i class="el-icon-money"></i>
|
|
|
</div>
|
|
|
<div class="metric-info">
|
|
|
- <div class="metric-label">库存价值</div>
|
|
|
+ <div class="metric-label">库存价值(成品)</div>
|
|
|
<div class="metric-value">{{ overviewData.totalValue }}</div>
|
|
|
<div class="metric-unit">万元</div>
|
|
|
</div>
|
|
|
@@ -37,7 +58,7 @@
|
|
|
<i class="el-icon-refresh"></i>
|
|
|
</div>
|
|
|
<div class="metric-info">
|
|
|
- <div class="metric-label">库存周转率</div>
|
|
|
+ <div class="metric-label">库存周转率(成品)</div>
|
|
|
<div class="metric-value">{{ overviewData.turnoverRate }}</div>
|
|
|
<div class="metric-unit">次</div>
|
|
|
</div>
|
|
|
@@ -51,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">
|
|
|
@@ -95,7 +147,7 @@
|
|
|
<el-col :span="24">
|
|
|
<el-card>
|
|
|
<template slot="header">
|
|
|
- <span>入库/销售/库存对比</span>
|
|
|
+ <span>成品入库/销售/库存对比</span>
|
|
|
</template>
|
|
|
<div ref="inventoryComparisonChart" class="chart" />
|
|
|
<el-empty v-if="!monthlyLoading && monthlyComparison.length === 0" description="暂无数据" />
|
|
|
@@ -104,117 +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>
|
|
|
- <el-table :data="skuTableData" stripe style="width: 100%" v-loading="loading">
|
|
|
- <el-table-column prop="sku" label="SKU" width="200" fixed />
|
|
|
- <el-table-column prop="attribute" label="属性" width="140">
|
|
|
- <template 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">
|
|
|
- <template slot-scope="scope">
|
|
|
- {{ formatNumber(scope.row.purchaseQty) }}
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="salesQty" label="销售数量" width="120" align="right">
|
|
|
- <template slot-scope="scope">
|
|
|
- {{ formatNumber(scope.row.salesQty) }}
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="inventory" label="现有库存" width="120" align="right">
|
|
|
- <template slot-scope="scope">
|
|
|
- {{ formatNumber(scope.row.inventory) }}
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="purchaseAmount" label="入库总金额" width="150" align="right">
|
|
|
- <template slot-scope="scope">
|
|
|
- {{ formatNumber(scope.row.purchaseAmount) }}
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="amountRatio" label="入库资金占比(%)" width="150" align="right">
|
|
|
- <template slot-scope="scope">
|
|
|
- {{ scope.row.amountRatio }}%
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="turnoverRate" label="库存周转率" align="right">
|
|
|
- <template slot-scope="scope">
|
|
|
- <el-tag :type="getTurnoverRateType(scope.row.turnoverRate)">
|
|
|
- {{ scope.row.turnoverRate }}
|
|
|
- </el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
+ <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="spuTableData" stripe style="width: 100%" v-loading="spuLoading">
|
|
|
- <el-table-column prop="spu" label="SPU" width="220" fixed />
|
|
|
- <el-table-column prop="attribute" label="属性" width="140">
|
|
|
- <template slot-scope="scope">
|
|
|
- {{ scope.row.attribute || '-' }}
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="skuCount" label="SKU数" width="90" align="right" />
|
|
|
- <el-table-column prop="purchaseQty" label="入库数量" width="120" align="right">
|
|
|
- <template slot-scope="scope">
|
|
|
- {{ formatNumber(scope.row.purchaseQty) }}
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="salesQty" label="销售数量" width="120" align="right">
|
|
|
- <template slot-scope="scope">
|
|
|
- {{ formatNumber(scope.row.salesQty) }}
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="inventory" label="现有库存" width="120" align="right">
|
|
|
- <template slot-scope="scope">
|
|
|
- {{ formatNumber(scope.row.inventory) }}
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="purchaseAmount" label="入库总金额" width="150" align="right">
|
|
|
- <template slot-scope="scope">
|
|
|
- {{ formatNumber(scope.row.purchaseAmount) }}
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="amountRatio" label="入库资金占比(%)" width="150" align="right">
|
|
|
- <template slot-scope="scope">
|
|
|
- {{ scope.row.amountRatio }}%
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="turnoverRate" label="库存周转率" align="right">
|
|
|
- <template slot-scope="scope">
|
|
|
- <el-tag :type="getTurnoverRateType(scope.row.turnoverRate)">
|
|
|
- {{ scope.row.turnoverRate }}
|
|
|
- </el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
+ <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>
|
|
|
@@ -229,7 +210,7 @@
|
|
|
type="info"
|
|
|
show-icon
|
|
|
:closable="false"
|
|
|
- title="至少上传入库数据与销售数据,组装/产品资料/半成品映射可选。"
|
|
|
+ title="可上传Excel进行分析;若不上传文件则默认从数据库读取。入库/销售为常用必传项,其余可选。"
|
|
|
class="upload-alert"
|
|
|
/>
|
|
|
<el-form label-width="110px" class="upload-form">
|
|
|
@@ -276,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>
|
|
|
|
|
|
@@ -298,16 +356,53 @@ 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,
|
|
|
spuLoading: false,
|
|
|
+ skuFilters: {
|
|
|
+ attribute: '',
|
|
|
+ category: '',
|
|
|
+ spu: ''
|
|
|
+ },
|
|
|
+ skuSort: {
|
|
|
+ prop: '',
|
|
|
+ order: ''
|
|
|
+ },
|
|
|
+ spuSort: {
|
|
|
+ prop: '',
|
|
|
+ order: ''
|
|
|
+ },
|
|
|
+ skuCurrentPage: 1,
|
|
|
+ skuPageSize: 10,
|
|
|
+ spuCurrentPage: 1,
|
|
|
+ spuPageSize: 10,
|
|
|
chartInstance: null,
|
|
|
+ valuePieChart: null,
|
|
|
+ turnoverTopChart: null,
|
|
|
uploadDialogVisible: false,
|
|
|
uploadLoading: false,
|
|
|
uploadFiles: {
|
|
|
@@ -328,7 +423,8 @@ export default {
|
|
|
uploadTaskId: '',
|
|
|
uploadTaskStatus: '',
|
|
|
uploadTaskStatusText: '',
|
|
|
- uploadTaskPolling: false
|
|
|
+ uploadTaskPolling: false,
|
|
|
+ riskDialogVisible: false
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
@@ -344,6 +440,60 @@ export default {
|
|
|
if (this.uploadTaskStatus === 'running') return '处理中'
|
|
|
if (this.uploadTaskStatus === 'pending') return '排队中'
|
|
|
return '任务状态'
|
|
|
+ },
|
|
|
+ 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
|
|
|
+ },
|
|
|
+ pagedSpuData() {
|
|
|
+ const start = (this.spuCurrentPage - 1) * this.spuPageSize
|
|
|
+ return this.sortedSpuData.slice(start, start + this.spuPageSize)
|
|
|
+ },
|
|
|
+ spuTotal() {
|
|
|
+ return this.sortedSpuData.length
|
|
|
+ },
|
|
|
+ sortedSpuData() {
|
|
|
+ const { prop, order } = this.spuSort
|
|
|
+ if (!prop || !order) {
|
|
|
+ return this.spuTableData
|
|
|
+ }
|
|
|
+ return this.sortTableData(this.spuTableData, prop, order)
|
|
|
}
|
|
|
},
|
|
|
mounted() {
|
|
|
@@ -353,17 +503,49 @@ export default {
|
|
|
})
|
|
|
window.addEventListener('resize', this.resizeChart)
|
|
|
},
|
|
|
+ watch: {
|
|
|
+ 'skuFilters.attribute'() {
|
|
|
+ this.skuCurrentPage = 1
|
|
|
+ },
|
|
|
+ 'skuFilters.category'() {
|
|
|
+ this.skuCurrentPage = 1
|
|
|
+ },
|
|
|
+ 'skuFilters.spu'() {
|
|
|
+ this.skuCurrentPage = 1
|
|
|
+ }
|
|
|
+ },
|
|
|
beforeDestroy() {
|
|
|
window.removeEventListener('resize', this.resizeChart)
|
|
|
if (this.chartInstance) {
|
|
|
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
|
|
|
@@ -371,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
|
|
|
@@ -399,21 +592,93 @@ export default {
|
|
|
trigger: 'axis',
|
|
|
axisPointer: { type: 'shadow' }
|
|
|
},
|
|
|
- legend: { data: ['入库', '销售', '当前库存'] },
|
|
|
+ legend: { data: ['成品入库', '成品销售', '成品库存'] },
|
|
|
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
|
|
xAxis: { type: 'category', data: months },
|
|
|
yAxis: { type: 'value', name: '数量 (件)' },
|
|
|
series: [
|
|
|
- { name: '入库', type: 'bar', data: purchase, itemStyle: { color: '#5470c6' } },
|
|
|
- { name: '销售', type: 'bar', data: sales, itemStyle: { color: '#91cc75' } },
|
|
|
- { name: '当前库存', type: 'line', data: inventory, itemStyle: { color: '#fac858' }, lineStyle: { width: 3 }, smooth: true }
|
|
|
+ { name: '成品入库', type: 'bar', data: purchase, itemStyle: { color: '#5470c6' } },
|
|
|
+ { name: '成品销售', type: 'bar', data: sales, itemStyle: { color: '#91cc75' } },
|
|
|
+ { name: '成品库存', type: 'line', data: inventory, itemStyle: { color: '#fac858' }, lineStyle: { width: 3 }, smooth: true }
|
|
|
]
|
|
|
}
|
|
|
this.chartInstance.setOption(option, true)
|
|
|
},
|
|
|
+ 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) {
|
|
|
@@ -423,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) => ({
|
|
|
@@ -444,12 +709,71 @@ 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 {
|
|
|
const res = await this.inventoryRequest({ url: '/api/inventory/sku-summary', method: 'get' })
|
|
|
const data = this.normalizeResponse(res)
|
|
|
- if (Array.isArray(data)) this.skuTableData = data
|
|
|
+ if (Array.isArray(data)) {
|
|
|
+ this.skuTableData = data
|
|
|
+ this.skuCurrentPage = 1
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
console.error('获取SKU汇总数据失败:', error)
|
|
|
} finally {
|
|
|
@@ -461,7 +785,10 @@ export default {
|
|
|
try {
|
|
|
const res = await this.inventoryRequest({ url: '/api/inventory/spu-summary', method: 'get' })
|
|
|
const data = this.normalizeResponse(res)
|
|
|
- if (Array.isArray(data)) this.spuTableData = data
|
|
|
+ if (Array.isArray(data)) {
|
|
|
+ this.spuTableData = data
|
|
|
+ this.spuCurrentPage = 1
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
console.error('获取SPU汇总数据失败:', error)
|
|
|
} finally {
|
|
|
@@ -469,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
|
|
|
@@ -524,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)
|
|
|
@@ -555,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)
|
|
|
@@ -607,15 +939,97 @@ export default {
|
|
|
sleep(ms) {
|
|
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
|
},
|
|
|
+ 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
|
|
|
+ },
|
|
|
+ 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
|
|
|
+ },
|
|
|
+ handleSpuSortChange({ prop, order }) {
|
|
|
+ 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 = ''
|
|
|
+ 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'
|
|
|
},
|
|
|
+ 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)}%`
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -626,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;
|
|
|
@@ -689,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;
|
|
|
@@ -733,6 +1220,20 @@ export default {
|
|
|
align-items: center;
|
|
|
}
|
|
|
|
|
|
+.sku-filters {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.table-pagination {
|
|
|
+ margin-top: 16px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
.chart {
|
|
|
height: 300px;
|
|
|
}
|