665 lines
17 KiB
Vue
665 lines
17 KiB
Vue
<template>
|
||
<view class="inventory-freshness-analysis">
|
||
<view class="report-header">
|
||
<text class="report-title">库存新鲜度分析</text>
|
||
<view class="report-period">
|
||
<text class="period-label">分析周期:</text>
|
||
<text class="period-value">{{ analysisPeriod }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 关键指标卡片 -->
|
||
<view class="metrics-row">
|
||
<view class="metric-card">
|
||
<view class="metric-icon fresh"></view>
|
||
<text class="metric-title">新鲜库存占比</text>
|
||
<view class="metric-value-container">
|
||
<text class="metric-value">{{ freshPercentage }}%</text>
|
||
<text class="metric-unit">良好</text>
|
||
</view>
|
||
</view>
|
||
<view class="metric-card">
|
||
<view class="metric-icon warning"></view>
|
||
<text class="metric-title">临期商品数量</text>
|
||
<view class="metric-value-container">
|
||
<text class="metric-value">{{ formatNumber(nearExpiryCount) }}</text>
|
||
<text class="metric-unit">件</text>
|
||
</view>
|
||
</view>
|
||
<view class="metric-card">
|
||
<view class="metric-icon expired"></view>
|
||
<text class="metric-title">过期商品数量</text>
|
||
<view class="metric-value-container">
|
||
<text class="metric-value">{{ formatNumber(expiredCount) }}</text>
|
||
<text class="metric-unit">件</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 库存新鲜度分布饼图 -->
|
||
<view class="chart-card">
|
||
<view class="chart-header">
|
||
<text class="chart-title">库存新鲜度分布</text>
|
||
<text class="chart-subtitle">按有效期状态分类统计</text>
|
||
</view>
|
||
<view class="chart-content">
|
||
<view class="pie-chart-container">
|
||
<QiunDataCharts
|
||
type="pie"
|
||
:opts="pieChartOpts"
|
||
:chartData="pieChartData"
|
||
:canvas2d="true"
|
||
:inScrollView="true"
|
||
canvasId="freshnessPieChart"
|
||
/>
|
||
<view class="pie-legend">
|
||
<view class="legend-item" v-for="(item, index) in freshnessData" :key="index">
|
||
<view class="legend-color" :style="{ backgroundColor: item.color }"></view>
|
||
<text class="legend-name">{{ item.name }}</text>
|
||
<text class="legend-value">{{ item.percentage }}%</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 库存周期分析折线图 -->
|
||
<view class="chart-card">
|
||
<view class="chart-header">
|
||
<text class="chart-title">库存周期趋势分析</text>
|
||
<text class="chart-subtitle">最近6个月库存周期变化趋势</text>
|
||
</view>
|
||
<view class="chart-content">
|
||
<view class="line-chart-container">
|
||
<QiunDataCharts
|
||
type="line"
|
||
:opts="lineChartOpts"
|
||
:chartData="lineChartData"
|
||
:canvas2d="true"
|
||
:inScrollView="true"
|
||
canvasId="inventoryCycleChart"
|
||
/>
|
||
<view class="chart-legend">
|
||
<view class="legend-item">
|
||
<view class="legend-dot cycle"></view>
|
||
<text class="legend-text">平均库存周期</text>
|
||
</view>
|
||
<view class="legend-item">
|
||
<view class="legend-dot freshness"></view>
|
||
<text class="legend-text">新鲜度指数</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 有效期分布柱状图 -->
|
||
<view class="chart-card">
|
||
<view class="chart-header">
|
||
<text class="chart-title">商品有效期分布</text>
|
||
<text class="chart-subtitle">不同有效期区间的商品数量分布</text>
|
||
</view>
|
||
<view class="chart-content">
|
||
<view class="bar-chart-container">
|
||
<QiunDataCharts
|
||
type="column"
|
||
:opts="barChartOpts"
|
||
:chartData="barChartData"
|
||
:canvas2d="true"
|
||
:inScrollView="true"
|
||
canvasId="expiryDistributionChart"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 临期商品预警列表 -->
|
||
<view class="chart-card">
|
||
<view class="chart-header">
|
||
<text class="chart-title">临期商品预警</text>
|
||
<text class="chart-subtitle">即将到期商品清单</text>
|
||
</view>
|
||
<view class="warning-list">
|
||
<view class="warning-item" v-for="(item, index) in nearExpiryList" :key="index" :class="[getWarningLevel(item.daysLeft)]">
|
||
<view class="warning-icon">{{ getWarningIcon(item.daysLeft) }}</view>
|
||
<view class="warning-content">
|
||
<text class="product-name">{{ item.name }}</text>
|
||
<text class="product-code">{{ item.code }}</text>
|
||
</view>
|
||
<view class="warning-info">
|
||
<text class="expiry-date">到期:{{ item.expiryDate }}</text>
|
||
<text class="days-left">{{ item.daysLeft }}天</text>
|
||
</view>
|
||
<view class="warning-action">
|
||
<text class="action-btn">处理</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import QiunDataCharts from './qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue'
|
||
|
||
export default {
|
||
components: {
|
||
QiunDataCharts
|
||
},
|
||
|
||
data() {
|
||
return {
|
||
analysisPeriod: '2024年10月',
|
||
freshPercentage: 87.5,
|
||
nearExpiryCount: 1234,
|
||
expiredCount: 56,
|
||
|
||
// 新鲜度数据
|
||
freshnessData: [
|
||
{ name: '新鲜(>30天)', value: 424856, percentage: 87.5, color: '#52C41A' },
|
||
{ name: '临期(7-30天)', value: 42345, percentage: 8.7, color: '#FAAD14' },
|
||
{ name: '紧急临期(<7天)', value: 12345, percentage: 2.5, color: '#FF7A45' },
|
||
{ name: '已过期', value: 56, percentage: 0.1, color: '#FF4D4F' },
|
||
{ name: '无有效期', value: 5670, percentage: 1.2, color: '#8C8C8C' }
|
||
],
|
||
|
||
// 库存周期趋势数据
|
||
cycleTrendData: {
|
||
categories: ['5月', '6月', '7月', '8月', '9月', '10月'],
|
||
avgCycleData: [45, 42, 48, 51, 46, 43],
|
||
freshnessIndexData: [92, 89, 85, 88, 91, 87.5]
|
||
},
|
||
|
||
// 有效期分布数据
|
||
expiryDistributionData: [
|
||
{ range: '0-7天', count: 12345, percentage: 2.5 },
|
||
{ range: '8-15天', count: 23456, percentage: 4.8 },
|
||
{ range: '16-30天', count: 45678, percentage: 9.4 },
|
||
{ range: '31-60天', count: 78901, percentage: 16.2 },
|
||
{ range: '61-90天', count: 123456, percentage: 25.4 },
|
||
{ range: '90天以上', count: 201836, percentage: 41.7 }
|
||
],
|
||
|
||
// 临期商品列表
|
||
nearExpiryList: [
|
||
{ name: '云南白药牙膏', code: 'YN001', expiryDate: '2024-10-28', daysLeft: 3, quantity: 234 },
|
||
{ name: '普洱茶饼', code: 'PU002', expiryDate: '2024-10-30', daysLeft: 5, quantity: 156 },
|
||
{ name: '鲜花饼', code: 'FH003', expiryDate: '2024-11-02', daysLeft: 8, quantity: 89 },
|
||
{ name: '三七粉', code: 'SQ005', expiryDate: '2024-11-05', daysLeft: 11, quantity: 67 },
|
||
{ name: '手工皂', code: 'GS008', expiryDate: '2024-11-08', daysLeft: 14, quantity: 45 },
|
||
{ name: '鲜花精油', code: 'XJ009', expiryDate: '2024-11-12', daysLeft: 18, quantity: 34 },
|
||
{ name: '云南咖啡', code: 'KF004', expiryDate: '2024-11-15', daysLeft: 21, quantity: 78 },
|
||
{ name: '银饰保养液', code: 'YS010', expiryDate: '2024-11-20', daysLeft: 26, quantity: 23 }
|
||
]
|
||
}
|
||
},
|
||
|
||
computed: {
|
||
// 饼图数据
|
||
pieChartData() {
|
||
return {
|
||
series: [{
|
||
data: this.freshnessData.map(item => ({
|
||
name: item.name,
|
||
value: item.value
|
||
}))
|
||
}]
|
||
}
|
||
},
|
||
|
||
// 饼图配置
|
||
pieChartOpts() {
|
||
return {
|
||
color: this.freshnessData.map(item => item.color),
|
||
padding: [5, 5, 5, 5],
|
||
dataLabel: true,
|
||
legend: {
|
||
show: false
|
||
},
|
||
extra: {
|
||
pie: {
|
||
activeOpacity: 0.5,
|
||
activeRadius: 10,
|
||
offsetAngle: 0,
|
||
labelWidth: 15,
|
||
border: false,
|
||
borderWidth: 3,
|
||
borderColor: '#FFFFFF'
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
// 折线图数据
|
||
lineChartData() {
|
||
return {
|
||
categories: this.cycleTrendData.categories,
|
||
series: [
|
||
{
|
||
name: '平均库存周期',
|
||
data: this.cycleTrendData.avgCycleData
|
||
},
|
||
{
|
||
name: '新鲜度指数',
|
||
data: this.cycleTrendData.freshnessIndexData
|
||
}
|
||
]
|
||
}
|
||
},
|
||
|
||
// 折线图配置
|
||
lineChartOpts() {
|
||
return {
|
||
color: ['#576EFF', '#52C41A'],
|
||
padding: [15, 15, 15, 15],
|
||
dataLabel: false,
|
||
legend: {
|
||
show: false
|
||
},
|
||
xAxis: {
|
||
disableGrid: true
|
||
},
|
||
yAxis: {
|
||
gridType: 'dash',
|
||
dashLength: 2,
|
||
data: [
|
||
{
|
||
min: 0,
|
||
title: '天数'
|
||
},
|
||
{
|
||
min: 0,
|
||
position: 'right',
|
||
title: '指数'
|
||
}
|
||
]
|
||
},
|
||
extra: {
|
||
line: {
|
||
type: 'curve',
|
||
width: 2,
|
||
activeType: 'hollow'
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
// 柱状图数据
|
||
barChartData() {
|
||
return {
|
||
categories: this.expiryDistributionData.map(item => item.range),
|
||
series: [{
|
||
name: '商品数量',
|
||
data: this.expiryDistributionData.map(item => item.count)
|
||
}]
|
||
}
|
||
},
|
||
|
||
// 柱状图配置
|
||
barChartOpts() {
|
||
return {
|
||
color: ['#576EFF', '#FF7A45', '#FAAD14', '#52C41A', '#13C2C2', '#B37FEB'],
|
||
padding: [15, 15, 15, 15],
|
||
dataLabel: false,
|
||
enableScroll: false,
|
||
xAxis: {
|
||
disableGrid: false,
|
||
gridType: 'dash',
|
||
rotateLabel: false,
|
||
itemCount: 6
|
||
},
|
||
yAxis: {
|
||
gridType: 'dash',
|
||
dashLength: 2,
|
||
data: [{
|
||
min: 0,
|
||
title: '商品数量'
|
||
}]
|
||
},
|
||
extra: {
|
||
column: {
|
||
type: 'group',
|
||
width: 35,
|
||
activeBgColor: '#000000',
|
||
activeBgOpacity: 0.08,
|
||
linearType: 'custom',
|
||
barBorderCircle: true,
|
||
seriesGap: 2
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
methods: {
|
||
formatNumber(num) {
|
||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
||
},
|
||
|
||
getWarningLevel(daysLeft) {
|
||
if (daysLeft <= 3) return 'urgent'
|
||
if (daysLeft <= 7) return 'high'
|
||
if (daysLeft <= 15) return 'medium'
|
||
return 'low'
|
||
},
|
||
|
||
getWarningIcon(daysLeft) {
|
||
if (daysLeft <= 3) return '🚨'
|
||
if (daysLeft <= 7) return '⚠️'
|
||
if (daysLeft <= 15) return '📅'
|
||
return 'ℹ️'
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
@primary-color: #667eea;
|
||
@secondary-color: #764ba2;
|
||
@success-color: #52c41a;
|
||
@warning-color: #faad14;
|
||
@error-color: #ff4d4f;
|
||
@text-primary: #333;
|
||
@text-secondary: #666;
|
||
@text-light: #999;
|
||
@bg-white: #ffffff;
|
||
@border-radius: 16rpx;
|
||
@shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||
|
||
.inventory-freshness-analysis {
|
||
.report-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 32rpx;
|
||
padding: 0 8rpx;
|
||
|
||
.report-title {
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
color: @text-primary;
|
||
}
|
||
|
||
.report-period {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.period-label {
|
||
font-size: 24rpx;
|
||
color: @text-secondary;
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.period-value {
|
||
font-size: 24rpx;
|
||
color: @primary-color;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
|
||
.metrics-row {
|
||
display: flex;
|
||
gap: 24rpx;
|
||
margin-bottom: 32rpx;
|
||
|
||
.metric-card {
|
||
flex: 1;
|
||
background: @bg-white;
|
||
border-radius: @border-radius;
|
||
padding: 32rpx 24rpx;
|
||
box-shadow: @shadow;
|
||
text-align: center;
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 4rpx;
|
||
background: linear-gradient(90deg, @primary-color, @secondary-color);
|
||
}
|
||
|
||
.metric-icon {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
margin: 0 auto 16rpx;
|
||
border-radius: 50%;
|
||
position: relative;
|
||
|
||
&.fresh {
|
||
background: linear-gradient(135deg, #52c41a, #73d13d);
|
||
&::after { content: '🌿'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24rpx; }
|
||
}
|
||
|
||
&.warning {
|
||
background: linear-gradient(135deg, #faad14, #ffc53d);
|
||
&::after { content: '⚠️'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24rpx; }
|
||
}
|
||
|
||
&.expired {
|
||
background: linear-gradient(135deg, #ff4d4f, #ff7875);
|
||
&::after { content: '🚫'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24rpx; }
|
||
}
|
||
}
|
||
|
||
.metric-title {
|
||
font-size: 24rpx;
|
||
color: @text-secondary;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.metric-value-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4rpx;
|
||
}
|
||
|
||
.metric-value {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: @text-primary;
|
||
font-family: 'DINAlternate-Bold', sans-serif;
|
||
line-height: 1;
|
||
}
|
||
|
||
.metric-unit {
|
||
font-size: 20rpx;
|
||
color: @text-light;
|
||
line-height: 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
.chart-card {
|
||
background: @bg-white;
|
||
border-radius: @border-radius;
|
||
padding: 24rpx;
|
||
box-shadow: @shadow;
|
||
margin-bottom: 24rpx;
|
||
|
||
.chart-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 20rpx;
|
||
|
||
.chart-title {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: @text-primary;
|
||
}
|
||
|
||
.chart-subtitle {
|
||
font-size: 22rpx;
|
||
color: @text-light;
|
||
margin-top: 4rpx;
|
||
}
|
||
}
|
||
|
||
.chart-content {
|
||
.pie-chart-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
|
||
.pie-legend {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
gap: 16rpx;
|
||
margin-top: 16rpx;
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
|
||
.legend-color {
|
||
width: 12rpx;
|
||
height: 12rpx;
|
||
border-radius: 2rpx;
|
||
}
|
||
|
||
.legend-name {
|
||
font-size: 22rpx;
|
||
color: @text-secondary;
|
||
}
|
||
|
||
.legend-value {
|
||
font-size: 22rpx;
|
||
color: @text-primary;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.line-chart-container {
|
||
.chart-legend {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 32rpx;
|
||
margin-top: 16rpx;
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
|
||
.legend-dot {
|
||
width: 12rpx;
|
||
height: 12rpx;
|
||
border-radius: 50%;
|
||
|
||
&.cycle {
|
||
background: #576EFF;
|
||
}
|
||
|
||
&.freshness {
|
||
background: #52C41A;
|
||
}
|
||
}
|
||
|
||
.legend-text {
|
||
font-size: 22rpx;
|
||
color: @text-secondary;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.bar-chart-container {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
.warning-list {
|
||
.warning-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 16rpx;
|
||
margin-bottom: 12rpx;
|
||
border-radius: 12rpx;
|
||
border: 1rpx solid #f0f0f0;
|
||
|
||
&.urgent {
|
||
background: rgba(255, 77, 79, 0.05);
|
||
border-color: rgba(255, 77, 79, 0.2);
|
||
}
|
||
|
||
&.high {
|
||
background: rgba(255, 122, 69, 0.05);
|
||
border-color: rgba(255, 122, 69, 0.2);
|
||
}
|
||
|
||
&.medium {
|
||
background: rgba(250, 173, 20, 0.05);
|
||
border-color: rgba(250, 173, 20, 0.2);
|
||
}
|
||
|
||
&.low {
|
||
background: rgba(82, 196, 26, 0.05);
|
||
border-color: rgba(82, 196, 26, 0.2);
|
||
}
|
||
|
||
.warning-icon {
|
||
font-size: 32rpx;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
.warning-content {
|
||
flex: 1;
|
||
|
||
.product-name {
|
||
font-size: 28rpx;
|
||
color: @text-primary;
|
||
font-weight: 500;
|
||
display: block;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.product-code {
|
||
font-size: 22rpx;
|
||
color: @text-light;
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
.warning-info {
|
||
text-align: right;
|
||
margin-right: 16rpx;
|
||
|
||
.expiry-date {
|
||
font-size: 22rpx;
|
||
color: @text-secondary;
|
||
display: block;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.days-left {
|
||
font-size: 24rpx;
|
||
color: @text-primary;
|
||
font-weight: 600;
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
.warning-action {
|
||
.action-btn {
|
||
font-size: 22rpx;
|
||
color: @primary-color;
|
||
padding: 8rpx 16rpx;
|
||
border: 1rpx solid @primary-color;
|
||
border-radius: 8rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style> |