|
@@ -0,0 +1,631 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="app-container">
|
|
|
|
|
+ <div class="app-content">
|
|
|
|
|
+ <div class="title">手动外呼看板</div>
|
|
|
|
|
+ <!-- 筛选区域 -->
|
|
|
|
|
+ <el-form class="search-form" :model="queryParams" ref="queryForm" :inline="true">
|
|
|
|
|
+ <el-form-item label="日期范围" prop="dateRange">
|
|
|
|
|
+ <el-date-picker
|
|
|
|
|
+ v-model="dateRange"
|
|
|
|
|
+ type="daterange"
|
|
|
|
|
+ value-format="yyyy-MM-dd"
|
|
|
|
|
+ start-placeholder="开始日期"
|
|
|
|
|
+ end-placeholder="结束日期"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ style="width: 240px"
|
|
|
|
|
+ ></el-date-picker>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
|
|
|
|
+ <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 汇总卡片区域 -->
|
|
|
|
|
+ <el-row :gutter="20" class="summary-cards">
|
|
|
|
|
+ <el-col :span="6">
|
|
|
|
|
+ <el-card class="summary-card card-total" shadow="hover">
|
|
|
|
|
+ <div class="card-content">
|
|
|
|
|
+ <div class="card-icon"><i class="el-icon-phone-outline"></i></div>
|
|
|
|
|
+ <div class="card-info">
|
|
|
|
|
+ <div class="card-label">总外呼数</div>
|
|
|
|
|
+ <div class="card-value">{{ summary.totalCalls }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="6">
|
|
|
|
|
+ <el-card class="summary-card card-connected" shadow="hover">
|
|
|
|
|
+ <div class="card-content">
|
|
|
|
|
+ <div class="card-icon"><i class="el-icon-phone"></i></div>
|
|
|
|
|
+ <div class="card-info">
|
|
|
|
|
+ <div class="card-label">接通数</div>
|
|
|
|
|
+ <div class="card-value">{{ summary.connectedCalls }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="6">
|
|
|
|
|
+ <el-card class="summary-card card-rate" shadow="hover">
|
|
|
|
|
+ <div class="card-content">
|
|
|
|
|
+ <div class="card-icon"><i class="el-icon-data-analysis"></i></div>
|
|
|
|
|
+ <div class="card-info">
|
|
|
|
|
+ <div class="card-label">接通率(%)</div>
|
|
|
|
|
+ <div class="card-value">{{ summary.connectRate }}%</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="6">
|
|
|
|
|
+ <el-card class="summary-card card-duration" shadow="hover">
|
|
|
|
|
+ <div class="card-content">
|
|
|
|
|
+ <div class="card-icon"><i class="el-icon-timer"></i></div>
|
|
|
|
|
+ <div class="card-info">
|
|
|
|
|
+ <div class="card-label">平均通话时长(秒)</div>
|
|
|
|
|
+ <div class="card-value">{{ summary.avgDuration }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 每日外呼趋势 -->
|
|
|
|
|
+ <el-card class="box-card" style="margin-bottom: 20px;" v-if="isMultiDay">
|
|
|
|
|
+ <div slot="header">每日外呼趋势</div>
|
|
|
|
|
+ <div id="trendChart" class="trend-chart"></div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 图表区域 -->
|
|
|
|
|
+ <el-row :gutter="20" class="chart-row">
|
|
|
|
|
+ <el-col :span="14">
|
|
|
|
|
+ <el-card shadow="never">
|
|
|
|
|
+ <div slot="header"><span>销售外呼统计</span></div>
|
|
|
|
|
+ <div id="barChart" class="chart-container"></div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="10">
|
|
|
|
|
+ <el-card shadow="never">
|
|
|
|
|
+ <div slot="header"><span>通话状态分布</span></div>
|
|
|
|
|
+ <div id="pieChart" class="chart-container"></div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 客户跟进阶段分布 -->
|
|
|
|
|
+ <el-card shadow="never" class="chart-row">
|
|
|
|
|
+ <div slot="header"><span>客户跟进阶段分布</span></div>
|
|
|
|
|
+ <div v-if="visitStatusList && visitStatusList.length > 0" id="visitStatusChart" class="visit-status-chart"></div>
|
|
|
|
|
+ <div v-else class="no-data">暂无数据</div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 明细表格区域 -->
|
|
|
|
|
+ <el-card shadow="never" class="table-card">
|
|
|
|
|
+ <div slot="header"><span>销售明细数据</span></div>
|
|
|
|
|
+ <el-table v-loading="loading" :data="list" border stripe style="width: 100%">
|
|
|
|
|
+ <el-table-column label="销售姓名" align="center" prop="companyUserName" />
|
|
|
|
|
+ <el-table-column label="总外呼数" align="center" prop="totalCalls" />
|
|
|
|
|
+ <el-table-column label="接通数" align="center" prop="connectedCalls" />
|
|
|
|
|
+ <el-table-column label="未接通数" align="center" prop="unconnectedCalls" />
|
|
|
|
|
+ <el-table-column label="接通率(%)" align="center" prop="connectRate">
|
|
|
|
|
+ <template slot-scope="scope">
|
|
|
|
|
+ <span :style="{ color: scope.row.connectRate >= 50 ? '#67C23A' : '#F56C6C', fontWeight: 'bold' }">
|
|
|
|
|
+ {{ scope.row.connectRate.toFixed(1) }}%
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="总通话时长" align="center" prop="totalDuration">
|
|
|
|
|
+ <template slot-scope="scope">
|
|
|
|
|
+ {{ formatDuration(scope.row.totalDuration) }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="平均通话时长" align="center" prop="avgDuration">
|
|
|
|
|
+ <template slot-scope="scope">
|
|
|
|
|
+ {{ formatDuration(scope.row.avgDuration) }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import { getCallStatistics, getVisitStatusStatistics, getDailyCallTrend } from "@/api/crm/manualOutboundCallDashboard";
|
|
|
|
|
+import { getTradeDicts } from "@/api/crm/customer";
|
|
|
|
|
+import echarts from 'echarts'
|
|
|
|
|
+import resize from '../../dashboard/mixins/resize'
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: "ManualOutboundCallDashboard",
|
|
|
|
|
+ mixins: [resize],
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ loading: false,
|
|
|
|
|
+ // 日期范围
|
|
|
|
|
+ dateRange: [],
|
|
|
|
|
+ // 查询参数
|
|
|
|
|
+ queryParams: {},
|
|
|
|
|
+ // 列表数据
|
|
|
|
|
+ list: [],
|
|
|
|
|
+ // 汇总数据
|
|
|
|
|
+ summary: {
|
|
|
|
|
+ totalCalls: 0,
|
|
|
|
|
+ connectedCalls: 0,
|
|
|
|
|
+ connectRate: '0.0',
|
|
|
|
|
+ avgDuration: 0
|
|
|
|
|
+ },
|
|
|
|
|
+ // 跟进阶段统计数据
|
|
|
|
|
+ visitStatusList: [],
|
|
|
|
|
+ // 跟进阶段字典选项
|
|
|
|
|
+ statusOptions: [],
|
|
|
|
|
+ // 每日趋势数据
|
|
|
|
|
+ dailyTrendList: [],
|
|
|
|
|
+ // 是否为多天查询
|
|
|
|
|
+ isMultiDay: false,
|
|
|
|
|
+ // 图表实例
|
|
|
|
|
+ chart: null,
|
|
|
|
|
+ chart1: null,
|
|
|
|
|
+ visitStatusChart: null,
|
|
|
|
|
+ trendChart: null
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ created() {
|
|
|
|
|
+ this.initDefaultDate()
|
|
|
|
|
+ this.loadDictData().then(() => {
|
|
|
|
|
+ this.getList()
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ /** 初始化默认日期为今天 */
|
|
|
|
|
+ initDefaultDate() {
|
|
|
|
|
+ const today = new Date()
|
|
|
|
|
+ const year = today.getFullYear()
|
|
|
|
|
+ const month = String(today.getMonth() + 1).padStart(2, '0')
|
|
|
|
|
+ const day = String(today.getDate()).padStart(2, '0')
|
|
|
|
|
+ const dateStr = `${year}-${month}-${day}`
|
|
|
|
|
+ this.dateRange = [dateStr, dateStr]
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 获取统计数据 */
|
|
|
|
|
+ getList() {
|
|
|
|
|
+ this.loading = true
|
|
|
|
|
+ const params = {}
|
|
|
|
|
+ if (this.dateRange && this.dateRange.length === 2) {
|
|
|
|
|
+ params.startTime = this.dateRange[0]
|
|
|
|
|
+ params.endTime = this.dateRange[1]
|
|
|
|
|
+ }
|
|
|
|
|
+ // 判断是否为多天查询
|
|
|
|
|
+ const startTime = params.startTime || ''
|
|
|
|
|
+ const endTime = params.endTime || ''
|
|
|
|
|
+ this.isMultiDay = (startTime !== endTime)
|
|
|
|
|
+
|
|
|
|
|
+ getCallStatistics(params).then(response => {
|
|
|
|
|
+ this.list = response.data || []
|
|
|
|
|
+ this.calcSummary()
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ this.initBarChart()
|
|
|
|
|
+ this.initPieChart()
|
|
|
|
|
+ })
|
|
|
|
|
+ this.loading = false
|
|
|
|
|
+ }).catch(() => {
|
|
|
|
|
+ this.loading = false
|
|
|
|
|
+ })
|
|
|
|
|
+ // 获取跟进阶段统计
|
|
|
|
|
+ getVisitStatusStatistics(params).then(response => {
|
|
|
|
|
+ this.visitStatusList = response.data || []
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ this.initVisitStatusChart()
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ // 获取每日外呼趋势
|
|
|
|
|
+ if (this.isMultiDay) {
|
|
|
|
|
+ getDailyCallTrend(params).then(response => {
|
|
|
|
|
+ this.dailyTrendList = response.data || []
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ this.initTrendChart()
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.dailyTrendList = []
|
|
|
|
|
+ if (this.trendChart) {
|
|
|
|
|
+ this.trendChart.dispose()
|
|
|
|
|
+ this.trendChart = null
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 计算汇总数据 */
|
|
|
|
|
+ calcSummary() {
|
|
|
|
|
+ let totalCalls = 0
|
|
|
|
|
+ let connectedCalls = 0
|
|
|
|
|
+ let totalDuration = 0
|
|
|
|
|
+ this.list.forEach(item => {
|
|
|
|
|
+ totalCalls += item.totalCalls || 0
|
|
|
|
|
+ connectedCalls += item.connectedCalls || 0
|
|
|
|
|
+ totalDuration += item.totalDuration || 0
|
|
|
|
|
+ })
|
|
|
|
|
+ const connectRate = totalCalls > 0 ? (connectedCalls / totalCalls * 100).toFixed(1) : '0.0'
|
|
|
|
|
+ const avgDuration = connectedCalls > 0 ? Math.round(totalDuration / connectedCalls) : 0
|
|
|
|
|
+ this.summary = {
|
|
|
|
|
+ totalCalls,
|
|
|
|
|
+ connectedCalls,
|
|
|
|
|
+ connectRate,
|
|
|
|
|
+ avgDuration
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 初始化柱状图 */
|
|
|
|
|
+ initBarChart() {
|
|
|
|
|
+ if (this.chart) {
|
|
|
|
|
+ this.chart.dispose()
|
|
|
|
|
+ }
|
|
|
|
|
+ const chartDom = document.getElementById('barChart')
|
|
|
|
|
+ if (!chartDom) return
|
|
|
|
|
+ this.chart = echarts.init(chartDom)
|
|
|
|
|
+ const names = this.list.map(item => item.companyUserName)
|
|
|
|
|
+ const totalData = this.list.map(item => item.totalCalls)
|
|
|
|
|
+ const connectedData = this.list.map(item => item.connectedCalls)
|
|
|
|
|
+ const option = {
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ trigger: 'axis',
|
|
|
|
|
+ axisPointer: { type: 'shadow' }
|
|
|
|
|
+ },
|
|
|
|
|
+ legend: {
|
|
|
|
|
+ data: ['总外呼数', '接通数']
|
|
|
|
|
+ },
|
|
|
|
|
+ grid: {
|
|
|
|
|
+ left: '3%',
|
|
|
|
|
+ right: '4%',
|
|
|
|
|
+ bottom: '3%',
|
|
|
|
|
+ containLabel: true
|
|
|
|
|
+ },
|
|
|
|
|
+ xAxis: {
|
|
|
|
|
+ type: 'category',
|
|
|
|
|
+ data: names,
|
|
|
|
|
+ axisLabel: {
|
|
|
|
|
+ rotate: names.length > 6 ? 30 : 0
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ yAxis: {
|
|
|
|
|
+ type: 'value'
|
|
|
|
|
+ },
|
|
|
|
|
+ series: [
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '总外呼数',
|
|
|
|
|
+ type: 'bar',
|
|
|
|
|
+ barWidth: '30%',
|
|
|
|
|
+ itemStyle: { color: '#409EFF' },
|
|
|
|
|
+ data: totalData
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '接通数',
|
|
|
|
|
+ type: 'bar',
|
|
|
|
|
+ barWidth: '30%',
|
|
|
|
|
+ itemStyle: { color: '#67C23A' },
|
|
|
|
|
+ data: connectedData
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ this.chart.setOption(option, true)
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 初始化饼图 */
|
|
|
|
|
+ initPieChart() {
|
|
|
|
|
+ if (this.chart1) {
|
|
|
|
|
+ this.chart1.dispose()
|
|
|
|
|
+ }
|
|
|
|
|
+ const chartDom = document.getElementById('pieChart')
|
|
|
|
|
+ if (!chartDom) return
|
|
|
|
|
+ this.chart1 = echarts.init(chartDom)
|
|
|
|
|
+ let connectedTotal = 0
|
|
|
|
|
+ let unconnectedTotal = 0
|
|
|
|
|
+ this.list.forEach(item => {
|
|
|
|
|
+ connectedTotal += item.connectedCalls || 0
|
|
|
|
|
+ unconnectedTotal += item.unconnectedCalls || 0
|
|
|
|
|
+ })
|
|
|
|
|
+ const option = {
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ trigger: 'item',
|
|
|
|
|
+ formatter: '{a} <br/>{b}: {c} ({d}%)'
|
|
|
|
|
+ },
|
|
|
|
|
+ legend: {
|
|
|
|
|
+ bottom: '5%',
|
|
|
|
|
+ data: ['接通', '未接通']
|
|
|
|
|
+ },
|
|
|
|
|
+ series: [
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '通话状态',
|
|
|
|
|
+ type: 'pie',
|
|
|
|
|
+ radius: ['40%', '65%'],
|
|
|
|
|
+ center: ['50%', '45%'],
|
|
|
|
|
+ label: {
|
|
|
|
|
+ formatter: '{b}: {c}\n({d}%)'
|
|
|
|
|
+ },
|
|
|
|
|
+ data: [
|
|
|
|
|
+ { value: connectedTotal, name: '接通', itemStyle: { color: '#67C23A' } },
|
|
|
|
|
+ { value: unconnectedTotal, name: '未接通', itemStyle: { color: '#F56C6C' } }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ this.chart1.setOption(option, true)
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 格式化通话时长为 X分Y秒 */
|
|
|
|
|
+ formatDuration(seconds) {
|
|
|
|
|
+ if (!seconds || seconds <= 0) return '0秒'
|
|
|
|
|
+ const min = Math.floor(seconds / 60)
|
|
|
|
|
+ const sec = seconds % 60
|
|
|
|
|
+ if (min > 0) {
|
|
|
|
|
+ return sec > 0 ? `${min}分${sec}秒` : `${min}分`
|
|
|
|
|
+ }
|
|
|
|
|
+ return `${sec}秒`
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 搜索按钮操作 */
|
|
|
|
|
+ handleQuery() {
|
|
|
|
|
+ this.getList()
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 重置按钮操作 */
|
|
|
|
|
+ resetQuery() {
|
|
|
|
|
+ this.visitStatusList = []
|
|
|
|
|
+ this.initDefaultDate()
|
|
|
|
|
+ this.handleQuery()
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 加载字典数据 */
|
|
|
|
|
+ loadDictData() {
|
|
|
|
|
+ this.dictPromise = getTradeDicts().then(response => {
|
|
|
|
|
+ const data = response.data || {}
|
|
|
|
|
+ this.statusOptions = data["crm_customer_user_status"] || []
|
|
|
|
|
+ })
|
|
|
|
|
+ return this.dictPromise
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 根据 visitStatus 值获取字典标签 */
|
|
|
|
|
+ getVisitStatusLabel(value) {
|
|
|
|
|
+ if (!value && value !== 0) return '未设置'
|
|
|
|
|
+ const strValue = String(value)
|
|
|
|
|
+ const item = this.statusOptions.find(dict => String(dict.dictValue) === strValue)
|
|
|
|
|
+ return item ? item.dictLabel : ('阶段' + value)
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 初始化跟进阶段图表 */
|
|
|
|
|
+ initVisitStatusChart() {
|
|
|
|
|
+ if (this.visitStatusChart) {
|
|
|
|
|
+ this.visitStatusChart.dispose()
|
|
|
|
|
+ }
|
|
|
|
|
+ const chartDom = document.getElementById('visitStatusChart')
|
|
|
|
|
+ if (!chartDom) return
|
|
|
|
|
+ if (!this.visitStatusList || this.visitStatusList.length === 0) return
|
|
|
|
|
+ this.visitStatusChart = echarts.init(chartDom)
|
|
|
|
|
+ const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#9B59B6', '#1ABC9C', '#F39C12']
|
|
|
|
|
+ const names = this.visitStatusList.map(item => this.getVisitStatusLabel(item.visitStatus))
|
|
|
|
|
+ const values = this.visitStatusList.map(item => item.customerCount)
|
|
|
|
|
+ const seriesData = values.map((val, idx) => ({
|
|
|
|
|
+ value: val,
|
|
|
|
|
+ itemStyle: { color: colors[idx % colors.length] }
|
|
|
|
|
+ }))
|
|
|
|
|
+ const option = {
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ trigger: 'axis',
|
|
|
|
|
+ axisPointer: { type: 'shadow' }
|
|
|
|
|
+ },
|
|
|
|
|
+ grid: {
|
|
|
|
|
+ left: '3%',
|
|
|
|
|
+ right: '6%',
|
|
|
|
|
+ bottom: '3%',
|
|
|
|
|
+ top: '40px',
|
|
|
|
|
+ containLabel: true
|
|
|
|
|
+ },
|
|
|
|
|
+ xAxis: {
|
|
|
|
|
+ type: 'category',
|
|
|
|
|
+ data: names,
|
|
|
|
|
+ axisLabel: {
|
|
|
|
|
+ rotate: names.length > 6 ? 30 : 0
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ yAxis: {
|
|
|
|
|
+ type: 'value',
|
|
|
|
|
+ name: '客户数量'
|
|
|
|
|
+ },
|
|
|
|
|
+ series: [
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '客户数量',
|
|
|
|
|
+ type: 'bar',
|
|
|
|
|
+ barWidth: '40%',
|
|
|
|
|
+ data: seriesData
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ this.visitStatusChart.setOption(option, true)
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 初始化每日外呼趋势图 */
|
|
|
|
|
+ initTrendChart() {
|
|
|
|
|
+ if (this.trendChart) {
|
|
|
|
|
+ this.trendChart.dispose()
|
|
|
|
|
+ }
|
|
|
|
|
+ const chartDom = document.getElementById('trendChart')
|
|
|
|
|
+ if (!chartDom) return
|
|
|
|
|
+ this.trendChart = echarts.init(chartDom)
|
|
|
|
|
+ const dates = this.dailyTrendList.map(item => item.callDate ? item.callDate.substring(5) : '')
|
|
|
|
|
+ const totalData = this.dailyTrendList.map(item => item.totalCalls || 0)
|
|
|
|
|
+ const connectedData = this.dailyTrendList.map(item => item.connectedCalls || 0)
|
|
|
|
|
+ const option = {
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ trigger: 'axis',
|
|
|
|
|
+ formatter: function(params) {
|
|
|
|
|
+ let tip = params[0].axisValue + '<br/>'
|
|
|
|
|
+ params.forEach(item => {
|
|
|
|
|
+ tip += item.marker + item.seriesName + ': ' + item.value + '<br/>'
|
|
|
|
|
+ })
|
|
|
|
|
+ return tip
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ legend: {
|
|
|
|
|
+ data: ['外呼总数', '接通数']
|
|
|
|
|
+ },
|
|
|
|
|
+ grid: {
|
|
|
|
|
+ left: '3%',
|
|
|
|
|
+ right: '4%',
|
|
|
|
|
+ bottom: '3%',
|
|
|
|
|
+ top: '40px',
|
|
|
|
|
+ containLabel: true
|
|
|
|
|
+ },
|
|
|
|
|
+ xAxis: {
|
|
|
|
|
+ type: 'category',
|
|
|
|
|
+ boundaryGap: true,
|
|
|
|
|
+ data: dates
|
|
|
|
|
+ },
|
|
|
|
|
+ yAxis: {
|
|
|
|
|
+ type: 'value'
|
|
|
|
|
+ },
|
|
|
|
|
+ series: [
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '外呼总数',
|
|
|
|
|
+ type: 'bar',
|
|
|
|
|
+ barWidth: '30%',
|
|
|
|
|
+ itemStyle: { color: '#409EFF' },
|
|
|
|
|
+ data: totalData
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '接通数',
|
|
|
|
|
+ type: 'line',
|
|
|
|
|
+ smooth: false,
|
|
|
|
|
+ itemStyle: { color: '#67C23A' },
|
|
|
|
|
+ lineStyle: { width: 2 },
|
|
|
|
|
+ data: connectedData
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ this.trendChart.setOption(option, true)
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 图表响应式 - 覆盖 mixin 的 resize */
|
|
|
|
|
+ resize() {
|
|
|
|
|
+ const { chart, chart1, visitStatusChart, trendChart } = this
|
|
|
|
|
+ chart && chart.resize()
|
|
|
|
|
+ chart1 && chart1.resize()
|
|
|
|
|
+ visitStatusChart && visitStatusChart.resize()
|
|
|
|
|
+ trendChart && trendChart.resize()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ beforeDestroy() {
|
|
|
|
|
+ if (this.chart) {
|
|
|
|
|
+ this.chart.dispose()
|
|
|
|
|
+ this.chart = null
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.chart1) {
|
|
|
|
|
+ this.chart1.dispose()
|
|
|
|
|
+ this.chart1 = null
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.visitStatusChart) {
|
|
|
|
|
+ this.visitStatusChart.dispose()
|
|
|
|
|
+ this.visitStatusChart = null
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.trendChart) {
|
|
|
|
|
+ this.trendChart.dispose()
|
|
|
|
|
+ this.trendChart = null
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+.app-container {
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .app-content {
|
|
|
|
|
+ background-color: #fff;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+
|
|
|
|
|
+ .title {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .search-form {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.summary-cards {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+
|
|
|
|
|
+ .summary-card {
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+
|
|
|
|
|
+ .card-content {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .card-icon {
|
|
|
|
|
+ font-size: 36px;
|
|
|
|
|
+ margin-right: 16px;
|
|
|
|
|
+ width: 50px;
|
|
|
|
|
+ height: 50px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .card-info {
|
|
|
|
|
+ .card-label {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .card-value {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .card-total .card-icon {
|
|
|
|
|
+ background-color: #ecf5ff;
|
|
|
|
|
+ color: #409EFF;
|
|
|
|
|
+ }
|
|
|
|
|
+ .card-connected .card-icon {
|
|
|
|
|
+ background-color: #f0f9eb;
|
|
|
|
|
+ color: #67C23A;
|
|
|
|
|
+ }
|
|
|
|
|
+ .card-rate .card-icon {
|
|
|
|
|
+ background-color: #fdf6ec;
|
|
|
|
|
+ color: #E6A23C;
|
|
|
|
|
+ }
|
|
|
|
|
+ .card-duration .card-icon {
|
|
|
|
|
+ background-color: #f4ecff;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chart-row {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chart-container {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 350px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.visit-status-chart {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 340px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.trend-chart {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 300px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.no-data {
|
|
|
|
|
+ height: 300px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.table-card {
|
|
|
|
|
+ margin-top: 0;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|