Signed-off-by: chy <chy@163.com>

This commit is contained in:
chy
2026-02-02 23:17:44 +08:00
parent e10f2f058c
commit a49878384e
774 changed files with 249821 additions and 0 deletions

631
src/views/Home/Index20.vue Normal file
View File

@@ -0,0 +1,631 @@
<template>
<ContentWrap>
<el-table
:data="processedTableData" :span-method="objectSpanMethod" border stripe
height="calc(100vh - 300px)" style="width: 100%" v-loading="loading" :summary-method="getSummaries"
show-summary :summary-text="summaryText">
<!-- 动态生成表头 -->
<template v-for="dim in selectedDimensions" :key="dim">
<el-table-column
:prop="dim" :label="getDimensionName(dim)" :min-width="getColumnWidth(dim)"
show-overflow-tooltip>
<template #default="{ row }">
<span>{{ row[dim] || '-' }}</span>
</template>
</el-table-column>
</template>
<!-- 统计指标列 -->
<el-table-column prop="salesAmount" label="销售额(万)" width="120" sortable>
<template #default="{ row }">
{{ formatNumber(row.salesAmount) }}
</template>
</el-table-column>
<el-table-column prop="salesQuantity" label="销售数量" width="120" sortable>
<template #default="{ row }">
{{ formatNumber(row.salesQuantity) }}
</template>
</el-table-column>
<el-table-column prop="growthRate" label="同比增长" width="120" sortable>
<template #default="{ row }">
<span :class="getGrowthClass(row.growthRate)">
{{ formatPercent(row.growthRate) }}
</span>
</template>
</el-table-column>
</el-table>
</ContentWrap>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Check } from '@element-plus/icons-vue'
import * as echarts from 'echarts'
// 维度定义 - 7个维度
const dimensions = ref([
{ id: 'salesType', name: '销售类型' },
{ id: 'customer', name: '客户' },
{ id: 'dosageForm', name: '剂型' },
{ id: 'product', name: '产品' },
{ id: 'administrativeArea', name: '行政区域' },
{ id: 'salesman', name: '业务员' },
{ id: 'specification', name: '品规' }
])
// 状态管理 - 默认选中所有维度
const selectedDimensions = ref(dimensions.value.map(d => d.id))
const primaryDimension = ref('product') // 默认主维度为产品
const tableData = ref([])
const loading = ref(false)
const lastUpdate = ref('')
const showChart = ref(true)
const activeChartTab = ref('bar')
// 合计行相关状态
const showSummaryRow = ref(true)
const totalStats = ref(null)
const summaryText = computed(() => showSummaryRow.value ? '合计' : '')
// 计算属性
const processedTableData = computed(() => {
return tableData.value.map(item => ({
...item,
formattedAmount: formatNumber(item.salesAmount),
formattedQuantity: formatNumber(item.salesQuantity),
formattedGrowth: formatPercent(item.growthRate)
}))
})
// 方法定义
const toggleDimension = (dimensionId) => {
const index = selectedDimensions.value.indexOf(dimensionId)
if (index > -1) {
// 如果当前维度是主维度,且只有一个维度被选中,不能移除
if (selectedDimensions.value.length === 1) {
ElMessage.warning('至少需要保留一个维度')
return
}
// 如果要移除的是当前主维度,则自动切换到下一个维度
if (dimensionId === primaryDimension.value) {
// 找下一个可用的维度作为主维度
const nextDim = selectedDimensions.value.find(d => d !== dimensionId)
if (nextDim) {
primaryDimension.value = nextDim
}
}
selectedDimensions.value.splice(index, 1)
} else {
selectedDimensions.value.push(dimensionId)
}
}
const getDimensionName = (dimensionId) => {
const dim = dimensions.value.find(d => d.id === dimensionId)
return dim ? dim.name : dimensionId
}
// 单元格合并逻辑
const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
if (!primaryDimension.value) return { rowspan: 1, colspan: 1 }
const prop = column.property
if (prop !== primaryDimension.value) return { rowspan: 1, colspan: 1 }
// 计算相同值的行数
let spanCount = 1
for (let i = rowIndex + 1; i < tableData.value.length; i++) {
if (tableData.value[i][prop] === row[prop]) {
spanCount++
} else {
break
}
}
// 如果是第一行,返回合并
let isFirstRow = true
for (let i = rowIndex - 1; i >= 0; i--) {
if (tableData.value[i][prop] === row[prop]) {
isFirstRow = false
break
}
}
if (isFirstRow && spanCount > 1) {
return { rowspan: spanCount, colspan: 1 }
} else {
return { rowspan: 0, colspan: 0 }
}
}
// 自定义合计方法 - 包含同比增长的加权平均计算
const getSummaries = (param) => {
const { columns, data } = param
const sums = []
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = showSummaryRow.value ? '合计' : ''
return
}
// 如果不是数值列,不显示合计
if (!['salesAmount', 'salesQuantity', 'growthRate'].includes(column.property)) {
sums[index] = ''
return
}
// 计算数值列的总和或加权平均
const values = data.map(item => Number(item[column.property]))
if (!values.every(value => isNaN(value))) {
if (column.property === 'salesAmount' || column.property === 'salesQuantity') {
// 销售额和销售数量直接求和
const sum = values.reduce((prev, curr) => {
const value = Number(curr)
if (!isNaN(value)) {
return prev + curr
} else {
return prev
}
}, 0)
if (column.property === 'salesAmount') {
sums[index] = formatNumber(sum)
} else if (column.property === 'salesQuantity') {
sums[index] = formatNumber(sum)
}
} else if (column.property === 'growthRate') {
// 同比增长率需要计算加权平均值(按销售额加权)
let totalAmount = 0
let weightedGrowthSum = 0
data.forEach(item => {
const amount = Number(item.salesAmount) || 0
const growth = Number(item.growthRate) || 0
if (!isNaN(amount) && !isNaN(growth)) {
totalAmount += amount
weightedGrowthSum += growth * amount
}
})
const avgGrowth = totalAmount > 0 ? weightedGrowthSum / totalAmount : 0
sums[index] = formatPercent(avgGrowth)
}
} else {
sums[index] = '-'
}
})
return sums
}
// 数据获取
const fetchData = async () => {
loading.value = true
try {
// 使用模拟数据
setTimeout(() => {
tableData.value = generateMockData()
// 计算并保存总统计数据
calculateTotalStats()
lastUpdate.value = new Date().toLocaleString()
loading.value = false
// 更新图表
if (showChart.value) {
updateCharts()
}
}, 1000)
} catch (error) {
console.error('获取数据失败:', error)
loading.value = false
ElMessage.error('获取数据失败')
}
}
// 计算总统计数据
const calculateTotalStats = () => {
const totalAmount = tableData.value.reduce((sum, row) => sum + (row.salesAmount || 0), 0)
const totalQuantity = tableData.value.reduce((sum, row) => sum + (row.salesQuantity || 0), 0)
// 计算加权平均增长率(按销售额加权)
const weightedGrowthSum = tableData.value.reduce(
(sum, row) => sum + (row.growthRate || 0) * (row.salesAmount || 0), 0
)
const avgGrowth = totalAmount > 0 ? weightedGrowthSum / totalAmount : 0
totalStats.value = {
totalAmount,
totalQuantity,
avgGrowth
}
}
// 生成模拟数据
const generateMockData = () => {
const data = []
// 定义各维度的取值范围
const dimValues = {
salesType: ['零售', '批发', '直销'],
customer: ['客户1', '客户2', '客户3', '客户4', '客户5'],
dosageForm: ['片剂', '胶囊', '颗粒', '口服液'],
product: ['产品A', '产品B', '产品C', '产品D'],
administrativeArea: ['上海', '北京', '广州', '深圳', '杭州'],
salesman: ['张三', '李四', '王五', '赵六'],
specification: ['10mg*20片', '20mg*30片', '50mg*10片', '100mg*5片']
}
// 为每个产品生成一些数据
const products = dimValues.product
const rowCount = 100 // 生成100行数据
for (let i = 0; i < rowCount; i++) {
const row = {}
// 为每个维度生成随机值
dimensions.value.forEach(dim => {
const values = dimValues[dim.id] || ['默认值']
row[dim.id] = values[Math.floor(Math.random() * values.length)]
})
// 添加统计指标
row.salesAmount = Math.random() * 200 + 50
row.salesQuantity = Math.floor(Math.random() * 5000) + 1000
row.growthRate = (Math.random() * 0.3 - 0.1) // -10% 到 20%
data.push(row)
}
// 按主维度排序
return data.sort((a, b) => {
const aValue = a[primaryDimension.value] || ''
const bValue = b[primaryDimension.value] || ''
return aValue.localeCompare(bValue)
})
}
// 辅助方法
const formatNumber = (num) => {
if (num === undefined || num === null) return '-'
return Number(num).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
}
const formatPercent = (num) => {
if (num === undefined || num === null) return '-'
return (num * 100).toFixed(2) + '%'
}
const getGrowthClass = (rate) => {
if (rate > 0) return 'positive'
if (rate < 0) return 'negative'
return 'neutral'
}
const getColumnWidth = (dimensionId) => {
const widthMap = {
salesType: 100,
customer: 120,
dosageForm: 80,
product: 100,
administrativeArea: 100,
salesman: 100,
specification: 120
}
return widthMap[dimensionId] || 100
}
// 图表相关
let barChartInstance = null
let pieChartInstance = null
const updateCharts = () => {
if (barChartInstance) {
barChartInstance.dispose()
}
if (pieChartInstance) {
pieChartInstance.dispose()
}
// 更新柱状图
const barElement = document.querySelector('.chart-container .el-tab-pane:first-child div')
if (barElement) {
barChartInstance = echarts.init(barElement)
// 按主维度分组数据
const groups = {}
tableData.value.forEach(d => {
const key = d[primaryDimension.value] || '其他'
if (!groups[key]) {
groups[key] = 0
}
groups[key] += d.salesAmount || 0
})
const barOption = {
title: { text: `${getDimensionName(primaryDimension.value)}销售额分布`, left: 'center' },
tooltip: {
trigger: 'axis',
formatter: '{b}: {c}万元'
},
xAxis: {
type: 'category',
data: Object.keys(groups)
},
yAxis: {
type: 'value',
name: '销售额(万)'
},
series: [{
data: Object.values(groups),
type: 'bar',
itemStyle: {
color: '#409eff'
}
}]
}
barChartInstance.setOption(barOption)
}
// 更新饼图
const pieElement = document.querySelector('.chart-container .el-tab-pane:last-child div')
if (pieElement) {
pieChartInstance = echarts.init(pieElement)
// 按第一个维度分组数据
const firstDim = selectedDimensions.value[0]
if (firstDim) {
const groups = {}
tableData.value.forEach(d => {
const key = d[firstDim] || '其他'
if (!groups[key]) {
groups[key] = 0
}
groups[key] += d.salesAmount || 0
})
const pieOption = {
title: { text: `${getDimensionName(firstDim)}占比`, left: 'center' },
tooltip: {
trigger: 'item',
formatter: '{a}<br/>{b}: {c}万元 ({d}%)'
},
series: [{
name: '销售额',
type: 'pie',
radius: '50%',
data: Object.keys(groups).map(key => ({
name: key,
value: groups[key]
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
}
pieChartInstance.setOption(pieOption)
}
}
}
const reset = () => {
selectedDimensions.value = dimensions.value.map(d => d.id)
primaryDimension.value = 'product'
tableData.value = []
showSummaryRow.value = true
totalStats.value = null
}
// 监听维度变化
watch(() => [...selectedDimensions.value], (newVal) => {
// 如果当前主维度不在选中维度中,则设置为第一个选中的维度
if (!newVal.includes(primaryDimension.value) && newVal.length > 0) {
primaryDimension.value = newVal[0]
}
})
// 监听主维度变化
watch(primaryDimension, () => {
if (tableData.value.length > 0) {
fetchData()
}
})
// 初始化
onMounted(() => {
fetchData()
// 监听窗口变化调整图表
window.addEventListener('resize', () => {
if (barChartInstance) barChartInstance.resize()
if (pieChartInstance) pieChartInstance.resize()
})
})
</script>
<style scoped>
.multi-dimension-report {
display: flex;
height: 100vh;
background: #f5f7fa;
flex-direction: column;
}
.dimension-controls {
padding: 20px;
background: white;
border-bottom: 1px solid #e6e6e6;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.dimension-selection {
margin-top: 10px;
}
.dimension-selection h3 {
margin: 0 0 15px;
font-size: 16px;
color: #606266;
}
.dimension-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-bottom: 10px;
}
.dimension-item {
position: relative;
padding: 12px;
font-weight: 500;
color: #409eff;
text-align: center;
cursor: pointer;
background-color: #ecf5ff;
border: 2px solid #409eff;
border-radius: 4px;
transition: all 0.3s;
user-select: none;
}
.dimension-item:hover {
background-color: #d9ecff;
transform: translateY(-2px);
box-shadow: 0 2px 8px rgb(64 158 255 / 20%);
}
.dimension-item.inactive {
font-weight: normal;
color: #606266;
background-color: #f5f7fa;
border-color: #dcdfe6;
}
.dimension-item.inactive:hover {
background-color: #ebeef5;
transform: translateY(-2px);
box-shadow: 0 2px 8px rgb(0 0 0 / 10%);
}
.dimension-item .check-icon {
position: absolute;
top: 5px;
right: 5px;
font-size: 12px;
color: #409eff;
}
.dimension-item.inactive .check-icon {
display: none;
}
.report-container {
flex: 1;
padding: 20px;
overflow: hidden;
}
.report-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.statistics-info {
padding: 8px 15px;
font-size: 14px;
color: #666;
background: #f5f7fa;
border: 1px solid #e6e6e6;
border-radius: 4px;
}
.table-container {
padding: 20px;
background: white;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
}
.positive {
color: #67c23a;
}
.negative {
color: #f56c6c;
}
.neutral {
color: #909399;
}
.chart-container {
padding: 20px;
margin-top: 20px;
background: white;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
}
:deep(.el-table .cell) {
padding: 4px 8px;
}
:deep(.el-table th) {
font-weight: 600;
background-color: #f8f9fa;
}
:deep(.el-table__footer-wrapper .el-table__cell) {
font-weight: bold;
background-color: #f0f9ff;
}
:deep(.el-table__footer-wrapper .positive) {
color: #67c23a;
}
:deep(.el-table__footer-wrapper .negative) {
color: #f56c6c;
}
:deep(.el-table__footer-wrapper .neutral) {
color: #909399;
}
/* 维度选择提示 */
.dimension-hint {
margin-top: 10px;
font-size: 12px;
color: #909399;
text-align: center;
}
</style>