Merge branch '0.8.X' of https://gitee.com/hdka/klp-oa into 0.8.X
This commit is contained in:
@@ -51,3 +51,28 @@ export function delProductSalesScript(scriptId) {
|
|||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// // 记录话术访问频率
|
||||||
|
// export function recordVisit(productId) {
|
||||||
|
// return request({
|
||||||
|
// url: '/klp/productSalesScript/recordVisit/' + productId,
|
||||||
|
// method: 'post'
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 获取热门产品排行
|
||||||
|
// export function getHotProducts(limit = 10) {
|
||||||
|
// return request({
|
||||||
|
// url: '/klp/productSalesScript/hotProducts',
|
||||||
|
// method: 'get',
|
||||||
|
// params: { limit }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 获取产品咨询热度排行
|
||||||
|
export function getProductRanking() {
|
||||||
|
return request({
|
||||||
|
url: '/klp/productSalesScript/dashboard/ranking',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
619
klp-ui/src/views/wms/hotProduct/index.vue
Normal file
619
klp-ui/src/views/wms/hotProduct/index.vue
Normal file
@@ -0,0 +1,619 @@
|
|||||||
|
<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>
|
||||||
|
<el-button type="text" @click="exportData">
|
||||||
|
<i class="el-icon-download"></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 ref="pieChart" class="chart-container"></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 ref="barChart" class="chart-container-large"></div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-button type="primary" @click="refreshData" :loading="loading">
|
||||||
|
<i class="el-icon-refresh"></i>
|
||||||
|
刷新数据
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="exportData">
|
||||||
|
<i class="el-icon-download"></i>
|
||||||
|
导出排行榜
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="openSettings">
|
||||||
|
<i class="el-icon-setting"></i>
|
||||||
|
设置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import { getProductRanking } from '@/api/wms/productSalesScript'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HotProduct',
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
background: #f5f7fa;
|
||||||
|
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>
|
||||||
@@ -6,7 +6,7 @@ import java.util.Arrays;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.validation.constraints.*;
|
import javax.validation.constraints.*;
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import com.klp.common.annotation.RepeatSubmit;
|
import com.klp.common.annotation.RepeatSubmit;
|
||||||
@@ -22,6 +22,9 @@ import com.klp.domain.vo.WmsProductSalesScriptVo;
|
|||||||
import com.klp.domain.bo.WmsProductSalesScriptBo;
|
import com.klp.domain.bo.WmsProductSalesScriptBo;
|
||||||
import com.klp.service.IWmsProductSalesScriptService;
|
import com.klp.service.IWmsProductSalesScriptService;
|
||||||
import com.klp.common.core.page.TableDataInfo;
|
import com.klp.common.core.page.TableDataInfo;
|
||||||
|
import com.klp.common.utils.redis.RedisUtils;
|
||||||
|
import java.time.Duration;
|
||||||
|
import com.klp.domain.vo.ProductRankingVo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 产品销售话术
|
* 产品销售话术
|
||||||
@@ -63,7 +66,14 @@ public class WmsProductSalesScriptController extends BaseController {
|
|||||||
@GetMapping("/{scriptId}")
|
@GetMapping("/{scriptId}")
|
||||||
public R<WmsProductSalesScriptVo> getInfo(@NotNull(message = "主键不能为空")
|
public R<WmsProductSalesScriptVo> getInfo(@NotNull(message = "主键不能为空")
|
||||||
@PathVariable Long scriptId) {
|
@PathVariable Long scriptId) {
|
||||||
return R.ok(iWmsProductSalesScriptService.queryById(scriptId));
|
WmsProductSalesScriptVo vo = iWmsProductSalesScriptService.queryById(scriptId);
|
||||||
|
|
||||||
|
// 记录产品访问次数到Redis
|
||||||
|
if (vo != null && vo.getProductId() != null) {
|
||||||
|
iWmsProductSalesScriptService.recordProductViewCount(vo.getProductId());
|
||||||
|
}
|
||||||
|
|
||||||
|
return R.ok(vo);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,4 +107,13 @@ public class WmsProductSalesScriptController extends BaseController {
|
|||||||
@PathVariable Long[] scriptIds) {
|
@PathVariable Long[] scriptIds) {
|
||||||
return toAjax(iWmsProductSalesScriptService.deleteWithValidByIds(Arrays.asList(scriptIds), true));
|
return toAjax(iWmsProductSalesScriptService.deleteWithValidByIds(Arrays.asList(scriptIds), true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取产品咨询热度排行
|
||||||
|
*/
|
||||||
|
@GetMapping("/dashboard/ranking")
|
||||||
|
public R<List<ProductRankingVo>> getProductRanking() {
|
||||||
|
List<ProductRankingVo> ranking = iWmsProductSalesScriptService.getProductRanking();
|
||||||
|
return R.ok(ranking);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,9 +155,6 @@ public class WmsStockIoController extends BaseController {
|
|||||||
* 扫码枪专用:根据明细ID直接入库,无需审核整单
|
* 扫码枪专用:根据明细ID直接入库,无需审核整单
|
||||||
*/
|
*/
|
||||||
@PostMapping("/scanInStock")
|
@PostMapping("/scanInStock")
|
||||||
/**
|
|
||||||
* 扫码枪专用:根据传入明细参数直接入库,无需审核整单
|
|
||||||
*/
|
|
||||||
public R<Void> scanInStock(@RequestBody WmsStockIoDetailBo bo) {
|
public R<Void> scanInStock(@RequestBody WmsStockIoDetailBo bo) {
|
||||||
try {
|
try {
|
||||||
boolean result = iWmsStockIoService.scanInStockByBo(bo);
|
boolean result = iWmsStockIoService.scanInStockByBo(bo);
|
||||||
|
|||||||
@@ -3,11 +3,37 @@ package com.klp.domain.vo;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据看板概览视图对象
|
||||||
|
*
|
||||||
|
* @author klp
|
||||||
|
* @date 2025-01-27
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class DashboardOverviewVO {
|
public class DashboardOverviewVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单汇总
|
||||||
|
*/
|
||||||
private OrderSummaryVO orderSummary;
|
private OrderSummaryVO orderSummary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销售经理饼图
|
||||||
|
*/
|
||||||
private List<SalesManagerPieVO> salesManagerPie;
|
private List<SalesManagerPieVO> salesManagerPie;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产品销量排行
|
||||||
|
*/
|
||||||
private List<ProductRankVO> productRank;
|
private List<ProductRankVO> productRank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单物料分析
|
||||||
|
*/
|
||||||
private List<OrderMaterialVO> orderMaterial;
|
private List<OrderMaterialVO> orderMaterial;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户分布
|
||||||
|
*/
|
||||||
private List<CustomerRegionVO> customerRegion;
|
private List<CustomerRegionVO> customerRegion;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.klp.domain.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产品咨询热度排行视图对象
|
||||||
|
*
|
||||||
|
* @author klp
|
||||||
|
* @date 2025-07-24
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ProductRankingVo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产品ID
|
||||||
|
*/
|
||||||
|
private Long productId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产品编号
|
||||||
|
*/
|
||||||
|
private String productCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产品名称
|
||||||
|
*/
|
||||||
|
private String productName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问次数
|
||||||
|
*/
|
||||||
|
private Long viewCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排名
|
||||||
|
*/
|
||||||
|
private Integer ranking;
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.klp.service;
|
package com.klp.service;
|
||||||
|
|
||||||
import com.klp.domain.WmsProductSalesScript;
|
|
||||||
import com.klp.domain.vo.WmsProductSalesScriptVo;
|
import com.klp.domain.vo.WmsProductSalesScriptVo;
|
||||||
import com.klp.domain.bo.WmsProductSalesScriptBo;
|
import com.klp.domain.bo.WmsProductSalesScriptBo;
|
||||||
import com.klp.common.core.page.TableDataInfo;
|
import com.klp.common.core.page.TableDataInfo;
|
||||||
import com.klp.common.core.domain.PageQuery;
|
import com.klp.common.core.domain.PageQuery;
|
||||||
|
import com.klp.domain.vo.ProductRankingVo;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -47,4 +47,13 @@ public interface IWmsProductSalesScriptService {
|
|||||||
*/
|
*/
|
||||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录产品访问次数
|
||||||
|
*/
|
||||||
|
void recordProductViewCount(Long productId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取产品咨询热度排行
|
||||||
|
*/
|
||||||
|
List<ProductRankingVo> getProductRanking();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import com.klp.common.utils.StringUtils;
|
import com.klp.common.utils.StringUtils;
|
||||||
|
import com.klp.domain.WmsProduct;
|
||||||
|
import com.klp.mapper.WmsProductMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import com.klp.domain.bo.WmsProductSalesScriptBo;
|
import com.klp.domain.bo.WmsProductSalesScriptBo;
|
||||||
@@ -14,10 +16,16 @@ import com.klp.domain.vo.WmsProductSalesScriptVo;
|
|||||||
import com.klp.domain.WmsProductSalesScript;
|
import com.klp.domain.WmsProductSalesScript;
|
||||||
import com.klp.mapper.WmsProductSalesScriptMapper;
|
import com.klp.mapper.WmsProductSalesScriptMapper;
|
||||||
import com.klp.service.IWmsProductSalesScriptService;
|
import com.klp.service.IWmsProductSalesScriptService;
|
||||||
|
import com.klp.domain.vo.ProductRankingVo;
|
||||||
|
import com.klp.common.utils.redis.RedisUtils;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import org.redisson.api.RScoredSortedSet;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 产品销售话术Service业务层处理
|
* 产品销售话术Service业务层处理
|
||||||
@@ -31,6 +39,9 @@ public class WmsProductSalesScriptServiceImpl implements IWmsProductSalesScriptS
|
|||||||
|
|
||||||
private final WmsProductSalesScriptMapper baseMapper;
|
private final WmsProductSalesScriptMapper baseMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private WmsProductMapper wmsProductMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询产品销售话术
|
* 查询产品销售话术
|
||||||
*/
|
*/
|
||||||
@@ -117,4 +128,59 @@ public class WmsProductSalesScriptServiceImpl implements IWmsProductSalesScriptS
|
|||||||
}
|
}
|
||||||
return baseMapper.deleteBatchIds(ids) > 0;
|
return baseMapper.deleteBatchIds(ids) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void recordProductViewCount(Long productId) {
|
||||||
|
String countKey = "product:view:count:" + productId;
|
||||||
|
String rankingKey = "product:view:ranking";
|
||||||
|
|
||||||
|
// 计数器+1(使用原子操作)
|
||||||
|
RedisUtils.incrAtomicValue(countKey);
|
||||||
|
|
||||||
|
// 设置计数器过期时间为1天
|
||||||
|
RedisUtils.expire(countKey, 24 * 60 * 60);
|
||||||
|
|
||||||
|
// 更新排行榜(使用ZSet操作)
|
||||||
|
RScoredSortedSet<String> zSet = RedisUtils.getClient().getScoredSortedSet(rankingKey);
|
||||||
|
zSet.addScore(productId.toString(), 1);
|
||||||
|
|
||||||
|
// 设置排行榜过期时间为1天
|
||||||
|
RedisUtils.expire(rankingKey, 24 * 60 * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProductRankingVo> getProductRanking() {
|
||||||
|
String rankingKey = "product:view:ranking";
|
||||||
|
|
||||||
|
// 获取排行榜前10名(按访问次数降序)
|
||||||
|
RScoredSortedSet<String> zSet = RedisUtils.getClient().getScoredSortedSet(rankingKey);
|
||||||
|
|
||||||
|
List<ProductRankingVo> rankingList = new ArrayList<>();
|
||||||
|
int rank = 1;
|
||||||
|
|
||||||
|
// 获取前10名的产品ID和分数
|
||||||
|
Collection<String> topProducts = zSet.valueRangeReversed(0, 9);
|
||||||
|
|
||||||
|
for (String productIdStr : topProducts) {
|
||||||
|
Long productId = Long.valueOf(productIdStr);
|
||||||
|
Double score = zSet.getScore(productIdStr);
|
||||||
|
Long viewCount = score != null ? score.longValue() : 0L;
|
||||||
|
|
||||||
|
ProductRankingVo vo = new ProductRankingVo();
|
||||||
|
vo.setProductId(productId);
|
||||||
|
vo.setViewCount(viewCount);
|
||||||
|
vo.setRanking(rank++);
|
||||||
|
|
||||||
|
//补充产品基础信息(这里需要注入ProductMapper)
|
||||||
|
WmsProduct product = wmsProductMapper.selectById(productId);
|
||||||
|
if (product != null) {
|
||||||
|
vo.setProductCode(product.getProductCode());
|
||||||
|
vo.setProductName(product.getProductName());
|
||||||
|
}
|
||||||
|
|
||||||
|
rankingList.add(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rankingList;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user