- 在PersonalDashboard组件中实现实时分析报告功能,包括数据为空和加载状态处理 - 添加SimpleChatService集成用于生成分析报告 - 将API基础路径从本地开发环境切换到生产环境 - 优化分析报告模态框样式和错误消息显示
1012 lines
26 KiB
Vue
1012 lines
26 KiB
Vue
<template>
|
|
<div class="personal-dashboard">
|
|
<!-- 头部标题 -->
|
|
<div class="dashboard-header" style="display: flex; justify-content: space-between; align-items: center;">
|
|
<h2>个人工作仪表板</h2>
|
|
<button @click="showSecondOrderAnalysisReport">阶段分析报告</button>
|
|
</div>
|
|
|
|
<!-- 核心KPI & 统计卡片 -->
|
|
<div class="stats-grid">
|
|
<!-- 核心KPI -->
|
|
<div class="stat-card kpi-card">
|
|
<h3 class="card-title">核心KPI</h3>
|
|
<div class="kpi-grid">
|
|
<div class="kpi-item">
|
|
<div class="kpi-value">{{ props.kpiData.totalCalls }}</div>
|
|
<p>今日通话 <i class="info-icon" @mouseenter="showTooltip('totalCalls', $event)" @mouseleave="hideTooltip">ⓘ</i></p>
|
|
</div>
|
|
<div class="kpi-item">
|
|
<div class="kpi-value">{{ props.kpiData.successRate }}</div>
|
|
<p>电话接通率 <i class="info-icon" @mouseenter="showTooltip('successRate', $event)" @mouseleave="hideTooltip">ⓘ</i></p>
|
|
</div>
|
|
<div class="kpi-item">
|
|
<div class="kpi-value">{{ props.kpiData.avgDuration }}<span class="kpi-unit">min</span></div>
|
|
<p>平均通话时长 <i class="info-icon" @mouseenter="showTooltip('avgDuration', $event)" @mouseleave="hideTooltip">ⓘ</i></p>
|
|
</div>
|
|
<div class="kpi-item">
|
|
<div class="kpi-value">{{ props.kpiData.conversionRate }}</div>
|
|
<p>成交转化率 <i class="info-icon" @mouseenter="showTooltip('conversionRate', $event)" @mouseleave="hideTooltip">ⓘ</i></p>
|
|
</div>
|
|
<div class="kpi-item">
|
|
<div class="kpi-value">{{ props.kpiData.assignedData }}</div>
|
|
<p>本期分配数据 <i class="info-icon" @mouseenter="showTooltip('assignedData', $event)" @mouseleave="hideTooltip">ⓘ</i></p>
|
|
</div>
|
|
<div class="kpi-item">
|
|
<div class="kpi-value">{{ props.kpiData.wechatAddRate }}</div>
|
|
<p>加微率 <i class="info-icon" @mouseenter="showTooltip('wechatAddRate', $event)" @mouseleave="hideTooltip">ⓘ</i></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- 统计指标 -->
|
|
<StatisticData
|
|
:customerCommunicationRate="props.statisticsData.customerCommunicationRate"
|
|
:averageResponseTime="props.statisticsData.averageResponseTime"
|
|
:timeoutResponseRate="props.statisticsData.timeoutResponseRate"
|
|
:severeTimeoutRate="props.statisticsData.severeTimeoutRate"
|
|
:formCompletionRate="props.statisticsData.formCompletionRate"
|
|
/>
|
|
</div>
|
|
|
|
<!-- 图表和功能区 -->
|
|
<div class="charts-section">
|
|
<!-- 客户迫切解决的问题排行榜 -->
|
|
<div class="chart-container">
|
|
<div class="chart-header">
|
|
<h3>客户迫切解决的问题</h3>
|
|
</div>
|
|
<div class="chart-content">
|
|
<div class="problem-ranking">
|
|
<div v-for="(item, index) in sortedProblemData" :key="item.name" class="ranking-item" :class="getRankingClass(index)">
|
|
<div class="rank-number">
|
|
<span class="rank-badge" :class="getRankBadgeClass(index)">{{ index + 1 }}</span>
|
|
</div>
|
|
<div class="problem-info">
|
|
<div class="problem-name">{{ item.name }}</div>
|
|
<div class="problem-count">{{ item.value }}次咨询</div>
|
|
</div>
|
|
<div class="problem-percentage">
|
|
<span class="percentage">{{ getPercentage(item.value) }}%</span>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" :style="{ width: getPercentage(item.value) + '%' }"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 销售漏斗 -->
|
|
<div class="chart-container">
|
|
<div class="chart-header">
|
|
<h3>销售漏斗</h3>
|
|
</div>
|
|
<div class="chart-content">
|
|
<canvas ref="personalFunnelChartCanvas"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 黄金联络时段 -->
|
|
<div class="chart-container">
|
|
<div class="chart-header">
|
|
<h3>黄金联络时段</h3>
|
|
</div>
|
|
<div class="chart-content">
|
|
<canvas ref="contactTimeChartCanvas"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 指标说明 Tooltip -->
|
|
<Tooltip
|
|
:visible="tooltip.visible"
|
|
:x="tooltip.x"
|
|
:y="tooltip.y"
|
|
:title="tooltip.title"
|
|
:description="tooltip.description"
|
|
/>
|
|
|
|
<!-- 阶段分析报告弹框 -->
|
|
<div v-if="showAnalysisModal" class="modal-overlay" @click.self="closeAnalysisModal">
|
|
<div class="modal-container">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">阶段分析报告</h3>
|
|
<div class="period-switcher">
|
|
<button @click="switchAnalysisPeriod('day')" :class="{ active: analysisPeriod === 'day' }">当日</button>
|
|
<button @click="switchAnalysisPeriod('camp')" :class="{ active: analysisPeriod === 'camp' }">当期</button>
|
|
<button @click="switchAnalysisPeriod('month')" :class="{ active: analysisPeriod === 'month' }">当月</button>
|
|
</div>
|
|
<button class="modal-close-btn" @click="closeAnalysisModal">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="analysis-content">
|
|
<div v-if="analysisPeriod === 'day'">
|
|
<h4>当日分析报告</h4>
|
|
<div v-if="analysisReport.day === '数据为空'" class="error-message">数据为空</div>
|
|
<div v-else-if="analysisReport.day" v-html="analysisReport.day.replace(/\n/g, '<br>')"></div>
|
|
<p v-else>正在生成分析报告...</p>
|
|
</div>
|
|
<div v-if="analysisPeriod === 'camp'">
|
|
<h4>当期分析报告</h4>
|
|
<div v-if="analysisReport.camp === '数据为空'" class="error-message">数据为空</div>
|
|
<div v-else-if="analysisReport.camp" v-html="analysisReport.camp.replace(/\n/g, '<br>')"></div>
|
|
<p v-else>正在生成分析报告...</p>
|
|
</div>
|
|
<div v-if="analysisPeriod === 'month'">
|
|
<h4>当月分析报告</h4>
|
|
<div v-if="analysisReport.month === '数据为空'" class="error-message">数据为空</div>
|
|
<div v-else-if="analysisReport.month" v-html="analysisReport.month.replace(/\n/g, '<br>')"></div>
|
|
<p v-else>正在生成分析报告...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import Tooltip from '@/components/Tooltip.vue';
|
|
import { ref, reactive, onMounted, onBeforeUnmount, computed, watch } from 'vue';
|
|
import StatisticData from './StatisticData.vue';
|
|
import * as echarts from 'echarts';
|
|
import Chart from 'chart.js/auto';
|
|
import {getSecondOrderAnalysisReport} from "@/api/api.js"
|
|
import { useUserStore } from "@/stores/user";
|
|
import { useRouter } from "vue-router";
|
|
import { SimpleChatService } from '@/utils/ChatService.js';
|
|
|
|
// 用户store
|
|
const userStore = useUserStore();
|
|
// 路由实例
|
|
const router = useRouter();
|
|
|
|
const Dify_API_Key_02 = 'app-MGaBOx5QFblsMZ7dSkxKJDKm'
|
|
const chatService_02= new SimpleChatService(Dify_API_Key_02)
|
|
|
|
// 获取通用请求参数的函数
|
|
const getRequestParams = () => {
|
|
const params = {}
|
|
// 只从路由参数获取
|
|
const routeUserLevel = router.currentRoute.value.query.user_level || router.currentRoute.value.params.user_level
|
|
const routeUserName = router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name
|
|
// 如果路由有参数,使用路由参数
|
|
if (routeUserLevel) {
|
|
params.user_level = routeUserLevel.toString()
|
|
}
|
|
if (routeUserName) {
|
|
params.user_name = routeUserName
|
|
}
|
|
return params
|
|
}
|
|
// 定义props
|
|
const props = defineProps({
|
|
kpiData: {
|
|
type: Object,
|
|
default: () => ({
|
|
totalCalls: 128,
|
|
successRate: 75,
|
|
avgDuration: 8.5,
|
|
conversionRate: 15,
|
|
assignedData: 256,
|
|
wechatAddRate: 68
|
|
})
|
|
},
|
|
funnelData: {
|
|
type: Object,
|
|
default: () => ({
|
|
labels: ['线索', '沟通', '意向', '预约', '成交'],
|
|
data: [200, 150, 90, 40, 12]
|
|
})
|
|
},
|
|
contactTimeData: {
|
|
type: Object,
|
|
default: () => ({})
|
|
},
|
|
statisticsData: {
|
|
type: Object,
|
|
default: () => ({
|
|
customerCommunicationRate: 0,
|
|
averageResponseTime: 0,
|
|
timeoutResponseRate: 0,
|
|
severeTimeoutRate: 0,
|
|
formCompletionRate: 0
|
|
})
|
|
},
|
|
urgentProblemData: {
|
|
type: Array,
|
|
default: () => []
|
|
}
|
|
});
|
|
|
|
async function CenterGetSecondOrderAnalysisReport(time) {
|
|
const params = getRequestParams()
|
|
const hasParams = {...params,time:time}
|
|
const res = await getSecondOrderAnalysisReport(hasParams)
|
|
if (res.code === 200) {
|
|
|
|
const records = res.data.records.join('\n')
|
|
|
|
// 检查数据是否为空
|
|
if (!records) {
|
|
console.error('数据为空')
|
|
// 将错误信息存储到对应的响应式变量中
|
|
analysisReport.value[time] = '数据为空'
|
|
return
|
|
}
|
|
|
|
const prompt = `请分析以下数据:\n${records}\n请提供一个阶段分析报告。`
|
|
console.log(prompt)
|
|
// 使用sendMessage方法替代chat方法
|
|
try {
|
|
await chatService_02.sendMessage(
|
|
prompt,
|
|
(update) => {
|
|
// 实时更新回调
|
|
if (!update.isStreaming) {
|
|
console.log('阶段分析报告:', update.content)
|
|
// 将结果存储到对应的响应式变量中
|
|
analysisReport.value[time] = update.content
|
|
}
|
|
},
|
|
() => {
|
|
// 流结束回调
|
|
console.log('阶段分析报告生成完成')
|
|
}
|
|
)
|
|
} catch (error) {
|
|
console.error('获取阶段分析报告失败:', error)
|
|
}
|
|
}
|
|
}
|
|
// Chart.js 实例
|
|
const chartInstances = {};
|
|
|
|
// DOM 元素引用
|
|
const personalFunnelChartCanvas = ref(null);
|
|
const contactTimeChartCanvas = ref(null);
|
|
|
|
// Tooltip 相关数据
|
|
const tooltip = reactive({
|
|
visible: false,
|
|
x: 0,
|
|
y: 0,
|
|
title: '',
|
|
description: ''
|
|
});
|
|
|
|
// 指标说明配置
|
|
const kpiDescriptions = {
|
|
totalCalls: {
|
|
title: '今日通话',
|
|
description: '今日总共通话的次数。'
|
|
},
|
|
successRate: {
|
|
title: '电话接通率',
|
|
description: '拨通电话 ÷ 拨打的电话'
|
|
},
|
|
avgDuration: {
|
|
title: '平均通话时长',
|
|
description: '所有通话总时长 ÷ 拨打电话次数。'
|
|
},
|
|
conversionRate: {
|
|
title: '成交转化率',
|
|
description: '成交人数 ÷ 本期总数据。'
|
|
},
|
|
assignedData: {
|
|
title: '本期分配数据',
|
|
description: '本期内分配到的数据总量。'
|
|
},
|
|
wechatAddRate: {
|
|
title: '加微率',
|
|
description: '加微人数 ÷ 本期数据总人数'
|
|
}
|
|
};
|
|
|
|
// Chart.js 数据 - 使用props传递的数据
|
|
const funnelData = computed(() => props.funnelData);
|
|
const contactTimeData = computed(() => props.contactTimeData);
|
|
|
|
// --- 计算属性 ---
|
|
const sortedProblemData = computed(() => {
|
|
if (!props.urgentProblemData || !Array.isArray(props.urgentProblemData)) {
|
|
return [];
|
|
}
|
|
return [...props.urgentProblemData].sort((a, b) => b.value - a.value);
|
|
});
|
|
const totalProblemCount = computed(() => {
|
|
if (!props.urgentProblemData || !Array.isArray(props.urgentProblemData)) {
|
|
return 0;
|
|
}
|
|
return props.urgentProblemData.reduce((sum, item) => sum + item.value, 0);
|
|
});
|
|
|
|
// Chart.js: 创建或更新图表
|
|
const createOrUpdateChart = (chartId, canvasRef, config) => {
|
|
if (chartInstances[chartId]) {
|
|
chartInstances[chartId].destroy();
|
|
}
|
|
if (canvasRef.value) {
|
|
const ctx = canvasRef.value.getContext('2d');
|
|
chartInstances[chartId] = new Chart(ctx, config);
|
|
}
|
|
};
|
|
|
|
// Chart.js: 渲染销售漏斗图
|
|
const renderPersonalFunnelChart = () => {
|
|
const config = {
|
|
type: 'bar',
|
|
data: {
|
|
labels: funnelData.value.labels,
|
|
datasets: [{
|
|
label: '数量', data: funnelData.value.data,
|
|
backgroundColor: ['rgba(59, 130, 246, 0.8)', 'rgba(16, 185, 129, 0.8)', 'rgba(245, 158, 11, 0.8)', 'rgba(239, 68, 68, 0.8)', 'rgba(168, 85, 247, 0.8)'],
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true, maintainAspectRatio: false, indexAxis: 'y',
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
y: { grid: { display: false }, ticks: { color: '#64748b', font: { size: 11 } } },
|
|
x: { beginAtZero: true, grid: { color: 'rgba(148, 163, 184, 0.2)' }, ticks: { color: '#64748b', font: { size: 11 } } }
|
|
}
|
|
}
|
|
};
|
|
createOrUpdateChart('personalFunnel', personalFunnelChartCanvas, config);
|
|
};
|
|
|
|
// Chart.js: 渲染黄金联络时段图
|
|
const renderContactTimeChart = () => {
|
|
if (!props.contactTimeData || !props.contactTimeData.gold_contact_success_rate) {
|
|
return;
|
|
}
|
|
|
|
const labels = Object.keys(props.contactTimeData.gold_contact_success_rate);
|
|
const data = Object.values(props.contactTimeData.gold_contact_success_rate).map(rate => parseFloat(rate));
|
|
|
|
const config = {
|
|
type: 'line',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: '成功率', data: data,
|
|
borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
borderWidth: 3, tension: 0.4, fill: true, pointRadius: 4,
|
|
pointBackgroundColor: '#10b981', pointBorderColor: '#ffffff', pointBorderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true, maintainAspectRatio: false,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
y: { beginAtZero: true, max: 100, grid: { color: 'rgba(148, 163, 184, 0.2)' }, ticks: { color: '#64748b', font: { size: 11 }, callback: (value) => value + '%' } },
|
|
x: { grid: { display: false }, ticks: { color: '#64748b', font: { size: 11 } } }
|
|
}
|
|
}
|
|
};
|
|
createOrUpdateChart('contactTime', contactTimeChartCanvas, config);
|
|
};
|
|
|
|
// 排行榜相关方法
|
|
const getPercentage = (value) => {
|
|
if (totalProblemCount.value === 0 || !value) {
|
|
return '0.0';
|
|
}
|
|
return ((value / totalProblemCount.value) * 100).toFixed(1);
|
|
};
|
|
const getRankingClass = (index) => ({ 'rank-first': index === 0, 'rank-second': index === 1, 'rank-third': index === 2, 'rank-other': index > 2 });
|
|
const getRankBadgeClass = (index) => ({ 'badge-gold': index === 0, 'badge-silver': index === 1, 'badge-bronze': index === 2, 'badge-default': index > 2 });
|
|
|
|
// Tooltip 相关方法
|
|
const showTooltip = (kpiType, event) => {
|
|
const description = kpiDescriptions[kpiType];
|
|
if (description) {
|
|
tooltip.title = description.title;
|
|
tooltip.description = description.description;
|
|
tooltip.x = event.clientX + 10;
|
|
tooltip.y = event.clientY - 10;
|
|
tooltip.visible = true;
|
|
}
|
|
};
|
|
|
|
const hideTooltip = () => {
|
|
tooltip.visible = false;
|
|
};
|
|
|
|
// 阶段分析报告模态框状态
|
|
const showAnalysisModal = ref(false);
|
|
const analysisPeriod = ref('day'); // 'day', 'camp', 'month'
|
|
const analysisReport = ref({
|
|
day: '',
|
|
camp: '',
|
|
month: ''
|
|
});
|
|
|
|
// 显示阶段分析报告模态框
|
|
const showSecondOrderAnalysisReport = () => {
|
|
showAnalysisModal.value = true;
|
|
CenterGetSecondOrderAnalysisReport(analysisPeriod.value)
|
|
};
|
|
|
|
// 关闭阶段分析报告模态框
|
|
const closeAnalysisModal = () => {
|
|
showAnalysisModal.value = false;
|
|
};
|
|
|
|
// 切换分析周期
|
|
const switchAnalysisPeriod = (period) => {
|
|
analysisPeriod.value = period;
|
|
CenterGetSecondOrderAnalysisReport(period)
|
|
};
|
|
|
|
|
|
|
|
watch(() => props.contactTimeData, () => {
|
|
renderContactTimeChart();
|
|
}, { deep: true });
|
|
|
|
// --- 生命周期钩子 ---
|
|
|
|
onMounted(() => {
|
|
renderPersonalFunnelChart();
|
|
renderContactTimeChart();
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
Object.values(chartInstances).forEach(chart => chart.destroy());
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
// --- 颜色和变量定义 ---
|
|
$slate-50: #f8fafc;
|
|
$slate-100: #f1f5f9;
|
|
$slate-200: #e2e8f0;
|
|
$slate-700: #334155;
|
|
$slate-800: #1e293b;
|
|
$slate-900: #303133;
|
|
$gray-400: #909399;
|
|
$gray-600: #606266;
|
|
$blue: #409eff;
|
|
$green: #67c23a;
|
|
$orange: #e6a23c;
|
|
$red: #f56c6c;
|
|
$white: #ffffff;
|
|
|
|
.personal-dashboard {
|
|
padding: 10px;
|
|
background-color: #f5f7fa;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
}
|
|
|
|
.dashboard-header {
|
|
background: $white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
margin-bottom: 24px;
|
|
h2 {
|
|
margin: 0;
|
|
color: $slate-900;
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
}
|
|
}
|
|
|
|
// --- 统计卡片网格 ---
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: $white;
|
|
padding: 24px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
display: flex;
|
|
align-items: center;
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
&:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
}
|
|
}
|
|
|
|
.stat-icon {
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 16px;
|
|
font-size: 24px;
|
|
color: $white;
|
|
&.customer-rate { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
|
&.response-time { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
|
|
&.timeout-rate { background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); }
|
|
&.form-rate { background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); }
|
|
&.severe-timeout-rate { background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%); }
|
|
}
|
|
|
|
.stat-content {
|
|
.stat-value { font-size: 20px; font-weight: 700; color: $slate-900; margin-bottom: 4px; }
|
|
.stat-label { font-size: 14px; color: $gray-400; font-weight: 500; }
|
|
}
|
|
|
|
// --- KPI 卡片特定样式 ---
|
|
.kpi-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
.card-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: $slate-900;
|
|
margin: -10px 0 16px;
|
|
padding-bottom: 12px;
|
|
border-bottom: 1px solid #ebeef5;
|
|
}
|
|
}
|
|
|
|
.kpi-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
// grid-template-rows: repeat(2, 1fr);
|
|
gap: 1rem;
|
|
width: 100%;
|
|
}
|
|
|
|
.kpi-item {
|
|
text-align: center;
|
|
padding: 0.75rem;
|
|
background-color: $slate-50;
|
|
border-radius: 0.5rem;
|
|
border: 1px solid $slate-200;
|
|
.kpi-value {
|
|
font-size: 1.5rem;
|
|
font-weight: bold;
|
|
color: $slate-800;
|
|
}
|
|
.kpi-unit {
|
|
font-size: 0.875rem;
|
|
font-weight: normal;
|
|
color: $gray-400;
|
|
margin-left: 2px;
|
|
}
|
|
p {
|
|
font-size: 0.875rem;
|
|
color: $gray-600;
|
|
margin: 0.25rem 0 0;
|
|
}
|
|
}
|
|
|
|
// 统计指标卡片特定样式
|
|
.stats-grid-inner {
|
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.stat-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 120px;
|
|
padding: 1rem 0.5rem;
|
|
|
|
.stat-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 18px;
|
|
color: $white;
|
|
margin-bottom: 0.75rem;
|
|
|
|
&.customer-rate { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
|
&.response-time { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
|
|
&.timeout-rate { background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); }
|
|
&.form-rate { background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); }
|
|
&.severe-timeout-rate { background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%); }
|
|
}
|
|
|
|
.kpi-value {
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- 图表区域 ---
|
|
.charts-section {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.chart-container {
|
|
background: $white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
min-height: 380px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.chart-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 20px 20px 16px;
|
|
border-bottom: 1px solid #ebeef5;
|
|
h3 { margin: 0; color: $slate-900; font-size: 18px; font-weight: 600; }
|
|
}
|
|
|
|
.chart-content {
|
|
padding-left: 20px;
|
|
padding-right: 20px;
|
|
padding-bottom: 20px;
|
|
|
|
flex-grow: 1;
|
|
position: relative;
|
|
canvas {
|
|
max-height: 280px;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.chart-select {
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
border: 1px solid $slate-200;
|
|
background-color: $slate-50;
|
|
font-size: 14px;
|
|
}
|
|
|
|
// --- 排行榜样式 ---
|
|
.problem-ranking {
|
|
max-height: 320px;
|
|
overflow-y: auto;
|
|
}
|
|
.ranking-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12px 0;
|
|
&:not(:last-child) { border-bottom: 1px solid #f0f2f5; }
|
|
}
|
|
.rank-number .rank-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
color: $white;
|
|
&.badge-gold { background: linear-gradient(135deg, #ffd700, #ffb300); }
|
|
&.badge-silver { background: linear-gradient(135deg, #c0c0c0, #a8a8a8); }
|
|
&.badge-bronze { background: linear-gradient(135deg, #cd7f32, #b8860b); }
|
|
&.badge-default { background: linear-gradient(135deg, #6c757d, #495057); }
|
|
}
|
|
.problem-info { flex: 1; margin: 0 16px; }
|
|
.problem-name { font-size: 15px; font-weight: 500; color: #212529; margin-bottom: 4px; }
|
|
.problem-count { font-size: 13px; color: #6c757d; }
|
|
.problem-percentage { min-width: 80px; text-align: right; }
|
|
.percentage { font-size: 15px; font-weight: bold; color: #495057; margin-bottom: 6px; display: block; }
|
|
.progress-bar { width: 100%; height: 6px; background: rgba(0,0,0,0.1); border-radius: 3px; overflow: hidden; }
|
|
.progress-fill { height: 100%; background: linear-gradient(90deg, #007bff, #0056b3); border-radius: 3px; }
|
|
.rank-first .progress-fill { background: linear-gradient(90deg, #ffd700, #ffb300); }
|
|
.rank-second .progress-fill { background: linear-gradient(90deg, #c0c0c0, #a8a8a8); }
|
|
.rank-third .progress-fill { background: linear-gradient(90deg, #cd7f32, #b8860b); }
|
|
|
|
|
|
|
|
// --- 响应式设计 ---
|
|
@media (max-width: 1200px) {
|
|
.charts-section { grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); }
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.personal-dashboard { padding: 15px; }
|
|
.stats-grid, .charts-section { grid-template-columns: 1fr; }
|
|
.stat-card { flex-direction: row; }
|
|
|
|
.dashboard-header {
|
|
padding: 16px;
|
|
h2 {
|
|
font-size: 20px;
|
|
}
|
|
}
|
|
|
|
.kpi-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.kpi-item {
|
|
padding: 0.5rem;
|
|
.kpi-value {
|
|
font-size: 1.25rem;
|
|
}
|
|
p {
|
|
font-size: 0.75rem;
|
|
}
|
|
}
|
|
|
|
.stats-grid-inner {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.stat-item {
|
|
min-height: 100px;
|
|
padding: 0.75rem 0.25rem;
|
|
|
|
.stat-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
font-size: 16px;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.kpi-value {
|
|
font-size: 1.125rem;
|
|
}
|
|
|
|
p {
|
|
font-size: 0.75rem;
|
|
}
|
|
}
|
|
|
|
.chart-container {
|
|
min-height: 300px;
|
|
}
|
|
|
|
.chart-header {
|
|
padding: 16px 16px 12px;
|
|
h3 {
|
|
font-size: 16px;
|
|
}
|
|
}
|
|
|
|
.chart-content {
|
|
padding-left: 16px;
|
|
padding-right: 16px;
|
|
padding-bottom: 16px;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
/* 小屏幕优化 */
|
|
@media (max-width: 480px) {
|
|
.personal-dashboard {
|
|
padding: 10px;
|
|
}
|
|
|
|
.dashboard-header {
|
|
padding: 12px;
|
|
h2 {
|
|
font-size: 18px;
|
|
}
|
|
}
|
|
|
|
.kpi-grid {
|
|
grid-template-columns: 1fr;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.kpi-item {
|
|
padding: 0.75rem;
|
|
.kpi-value {
|
|
font-size: 1.5rem;
|
|
}
|
|
}
|
|
|
|
.stats-grid-inner {
|
|
grid-template-columns: 1fr;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.stat-item {
|
|
min-height: 80px;
|
|
padding: 1rem;
|
|
flex-direction: row;
|
|
text-align: left;
|
|
|
|
.stat-icon {
|
|
margin-bottom: 0;
|
|
margin-right: 0.75rem;
|
|
}
|
|
}
|
|
|
|
.chart-container {
|
|
min-height: 250px;
|
|
}
|
|
|
|
.charts-section {
|
|
grid-template-columns: 1fr;
|
|
gap: 16px;
|
|
}
|
|
|
|
.modal-header {
|
|
padding-left: 15px;
|
|
padding-right: 15px;
|
|
}
|
|
|
|
.modal-title {
|
|
font-size: 16px;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-icon {
|
|
font-style: normal;
|
|
color: $blue;
|
|
font-size: 12px;
|
|
margin-left: 4px;
|
|
opacity: 0.7;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
|
|
&:hover {
|
|
opacity: 1;
|
|
color: #007bff;
|
|
transform: scale(1.2);
|
|
}
|
|
}
|
|
|
|
.kpi-item:hover .info-icon {
|
|
opacity: 1;
|
|
}
|
|
|
|
.kpi-item {
|
|
position: relative;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
/* 模态框样式 */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal-container {
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
width: 90%;
|
|
max-width: 600px;
|
|
max-height: 60vh;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid #ebeef5;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.modal-title {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #303133;
|
|
margin-right: auto; // 将标题推到最左边
|
|
}
|
|
|
|
.modal-close-btn {
|
|
background: none;
|
|
border: none;
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
color: #909399;
|
|
padding: 0;
|
|
width: 24px;
|
|
height: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-left: 16px; // 与按钮组保持间距
|
|
flex-shrink: 0;
|
|
|
|
|
|
&:hover {
|
|
color: #303133;
|
|
}
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 20px;
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.period-switcher {
|
|
display: flex;
|
|
flex-shrink: 0; // 防止按钮组在空间不足时被压缩
|
|
}
|
|
|
|
.period-switcher button {
|
|
padding: 6px 14px;
|
|
border: 1px solid #dcdfe6;
|
|
background: white;
|
|
border-radius: 0;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
transition: all 0.3s ease;
|
|
margin-left: -1px; // 让边框重叠,形成一体化效果
|
|
|
|
&:first-child {
|
|
border-radius: 4px 0 0 4px;
|
|
margin-left: 0;
|
|
}
|
|
|
|
&:last-child {
|
|
border-radius: 0 4px 4px 0;
|
|
}
|
|
|
|
&:hover {
|
|
border-color: #a0cfff;
|
|
color: #409eff;
|
|
z-index: 1;
|
|
position: relative;
|
|
}
|
|
|
|
&.active {
|
|
background: #409eff;
|
|
border-color: #409eff;
|
|
color: white;
|
|
z-index: 2;
|
|
position: relative;
|
|
}
|
|
}
|
|
|
|
|
|
.analysis-content h4 {
|
|
margin-top: 0;
|
|
margin-bottom: 15px;
|
|
color: #303133;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.analysis-content p {
|
|
color: #606266;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.error-message {
|
|
color: #f56c6c;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
padding: 20px;
|
|
border: 1px solid #f56c6c;
|
|
border-radius: 4px;
|
|
background-color: #fef0f0;
|
|
}
|
|
</style> |