|
|
@@ -8,10 +8,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 +22,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 +36,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>
|
|
|
@@ -95,7 +94,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="暂无数据" />
|
|
|
@@ -114,8 +113,37 @@
|
|
|
</el-button>
|
|
|
</div>
|
|
|
</template>
|
|
|
- <el-table :data="skuTableData" stripe style="width: 100%" v-loading="loading">
|
|
|
- <el-table-column prop="sku" label="SKU" width="200" fixed />
|
|
|
+ <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 || '-' }}
|
|
|
@@ -126,32 +154,32 @@
|
|
|
{{ scope.row.spuName || '-' }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column prop="purchaseQty" label="入库数量" width="120" align="right">
|
|
|
+ <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">
|
|
|
+ <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">
|
|
|
+ <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">
|
|
|
+ <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">
|
|
|
+ <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">
|
|
|
+ <el-table-column prop="turnoverRate" label="库存周转率" align="right" sortable="custom">
|
|
|
<template slot-scope="scope">
|
|
|
<el-tag :type="getTurnoverRateType(scope.row.turnoverRate)">
|
|
|
{{ scope.row.turnoverRate }}
|
|
|
@@ -159,6 +187,16 @@
|
|
|
</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"
|
|
|
+ />
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
@@ -174,40 +212,41 @@
|
|
|
</el-button>
|
|
|
</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">
|
|
|
+ <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">
|
|
|
+ <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">
|
|
|
+ <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">
|
|
|
+ <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">
|
|
|
+ <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">
|
|
|
+ <el-table-column prop="turnoverRate" label="库存周转率" align="right" sortable="custom">
|
|
|
<template slot-scope="scope">
|
|
|
<el-tag :type="getTurnoverRateType(scope.row.turnoverRate)">
|
|
|
{{ scope.row.turnoverRate }}
|
|
|
@@ -215,6 +254,16 @@
|
|
|
</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"
|
|
|
+ />
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
@@ -307,6 +356,23 @@ export default {
|
|
|
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,
|
|
|
uploadDialogVisible: false,
|
|
|
uploadLoading: false,
|
|
|
@@ -344,6 +410,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,6 +473,17 @@ 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) {
|
|
|
@@ -399,14 +530,14 @@ 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)
|
|
|
@@ -449,7 +580,10 @@ export default {
|
|
|
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 +595,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 {
|
|
|
@@ -607,6 +744,52 @@ 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
|
|
|
+ },
|
|
|
+ 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'
|
|
|
@@ -733,6 +916,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;
|
|
|
}
|