feat: 初始化Vue3项目并添加核心功能模块

新增项目基础结构,包括Vue3、Pinia、Element Plus等核心依赖
添加路由配置和用户认证状态管理
实现销售数据看板、客户画像、团队管理等核心功能模块
集成图表库和API请求工具,完成基础样式配置
This commit is contained in:
2025-08-12 14:34:44 +08:00
commit f93236ab36
71 changed files with 32821 additions and 0 deletions

View File

@@ -0,0 +1,245 @@
<template>
<div class="center-overview">
<h2>整体概览</h2>
<div class="overview-grid">
<div class="overview-card primary">
<div class="card-header">
<span class="card-title">中心总业绩</span>
<span class="card-trend positive">+12% vs 昨日</span>
</div>
<div class="card-value">552,000 </div>
<div class="card-subtitle">月目标完成率: 56%</div>
</div>
<div class="overview-card">
<div class="card-header">
<span class="card-title">活跃组数</span>
<span class="card-trend stable">5/5 </span>
</div>
<div class="card-value">5 </div>
<div class="card-subtitle">总人数: 40</div>
</div>
<div class="overview-card">
<div class="card-header">
<span class="card-title">中心转化率</span>
<span class="card-trend positive">+0.3% vs 上期</span>
</div>
<div class="card-value">5.2%</div>
<div class="card-subtitle">行业平均: 4.8%</div>
</div>
<div class="overview-card">
<div class="card-header">
<span class="card-title">总通话次数</span>
<span class="card-trend positive">+8% vs 上期</span>
</div>
<div class="card-value">1,247 </div>
<div class="card-subtitle">有效通话: 892</div>
</div>
<div class="overview-card">
<div class="card-header">
<span class="card-title">新增客户</span>
<span class="card-trend positive">+15% vs 上期</span>
</div>
<div class="card-value">117 </div>
<div class="card-subtitle">意向客户: 89</div>
</div>
<div class="overview-card">
<div class="card-header">
<span class="card-title">定金转化</span>
<span class="card-trend positive">+18% vs 上期</span>
</div>
<div class="card-value">40 </div>
<div class="card-subtitle">本月定金转化率: 10%</div>
</div>
</div>
</div>
</template>
<script setup>
// 中心整体概览组件
</script>
<style lang="scss" scoped>
.center-overview {
background: white;
border-radius: 12px;
padding: 0.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
h2 {
font-size: 1.3rem;
font-weight: 600;
color: #1e293b;
margin: 0 0 1.5rem 0;
}
.overview-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 0;
}
.overview-card {
padding: 0.5rem;
border: 1px solid #e2e8f0;
border-radius: 10px;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
&.primary {
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
color: white;
border: none;
.card-title, .card-subtitle {
color: rgba(255, 255, 255, 0.9);
}
.card-trend {
color: rgba(255, 255, 255, 0.8);
}
.card-value {
color: white;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.card-title {
color: #64748b;
font-size: 0.9rem;
font-weight: 500;
}
.card-trend {
font-size: 0.75rem;
font-weight: 600;
&.positive {
color: #059669;
}
&.negative {
color: #dc2626;
}
&.stable {
color: #7c3aed;
}
}
.card-value {
font-size: 1.8rem;
font-weight: bold;
color: #1e293b;
margin-bottom: 0.25rem;
}
.card-subtitle {
color: #94a3b8;
font-size: 0.8rem;
}
}
.trend-section {
h3 {
font-size: 1.1rem;
font-weight: 600;
color: #1e293b;
margin: 0 0 1rem 0;
}
.trend-charts {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.trend-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem;
background: #f8fafc;
border-radius: 8px;
.trend-label {
font-size: 0.85rem;
color: #64748b;
min-width: 80px;
}
.trend-bar {
flex: 1;
height: 6px;
background: #e2e8f0;
border-radius: 3px;
overflow: hidden;
.trend-fill {
height: 100%;
background: linear-gradient(90deg, #10b981, #059669);
border-radius: 3px;
transition: width 0.3s ease;
}
}
.trend-value {
font-size: 0.8rem;
font-weight: 600;
color: #059669;
min-width: 40px;
text-align: right;
}
}
}
}
// 移动端适配
@media (max-width: 768px) {
.center-overview {
padding: 1rem;
.overview-grid {
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.overview-card {
padding: 1rem;
.card-value {
font-size: 1.5rem;
}
}
.trend-charts {
grid-template-columns: 1fr;
}
}
}
@media (max-width: 480px) {
.center-overview {
.overview-grid {
grid-template-columns: 1fr;
}
}
}
</style>

View File

@@ -0,0 +1,407 @@
<template>
<div style="width: 47vw;">
<h2 class="section-title">客户详情</h2>
<div id="context-panel" ref="contextPanelRef" class="section-card">
<div v-if="selectedContact" class="context-panel-content" style="min-height: 570px;">
<div class="panel-header">
<h3>{{ selectedContact.name }}</h3>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue';
import Chart from 'chart.js/auto';
// 定义props
const props = defineProps({
selectedContact: {
type: Object,
default: null
}
});
const contextPanelRef = ref(null);
const sentimentChartCanvas = ref(null);
const chartInstances = {};
// CHARTING
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);
}
};
const renderSentimentChart = (history) => {
if (!sentimentChartCanvas.value) return;
const ctx = sentimentChartCanvas.value.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 0, 120);
gradient.addColorStop(0, 'rgba(59, 130, 246, 0.3)');
gradient.addColorStop(1, 'rgba(59, 130, 246, 0.05)');
const config = {
type: 'line',
data: {
labels: history.map((_, i) => `${i+1}`),
datasets: [{
label: '情绪值',
data: history,
borderColor: '#3b82f6',
borderWidth: 3,
tension: 0.4,
fill: true,
backgroundColor: gradient,
pointRadius: 4,
pointBackgroundColor: '#3b82f6',
pointBorderColor: '#ffffff',
pointBorderWidth: 2,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
enabled: true,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: '#ffffff',
bodyColor: '#ffffff',
borderColor: '#3b82f6',
borderWidth: 1,
callbacks: {
label: function(context) {
return `情绪值: ${context.parsed.y}`;
}
}
}
},
scales: {
y: {
display: true,
min: 0,
max: 100,
grid: {
color: 'rgba(148, 163, 184, 0.2)',
drawBorder: false
},
ticks: {
color: '#64748b',
font: {
size: 11
},
stepSize: 25
}
},
x: {
display: true,
grid: {
display: false
},
ticks: {
color: '#64748b',
font: {
size: 11
}
}
}
}
}
};
createOrUpdateChart('sentiment', sentimentChartCanvas, config);
};
// WATCHERS
watch(() => props.selectedContact, (newContact) => {
if (newContact && newContact.sentimentHistory && newContact.sentimentHistory.length > 0) {
nextTick(() => {
renderSentimentChart(newContact.sentimentHistory);
});
}
}, { immediate: true });
</script>
<style lang="scss" scoped>
// Color Palette
$slate-50: #f8fafc;
$slate-100: #f1f5f9;
$slate-200: #e2e8f0;
$slate-400: #94a3b8;
$slate-500: #64748b;
$slate-600: #475569;
$slate-700: #334155;
$slate-800: #1e293b;
$white: #ffffff;
$blue: #3b82f6;
$green: #22c55e;
$amber: #f59e0b;
$red: #ef4444;
$indigo: #4f46e5;
$purple: #a855f7;
h2.section-title {
font-size: 1.25rem;
font-weight: bold;
color: $slate-700;
}
h4 {
font-weight: 600;
color: $slate-700;
margin-bottom: 0.5rem;
}
// Context Panel
.section-card {
background-color: $white;
border-radius: 0.75rem;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
padding-top: 0rem;
padding-left: 1rem;
padding-right: 1rem;
padding-bottom: 1rem;
margin-top: 12px;
}
.context-panel-content {
.panel-header {
border-bottom: 1px solid $slate-200;
padding-bottom: 1rem;
h3 { margin-bottom: 0; font-size: 1.25rem; }
}
.detail-blocks-container {
display: flex;
gap: 1rem;
// 移动端适配
@media (max-width: 768px) {
flex-direction: column;
gap: 1.5rem;
}
}
.detail-column {
display: flex;
flex-direction: column;
gap: 1rem;
&:first-child {
flex: 1; // 左列占1份
}
&:last-child {
flex: 2; // 右列占2份
}
// 移动端适配
@media (max-width: 768px) {
&:first-child,
&:last-child {
flex: 1;
}
}
}
.detail-block {
min-width: 0; // 防止flex项目溢出
}
.form-details {
background-color: $slate-50;
padding: 0.75rem;
border-radius: 0.5rem;
font-size: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
// 移动端适配
@media (max-width: 768px) {
padding: 1rem;
font-size: 0.95rem;
}
> div {
display: flex;
gap: 0.5rem;
&.concerns {
align-items: flex-start;
}
span:last-child {
font-weight: 500;
text-align: right;
}
}
}
.communication-insights {
background-color: $slate-50;
padding: 0.75rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
.insight-item {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
h5 {
font-size: 0.875rem;
font-weight: 600;
color: $slate-700;
margin-bottom: 0.5rem;
}
}
.suggestion-tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.suggestion-tag {
font-size: 0.75rem;
font-weight: 500;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
&.positive {
background-color: #dcfce7;
color: #166534;
}
}
.ratio-chart {
.ratio-bar {
display: flex;
height: 1.5rem;
border-radius: 0.375rem;
overflow: hidden;
margin-bottom: 0.5rem;
.speak-portion {
background-color: #3b82f6;
transition: width 0.3s ease;
}
.listen-portion {
background-color: #22c55e;
transition: width 0.3s ease;
}
}
.ratio-labels {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
.speak-label {
color: #3b82f6;
font-weight: 500;
}
.listen-label {
color: #22c55e;
font-weight: 500;
}
}
}
}
.sentiment-summary {
background-color: $slate-50;
padding: 0.75rem;
border-radius: 0.5rem;
p { font-size: 0.875rem; font-weight: 500; span { font-weight: 400; } }
.sentiment-chart-block {
margin-top: 0.75rem;
padding: 0.75rem;
background-color: $white;
border-radius: 0.5rem;
border: 1px solid $slate-200;
p {
margin-bottom: 0.5rem;
font-weight: 500;
color: $slate-700;
}
}
}
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
color: $slate-500;
}
// Timeline
.timeline {
border-left: 2px solid $slate-200;
.timeline-item {
position: relative;
padding-left: 1rem;
padding-bottom: 1rem;
&:last-child {
padding-bottom: 0;
}
&::before {
content: '';
position: absolute;
left: -6px;
top: 5px;
width: 10px;
height: 10px;
border-radius: 9999px;
background-color: $slate-400;
border: 2px solid $white;
}
&.call::before { background-color: $blue; }
&.email::before { background-color: $green; }
&.meeting::before { background-color: $purple; }
&.system::before { background-color: $slate-500; }
}
.timeline-date { font-size: 0.75rem; color: $slate-400; }
.timeline-summary { font-size: 0.875rem; color: $slate-600; }
.no-interactions { padding-left: 1rem; font-size: 0.875rem; color: $slate-400; }
}
// Tags
.tags-container {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag {
font-size: 0.75rem;
font-weight: 600;
padding: 0.125rem 0.625rem;
border-radius: 9999px;
&.concern-tag { background-color: $slate-200; color: $slate-700; }
}
// Chart Containers
.chart-container {
position: relative;
width: 100%;
height: 180px;
&.sentiment-chart {
height: 120px;
margin-top: 0.5rem;
}
}
</style>

View File

@@ -0,0 +1,346 @@
<template>
<div class="group-comparison">
<!-- 综合排名 -->
<div class="ranking-section">
<h3>综合表现排名</h3>
<div class="ranking-grid compact">
<div
v-for="(group, index) in sortedGroups"
:key="group.id"
class="ranking-card"
:class="getRankingClass(index)"
@click="$emit('select-group', group)"
>
<div class="rank-badge">{{ index + 1 }}</div>
<div class="group-info">
<div class="group-name">{{ group.name }}</div>
<div class="group-leader">{{ group.leader }}</div>
</div>
<div class="performance-score">
<div class="score">{{ calculateScore(group) }}</div>
<div class="score-label">综合分</div>
</div>
<div class="key-metrics">
<div class="mini-metric">
<span class="mini-label">业绩</span>
<span class="mini-value">{{ formatCurrency(group.todayPerformance) }}</span>
</div>
<div class="mini-metric">
<span class="mini-label">转化</span>
<span class="mini-value">{{ group.conversionRate }}%</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
groups: {
type: Array,
required: true
}
})
const emit = defineEmits(['select-group'])
// 按综合表现排序的组别
const sortedGroups = computed(() => {
return [...props.groups].sort((a, b) => calculateScore(b) - calculateScore(a))
})
// 计算综合分数
const calculateScore = (group) => {
const performanceScore = (group.todayPerformance / 200000) * 30
const conversionScore = (group.conversionRate / 10) * 25
const clientScore = (group.newClients / 50) * 25
const dealScore = (group.deals / 20) * 20
return Math.round(performanceScore + conversionScore + clientScore + dealScore)
}
// 格式化指标值
const formatMetricValue = (value, unit) => {
switch (unit) {
case 'currency':
return formatCurrency(value)
case 'percent':
return value + '%'
case 'number':
return value.toString()
default:
return value
}
}
// 格式化货币
const formatCurrency = (value) => {
if (value >= 10000) {
return (value / 10000).toFixed(1) + '万'
}
return value.toLocaleString()
}
// 获取进度条宽度
const getProgressWidth = (value, metricKey) => {
const maxValues = {
todayPerformance: 200000,
conversionRate: 10,
newClients: 50,
deals: 20
}
return Math.min((value / maxValues[metricKey]) * 100, 100)
}
// 获取表现等级样式
const getPerformanceClass = (value, metricKey) => {
const thresholds = {
todayPerformance: { excellent: 120000, good: 80000 },
conversionRate: { excellent: 6, good: 4 },
newClients: { excellent: 25, good: 15 },
deals: { excellent: 8, good: 5 }
}
const threshold = thresholds[metricKey]
if (value >= threshold.excellent) return 'excellent'
if (value >= threshold.good) return 'good'
return 'poor'
}
// 获取排名样式
const getRankingClass = (index) => {
if (index === 0) return 'rank-1'
if (index === 1) return 'rank-2'
if (index === 2) return 'rank-3'
return 'rank-other'
}
// 获取趋势图标
const getTrendIcon = (trend) => {
const icons = {
up: '↗',
down: '↘',
stable: '→'
}
return icons[trend] || '→'
}
// 获取预警图标
const getAlertIcon = (level) => {
const icons = {
warning: '⚠️',
info: '',
urgent: '🚨'
}
return icons[level] || ''
}
</script>
<style lang="scss" scoped>
.group-comparison {
background: white;
border-radius: 12px;
padding: 1rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
height: 24rem;
overflow: auto;
// 综合排名
.ranking-section {
// margin-bottom: 2rem;
h3 {
font-size: 1.1rem;
font-weight: 600;
color: #374151;
margin: 0 0 1rem 0;
}
.ranking-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
&.compact {
grid-template-columns: repeat(1, 1fr);
gap: 0.75rem;
.ranking-card {
padding: 0.75rem;
gap: 0.5rem;
.rank-badge {
width: 24px;
height: 24px;
font-size: 0.8rem;
}
.group-info {
.group-name {
font-size: 0.9rem;
}
.group-leader {
font-size: 0.75rem;
}
}
.performance-score {
.score {
font-size: 1.1rem;
}
.score-label {
font-size: 0.7rem;
}
}
.key-metrics {
.mini-metric {
.mini-label {
font-size: 0.7rem;
}
.mini-value {
font-size: 0.8rem;
}
}
}
}
}
}
.ranking-card {
display: flex;
align-items: center;
padding: 1rem;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
&.rank-1 {
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
border: 2px solid #f59e0b;
}
&.rank-2 {
background: linear-gradient(135deg, #c0c0c0 0%, #e5e7eb 100%);
border: 2px solid #9ca3af;
}
&.rank-3 {
background: linear-gradient(135deg, #cd7f32 0%, #d97706 100%);
border: 2px solid #b45309;
}
&.rank-other {
background: white;
border: 1px solid #e5e7eb;
}
.rank-badge {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.2rem;
margin-right: 1rem;
background: rgba(255, 255, 255, 0.9);
color: #1f2937;
}
.group-info {
flex: 1;
.group-name {
font-weight: 600;
color: #1f2937;
margin-bottom: 0.25rem;
}
.group-leader {
font-size: 0.85rem;
color: #6b7280;
}
}
.performance-score {
text-align: center;
margin: 0 1rem;
.score {
font-size: 1.5rem;
font-weight: bold;
color: #1f2937;
}
.score-label {
font-size: 0.75rem;
color: #6b7280;
}
}
.key-metrics {
display: flex;
flex-direction: column;
gap: 0.25rem;
.mini-metric {
display: flex;
justify-content: space-between;
gap: 0.5rem;
.mini-label {
font-size: 0.75rem;
color: #6b7280;
}
.mini-value {
font-size: 0.75rem;
font-weight: 600;
color: #1f2937;
}
}
}
}
}
}
// 移动端适配
@media (max-width: 768px) {
.group-comparison {
padding: 0.75rem;
.ranking-grid {
grid-template-columns: 1fr;
&.compact {
grid-template-columns: 1fr;
}
}
.ranking-card {
.key-metrics {
display: none;
}
}
}
}
</style>

View File

@@ -0,0 +1,192 @@
<template>
<div class="group-ranking">
<!-- 销售漏斗 -->
<div class="chart-container">
<div class="chart-header">
<h3>销售漏斗</h3>
</div>
<div class="chart-content">
<canvas ref="personalFunnelChartCanvas"></canvas>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
import Chart from 'chart.js/auto'
const props = defineProps({
selectedGroup: {
type: Object,
default: null
}
})
// Chart.js 实例
const chartInstances = {}
// DOM 元素引用
const personalFunnelChartCanvas = ref(null)
// Chart.js 数据
const funnelData = reactive({
labels: ['线索', '加微', '到课', '定金', '成交'],
data: [120, 90, 45, 18, 10],
})
// 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.labels,
datasets: [{
label: '数量', data: funnelData.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)'],
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)
}
// 生命周期钩子
onMounted(() => {
renderPersonalFunnelChart()
})
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;
.group-ranking {
background: $white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
height: 23rem;
display: flex;
flex-direction: column;
}
.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;
flex: 1;
}
.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;
}
}
// 响应式设计
@media (max-width: 768px) {
.group-ranking {
padding: 1rem;
height: auto;
min-height: 20rem;
}
.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) {
.group-ranking {
padding: 0.75rem;
height: auto;
min-height: 18rem;
}
.chart-container {
min-height: 250px;
}
.chart-header {
padding: 12px;
h3 {
font-size: 14px;
}
}
.chart-content {
padding: 12px;
}
}
</style>

View File

@@ -0,0 +1,200 @@
<template>
<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>
</template>
<script setup>
import { reactive, computed } from 'vue';
// 问题排行榜数据
const problemData = reactive([
{ value: 180, name: '学习成绩提升' },
{ value: 150, name: '学习习惯培养' },
{ value: 120, name: '兴趣爱好发展' },
{ value: 100, name: '心理健康问题' },
{ value: 80, name: '升学规划' },
{ value: 70, name: '亲子关系改善' }
]);
// 计算属性
const sortedProblemData = computed(() => {
return [...problemData].sort((a, b) => b.value - a.value);
});
const totalProblemCount = computed(() => {
return problemData.reduce((sum, item) => sum + item.value, 0);
});
// 排行榜相关方法
const getPercentage = (value) => ((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
});
</script>
<style lang="scss" scoped>
// 颜色变量
$slate-200: #e2e8f0;
$slate-900: #0f172a;
$white: #ffffff;
.chart-container {
background: $white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
height: 26rem !important;
max-height: 26rem;
// flex: 1;
}
.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;
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);
}
</style>

View File

@@ -0,0 +1,120 @@
<template>
<div class="stat-card kpi-card">
<div class="kpi-grid stats-grid-inner">
<div class="kpi-item stat-item">
<div class="stat-icon customer-rate">
<i class="el-icon-chat-dot-round"></i>
</div>
<div class="kpi-value">{{ customerCommunicationRate }}%</div>
<p>活跃客户沟通率</p>
</div>
<div class="kpi-item stat-item">
<div class="stat-icon response-time">
<i class="el-icon-timer"></i>
</div>
<div class="kpi-value">{{ averageResponseTime }}<span class="kpi-unit">分钟</span></div>
<p>平均应答时间</p>
</div>
<div class="kpi-item stat-item">
<div class="stat-icon timeout-rate">
<i class="el-icon-warning"></i>
</div>
<div class="kpi-value">{{ timeoutResponseRate }}%</div>
<p>超时应答率</p>
</div>
<div class="kpi-item stat-item">
<div class="stat-icon severe-timeout-rate">
<i class="el-icon-warning-outline"></i>
</div>
<div class="kpi-value">{{ severeTimeoutRate }}%</div>
<p>严重超时应答率</p>
</div>
<div class="kpi-item stat-item">
<div class="stat-icon form-rate">
<i class="el-icon-document"></i>
</div>
<div class="kpi-value">{{ formCompletionRate }}%</div>
<p>表格填写率</p>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
customerCommunicationRate: {
type: Number,
default: 0
},
averageResponseTime: {
type: Number,
default: 0
},
timeoutResponseRate: {
type: Number,
default: 0
},
severeTimeoutRate: {
type: Number,
default: 0
},
formCompletionRate: {
type: Number,
default: 0
}
});
</script>
<style scoped>
.stat-card {
background-color: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.card-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
}
.kpi-item {
text-align: center;
}
.stat-icon {
font-size: 24px;
margin-bottom: 10px;
}
.kpi-value {
font-size: 24px;
font-weight: bold;
}
.kpi-unit {
font-size: 14px;
margin-left: 4px;
}
p {
font-size: 14px;
color: #666;
margin-top: 5px;
}
.customer-rate { color: #409EFF; }
.response-time { color: #67C23A; }
.timeout-rate { color: #E6A23C; }
.severe-timeout-rate { color: #F56C6C; }
.form-rate { color: #909399; }
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff