Files
xgy-oa/klp-ui/src/views/wms/hotProduct/index.vue
2025-08-28 16:44:04 +08:00

616 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="hot-product-page">
<!-- 页面标题 -->
<!-- <div class="page-header">
<h2 class="page-title">
<i class="el-icon-trophy"></i>
热门产品排行
</h2>
<div class="page-subtitle">基于产品咨询访问频率的热门产品分析</div>
</div> -->
<!-- 统计卡片 -->
<el-row :gutter="20" class="statistics-row">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<i class="el-icon-view"></i>
</div>
<div class="stat-info">
<div class="stat-number">{{ totalViews }}</div>
<div class="stat-label">总访问量</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<i class="el-icon-goods"></i>
</div>
<div class="stat-info">
<div class="stat-number">{{ totalProducts }}</div>
<div class="stat-label">热门产品数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
<i class="el-icon-star-on"></i>
</div>
<div class="stat-info">
<div class="stat-number">{{ avgViews }}</div>
<div class="stat-label">平均访问量</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">
<i class="el-icon-trend-charts"></i>
</div>
<div class="stat-info">
<div class="stat-number">{{ maxViews }}</div>
<div class="stat-label">最高访问量</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 主要内容区域 -->
<el-row :gutter="20" class="main-content">
<!-- 左侧排行榜 -->
<el-col :span="16">
<el-card shadow="hover" class="ranking-card">
<div slot="header" class="card-header">
<span class="header-title">
<i class="el-icon-medal"></i>
产品咨询热度排行榜
</span>
<div class="header-actions">
<el-button type="text" @click="refreshData" :loading="loading">
<i class="el-icon-refresh"></i>
刷新
</el-button>
</div>
</div>
<div class="ranking-list">
<div
v-for="(item, index) in productRanking"
:key="item.productId"
class="ranking-item"
:class="{ 'top-three': index < 3 }"
>
<div class="ranking-number" :class="`rank-${index + 1}`">
{{ item.ranking }}
</div>
<div class="product-info">
<div class="product-name">{{ item.productName }}</div>
<div class="product-code">{{ item.productCode }}</div>
</div>
<div class="view-count">
<div class="count-number">{{ item.viewCount }}</div>
<div class="count-label">次访问</div>
</div>
<div class="trend-indicator">
<i class="el-icon-arrow-up" v-if="index < 3"></i>
<i class="el-icon-minus" v-else></i>
</div>
</div>
</div>
</el-card>
</el-col>
<!-- 右侧图表 -->
<el-col :span="8">
<el-card shadow="hover" class="chart-card">
<div slot="header" class="card-header">
<span class="header-title">
<i class="el-icon-pie-chart"></i>
访问量分布
</span>
</div>
<div class="chart-container">
<ChartWrapper>
<div ref="pieChart" style="height: 100%; width: 100%;"></div>
</ChartWrapper>
</div>
</el-card>
</el-col>
</el-row>
<!-- 底部图表 -->
<el-row :gutter="20" class="bottom-charts">
<el-col :span="24">
<el-card shadow="hover" class="chart-card">
<div slot="header" class="card-header">
<span class="header-title">
<i class="el-icon-data-line"></i>
访问量趋势图
</span>
</div>
<div class="chart-container-large">
<ChartWrapper>
<div ref="barChart" style="height: 100%; width: 100%;"></div>
</ChartWrapper>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import * as echarts from 'echarts'
import { getProductRanking } from '@/api/wms/productSalesScript'
import ChartWrapper from '@/components/ChartWrapper/index.vue'
import resizeMinix from '@/views/wms/stock/panels/resize.js'
export default {
mixins: [resizeMinix],
name: 'HotProduct',
components: {
ChartWrapper
},
data() {
return {
loading: false,
productRanking: [],
totalViews: 0,
totalProducts: 0,
avgViews: 0,
maxViews: 0,
charts: {
pie: null,
bar: null
}
}
},
mounted() {
this.loadData()
this.initCharts()
window.addEventListener('resize', this.handleResize)
this.initResizeListener(this.$refs.barChart)
this.initResizeListener(this.$refs.pieChart)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
Object.values(this.charts).forEach(chart => {
if (chart) {
chart.dispose()
}
})
},
methods: {
async loadData() {
this.loading = true
try {
const response = await getProductRanking()
this.productRanking = response.data || []
this.calculateStatistics()
this.renderCharts()
} catch (error) {
console.error('加载数据失败:', error)
this.$message.error('加载数据失败')
} finally {
this.loading = false
}
},
calculateStatistics() {
if (this.productRanking.length === 0) {
this.totalViews = 0
this.totalProducts = 0
this.avgViews = 0
this.maxViews = 0
return
}
this.totalViews = this.productRanking.reduce((sum, item) => sum + (item.viewCount || 0), 0)
this.totalProducts = this.productRanking.length
this.avgViews = Math.round(this.totalViews / this.totalProducts)
this.maxViews = Math.max(...this.productRanking.map(item => item.viewCount || 0))
},
initCharts() {
this.charts.pie = echarts.init(this.$refs.pieChart)
this.charts.bar = echarts.init(this.$refs.barChart)
},
renderCharts() {
this.renderPieChart()
this.renderBarChart()
},
renderPieChart() {
const data = this.productRanking.slice(0, 5) // 只显示前5名
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
top: 'middle'
},
series: [
{
name: '访问量',
type: 'pie',
radius: ['40%', '70%'],
center: ['60%', '50%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: data.map((item, index) => ({
name: item.productName,
value: item.viewCount,
itemStyle: {
color: this.getRankColor(index + 1)
}
}))
}
]
}
this.charts.pie.setOption(option)
},
renderBarChart() {
const data = this.productRanking.slice(0, 10) // 显示前10名
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: data.map(item => item.productName),
axisLabel: {
interval: 0,
rotate: 30,
fontSize: 12
}
},
yAxis: {
type: 'value',
name: '访问次数'
},
series: [
{
name: '访问量',
type: 'bar',
data: data.map((item, index) => ({
value: item.viewCount,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: this.getRankColor(index + 1) },
{ offset: 1, color: this.getRankColor(index + 1, 0.7) }
])
}
})),
barWidth: '60%',
label: {
show: true,
position: 'top',
fontSize: 12
}
}
]
}
this.charts.bar.setOption(option)
},
getRankColor(rank, alpha = 1) {
const colors = {
1: `rgba(255, 215, 0, ${alpha})`, // 金色
2: `rgba(192, 192, 192, ${alpha})`, // 银色
3: `rgba(205, 127, 50, ${alpha})`, // 铜色
4: `rgba(100, 149, 237, ${alpha})`, // 蓝色
5: `rgba(50, 205, 50, ${alpha})` // 绿色
}
return colors[rank] || `rgba(128, 128, 128, ${alpha})`
},
handleResize() {
Object.values(this.charts).forEach(chart => {
if (chart) {
chart.resize()
}
})
},
refreshData() {
this.loadData()
},
exportData() {
// 导出功能
const data = this.productRanking.map(item => ({
'排名': item.ranking,
'产品名称': item.productName,
'产品编号': item.productCode,
'访问次数': item.viewCount
}))
// 这里可以调用导出Excel的方法
this.$message.success('导出功能开发中...')
},
openSettings() {
this.$message.info('设置功能开发中...')
}
}
}
</script>
<style scoped>
.hot-product-page {
padding: 20px;
min-height: 100vh;
}
.page-header {
text-align: center;
margin-bottom: 30px;
}
.page-title {
font-size: 28px;
font-weight: bold;
color: #303133;
margin: 0 0 10px 0;
display: flex;
align-items: center;
justify-content: center;
}
.page-title i {
margin-right: 10px;
color: #409eff;
}
.page-subtitle {
font-size: 14px;
color: #909399;
}
.statistics-row {
margin-bottom: 30px;
}
.stat-card {
border-radius: 12px;
overflow: hidden;
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-content {
display: flex;
align-items: center;
padding: 20px;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
color: white;
font-size: 24px;
}
.stat-info {
flex: 1;
}
.stat-number {
font-size: 28px;
font-weight: bold;
color: #303133;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
color: #909399;
}
.main-content {
margin-bottom: 30px;
}
.ranking-card, .chart-card {
border-radius: 12px;
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0;
}
.header-title {
font-size: 16px;
font-weight: bold;
color: #303133;
display: flex;
align-items: center;
}
.header-title i {
margin-right: 8px;
color: #409eff;
}
.header-actions {
display: flex;
gap: 10px;
}
.ranking-list {
padding: 10px 0;
}
.ranking-item {
display: flex;
align-items: center;
padding: 15px 20px;
margin-bottom: 10px;
background: #fafafa;
border-radius: 8px;
transition: all 0.3s ease;
border-left: 4px solid #e4e7ed;
}
.ranking-item:hover {
background: #f0f9ff;
transform: translateX(5px);
}
.ranking-item.top-three {
background: linear-gradient(135deg, #fff9e6 0%, #fff2cc 100%);
border-left-color: #ffd700;
}
.ranking-number {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 16px;
margin-right: 20px;
color: white;
}
.rank-1 {
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
}
.rank-2 {
background: linear-gradient(135deg, #c0c0c0 0%, #e5e5e5 100%);
}
.rank-3 {
background: linear-gradient(135deg, #cd7f32 0%, #daa520 100%);
}
.ranking-number:not(.rank-1):not(.rank-2):not(.rank-3) {
background: linear-gradient(135deg, #409eff 0%, #67c23a 100%);
}
.product-info {
flex: 1;
margin-right: 20px;
}
.product-name {
font-size: 16px;
font-weight: bold;
color: #303133;
margin-bottom: 5px;
}
.product-code {
font-size: 12px;
color: #909399;
}
.view-count {
text-align: center;
margin-right: 20px;
}
.count-number {
font-size: 20px;
font-weight: bold;
color: #409eff;
}
.count-label {
font-size: 12px;
color: #909399;
}
.trend-indicator {
width: 30px;
text-align: center;
}
.trend-indicator i {
font-size: 16px;
color: #67c23a;
}
.chart-container {
height: 300px;
width: 100%;
}
.chart-container-large {
height: 400px;
width: 100%;
}
.bottom-charts {
margin-bottom: 30px;
}
.action-buttons {
text-align: center;
padding: 20px 0;
}
.action-buttons .el-button {
margin: 0 10px;
padding: 12px 24px;
border-radius: 8px;
}
.action-buttons .el-button i {
margin-right: 5px;
}
</style>