456 lines
12 KiB
Vue
456 lines
12 KiB
Vue
<template>
|
||
<ContentWrap>
|
||
<!-- 搜索工具栏卡片 -->
|
||
<el-card class="member-stat-card search-card mb-4" shadow="hover">
|
||
<el-form :inline="true" @submit.prevent>
|
||
<el-form-item label="时间范围">
|
||
<el-date-picker
|
||
v-model="dateRange"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
@change="onDateChange"
|
||
style="width: 300px;"
|
||
clearable
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="图表类型">
|
||
<el-button :type="chartType === 'line' ? 'primary' : 'default'" @click="chartType = 'line'">
|
||
折线图
|
||
</el-button>
|
||
<el-button :type="chartType === 'bar' ? 'primary' : 'default'" @click="chartType = 'bar'">
|
||
柱状图
|
||
</el-button>
|
||
<el-button :type="chartType === 'stack' ? 'primary' : 'default'" @click="chartType = 'stack'">
|
||
堆叠图
|
||
</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
<!-- 统计图表卡片,下方主内容区域 -->
|
||
<el-card class="member-stat-card stat-card" shadow="hover">
|
||
<div class="chart-container">
|
||
<div id="main" class="line-chart"></div>
|
||
<div id="pieChart" class="pie-chart"></div>
|
||
</div>
|
||
</el-card>
|
||
</ContentWrap>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, watch } from 'vue'
|
||
import * as echarts from 'echarts'
|
||
import ContentWrap from '@/components/ContentWrap/src/ContentWrap.vue'
|
||
import { RecordApi } from '@/api/record'
|
||
import dayjs from 'dayjs'
|
||
import { getUserProfile } from '@/api/system/user/profile'
|
||
|
||
// 图表类型:'line' 折线图,'bar' 柱状图,'stack' 堆叠图
|
||
const chartType = ref<'line' | 'bar' | 'stack'>('bar')
|
||
// 回访统计数据
|
||
const chartData = ref<{
|
||
date: string;
|
||
totalCount: number;
|
||
visitedCount: number;
|
||
unvisitedCount: number;
|
||
satisfiedCount: number;
|
||
normalCount: number;
|
||
unsatisfiedCount: number;
|
||
}[]>([])
|
||
// 时间范围,默认一周
|
||
const dateRange = ref<[string, string]>([
|
||
dayjs().subtract(6, 'day').startOf('day').format('YYYY-MM-DD 00:00:00'),
|
||
dayjs().endOf('day').format('YYYY-MM-DD 23:59:59')
|
||
])
|
||
|
||
// 渲染echarts图表
|
||
const renderChart = () => {
|
||
const dom = document.getElementById('main')
|
||
if (!dom) return
|
||
const myChart = echarts.init(dom)
|
||
|
||
// 当没有数据时显示提示
|
||
if (!chartData.value || chartData.value.length === 0) {
|
||
myChart.setOption(
|
||
{
|
||
title: {
|
||
text: '当前时间段无数据',
|
||
left: 'center',
|
||
top: 'center',
|
||
textStyle: { color: '#909399', fontSize: 16 }
|
||
},
|
||
// 清空系列的和轴线,避免旧内容残留
|
||
xAxis: { show: false },
|
||
yAxis: { show: false },
|
||
series: []
|
||
},
|
||
true
|
||
) // true表示不跟之前的option合并
|
||
return
|
||
}
|
||
|
||
const option: any = {
|
||
title: {
|
||
text: '电话回访统计',
|
||
left: 'center',
|
||
top: 0,
|
||
textStyle: {
|
||
fontSize: 18,
|
||
fontWeight: 'bold'
|
||
}
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
formatter: function(params: any) {
|
||
const date = params[0].name
|
||
let result = `${date}<br/>`
|
||
|
||
params.forEach((param: any) => {
|
||
result += `${param.seriesName}: ${param.value}<br/>`
|
||
})
|
||
|
||
return result
|
||
}
|
||
},
|
||
legend: {
|
||
data: ['总回访数', '已回访', '未回访', '满意', '一般', '不满意'],
|
||
top: 30
|
||
},
|
||
grid: { left: 40, right: 20, top: 80, bottom: 40 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: chartData.value.map(item => item.date),
|
||
axisLine: { lineStyle: { color: '#dcdfe6' } },
|
||
axisLabel: { color: '#666', fontSize: 14 }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
minInterval: 1,
|
||
axisLine: { lineStyle: { color: '#dcdfe6' } },
|
||
axisLabel: { color: '#666', fontSize: 14 }
|
||
},
|
||
series: []
|
||
}
|
||
|
||
if (chartType.value === 'stack') {
|
||
// 堆叠图:显示回访状态和满意度
|
||
option.series = [
|
||
{
|
||
name: '已回访',
|
||
type: 'bar',
|
||
stack: 'status',
|
||
data: chartData.value.map(item => item.visitedCount),
|
||
itemStyle: { color: '#67C23A' }
|
||
},
|
||
{
|
||
name: '未回访',
|
||
type: 'bar',
|
||
stack: 'status',
|
||
data: chartData.value.map(item => item.unvisitedCount),
|
||
itemStyle: { color: '#909399' }
|
||
},
|
||
{
|
||
name: '满意',
|
||
type: 'bar',
|
||
stack: 'result',
|
||
data: chartData.value.map(item => item.satisfiedCount),
|
||
itemStyle: { color: '#409EFF' }
|
||
},
|
||
{
|
||
name: '一般',
|
||
type: 'bar',
|
||
stack: 'result',
|
||
data: chartData.value.map(item => item.normalCount),
|
||
itemStyle: { color: '#E6A23C' }
|
||
},
|
||
{
|
||
name: '不满意',
|
||
type: 'bar',
|
||
stack: 'result',
|
||
data: chartData.value.map(item => item.unsatisfiedCount),
|
||
itemStyle: { color: '#F56C6C' }
|
||
}
|
||
]
|
||
} else {
|
||
// 普通柱状图或折线图
|
||
option.series = [
|
||
{
|
||
name: '总回访数',
|
||
type: chartType.value,
|
||
data: chartData.value.map(item => item.totalCount),
|
||
smooth: chartType.value === 'line',
|
||
barWidth: chartType.value === 'bar' ? '40%' : undefined,
|
||
itemStyle: {
|
||
color: '#409EFF',
|
||
borderRadius: chartType.value === 'bar' ? [6, 6, 0, 0] : 0
|
||
},
|
||
lineStyle: {
|
||
width: 3,
|
||
color: '#409EFF'
|
||
},
|
||
areaStyle: chartType.value === 'line' ? { color: 'rgba(64,158,255,0.15)' } : undefined
|
||
},
|
||
{
|
||
name: '已回访',
|
||
type: chartType.value,
|
||
data: chartData.value.map(item => item.visitedCount),
|
||
smooth: chartType.value === 'line',
|
||
barWidth: chartType.value === 'bar' ? '40%' : undefined,
|
||
itemStyle: {
|
||
color: '#67C23A',
|
||
borderRadius: chartType.value === 'bar' ? [6, 6, 0, 0] : 0
|
||
},
|
||
lineStyle: {
|
||
width: 3,
|
||
color: '#67C23A'
|
||
}
|
||
},
|
||
{
|
||
name: '未回访',
|
||
type: chartType.value,
|
||
data: chartData.value.map(item => item.unvisitedCount),
|
||
smooth: chartType.value === 'line',
|
||
barWidth: chartType.value === 'bar' ? '40%' : undefined,
|
||
itemStyle: {
|
||
color: '#909399',
|
||
borderRadius: chartType.value === 'bar' ? [6, 6, 0, 0] : 0
|
||
},
|
||
lineStyle: {
|
||
width: 3,
|
||
color: '#909399'
|
||
}
|
||
}
|
||
]
|
||
}
|
||
|
||
myChart.setOption(option)
|
||
}
|
||
|
||
// 渲染饼图
|
||
const renderPieChart = () => {
|
||
const dom = document.getElementById('pieChart')
|
||
if (!dom) return
|
||
const pieChart = echarts.init(dom)
|
||
|
||
// 当没有数据时显示提示
|
||
if (!chartData.value || chartData.value.length === 0) {
|
||
pieChart.setOption(
|
||
{
|
||
title: {
|
||
text: '当前时间段无数据',
|
||
left: 'center',
|
||
top: 'center',
|
||
textStyle: { color: '#909399', fontSize: 16 }
|
||
},
|
||
series: []
|
||
},
|
||
true
|
||
)
|
||
return
|
||
}
|
||
|
||
// 计算总数
|
||
const totalVisited = chartData.value.reduce((sum, item) => sum + item.visitedCount, 0)
|
||
const totalUnvisited = chartData.value.reduce((sum, item) => sum + item.unvisitedCount, 0)
|
||
const totalSatisfied = chartData.value.reduce((sum, item) => sum + item.satisfiedCount, 0)
|
||
const totalNormal = chartData.value.reduce((sum, item) => sum + item.normalCount, 0)
|
||
const totalUnsatisfied = chartData.value.reduce((sum, item) => sum + item.unsatisfiedCount, 0)
|
||
|
||
pieChart.setOption({
|
||
title: {
|
||
text: '回访状态分布',
|
||
left: 'center',
|
||
top: 0,
|
||
textStyle: {
|
||
fontSize: 16,
|
||
fontWeight: 'bold'
|
||
}
|
||
},
|
||
tooltip: {
|
||
trigger: 'item',
|
||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||
},
|
||
legend: {
|
||
orient: 'vertical',
|
||
left: 'left',
|
||
top: 'middle'
|
||
},
|
||
series: [
|
||
{
|
||
name: '回访状态',
|
||
type: 'pie',
|
||
radius: ['40%', '70%'],
|
||
center: ['65%', '50%'],
|
||
avoidLabelOverlap: false,
|
||
label: {
|
||
show: false,
|
||
position: 'center'
|
||
},
|
||
emphasis: {
|
||
label: {
|
||
show: true,
|
||
fontSize: '18',
|
||
fontWeight: 'bold'
|
||
}
|
||
},
|
||
labelLine: {
|
||
show: false
|
||
},
|
||
data: [
|
||
{ value: totalVisited, name: '已回访', itemStyle: { color: '#67C23A' } },
|
||
{ value: totalUnvisited, name: '未回访', itemStyle: { color: '#909399' } }
|
||
]
|
||
}
|
||
]
|
||
})
|
||
}
|
||
|
||
const params = ref({
|
||
startTime: dateRange.value[0],
|
||
endTime: dateRange.value[1],
|
||
orgid: 0,
|
||
})
|
||
|
||
// 获取回访统计数据
|
||
const fetchData = async () => {
|
||
try {
|
||
const userProfile = await getUserProfile()
|
||
params.value.orgid = userProfile.dept.id
|
||
|
||
// 调用回访统计的API
|
||
const res = await RecordApi.getStatistics(
|
||
params.value.orgid,
|
||
params.value.startTime,
|
||
params.value.endTime
|
||
)
|
||
console.log(res)
|
||
// 处理返回的数据格式
|
||
if (res && Array.isArray(res)) {
|
||
chartData.value = res.map((item: any) => {
|
||
// 处理date字段
|
||
let dateStr = ''
|
||
if (typeof item.date === 'number') {
|
||
// 时间戳格式
|
||
dateStr = dayjs(item.date).format('YYYY-MM-DD')
|
||
} else if (Array.isArray(item.date) && item.date.length >= 3) {
|
||
// 数组格式 [year, month, day]
|
||
const [year, month, day] = item.date
|
||
// 月份需要减1,因为JavaScript的Date构造函数中月份是从0开始的
|
||
const date = new Date(year, month - 1, day)
|
||
dateStr = dayjs(date).format('YYYY-MM-DD')
|
||
} else {
|
||
// 默认是字符串格式
|
||
dateStr = item.date
|
||
}
|
||
|
||
return {
|
||
date: dateStr,
|
||
totalCount: item.totalCount || 0,
|
||
visitedCount: item.visitedCount || 0,
|
||
unvisitedCount: item.unvisitedCount || 0,
|
||
satisfiedCount: item.satisfiedCount || 0,
|
||
normalCount: item.normalCount || 0,
|
||
unsatisfiedCount: item.unsatisfiedCount || 0
|
||
}
|
||
})
|
||
} else {
|
||
chartData.value = []
|
||
}
|
||
|
||
console.log('处理后的图表数据:', chartData.value)
|
||
} catch (error) {
|
||
console.error('获取回访统计数据失败:', error)
|
||
chartData.value = []
|
||
}
|
||
}
|
||
|
||
// 时间范围变化时
|
||
const onDateChange = (range: [string, string] | null) => {
|
||
// 当range不为空时,更新参数并获取数据
|
||
if (range && range.length === 2) {
|
||
dateRange.value = range
|
||
params.value.startTime = range[0]
|
||
params.value.endTime = range[1]
|
||
fetchData()
|
||
} else {
|
||
// 当日期被清空时,清空图表数据
|
||
chartData.value = []
|
||
}
|
||
}
|
||
|
||
// 监听数据和图表类型变化,自动渲染图表
|
||
watch([chartData, chartType], () => {
|
||
renderChart()
|
||
renderPieChart()
|
||
})
|
||
|
||
// 默认一周,首次渲染
|
||
onMounted(() => {
|
||
fetchData()
|
||
setTimeout(() => {
|
||
renderChart()
|
||
renderPieChart()
|
||
}, 300)
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.member-stat-card {
|
||
border-radius: 16px;
|
||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.04);
|
||
background: #fff;
|
||
padding: 24px 24px 16px 24px;
|
||
min-height: 80px; /* 卡片最小高度,默认80px,具体卡片可单独覆盖 */
|
||
}
|
||
.search-card {
|
||
height: 48px;
|
||
padding-top: 5px;
|
||
padding-bottom: 12px;
|
||
}
|
||
.mb-4 {
|
||
margin-bottom: 24px;
|
||
}
|
||
@media (max-width: 768px) {
|
||
.member-stat-card {
|
||
padding: 12px;
|
||
}
|
||
.mb-4 {
|
||
margin-bottom: 12px;
|
||
}
|
||
}
|
||
.stat-card {
|
||
min-height: 320px; /* 统计图卡片主内容区高度,可根据实际需求调整 */
|
||
}
|
||
|
||
.chart-container {
|
||
display: flex;
|
||
gap: 20px;
|
||
height: 580px;
|
||
}
|
||
|
||
.line-chart {
|
||
flex: 2;
|
||
height: 100%;
|
||
}
|
||
|
||
.pie-chart {
|
||
flex: 1;
|
||
height: 100%;
|
||
min-width: 300px;
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.chart-container {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.line-chart,
|
||
.pie-chart {
|
||
flex: none;
|
||
height: 400px;
|
||
}
|
||
}
|
||
</style>
|