feat: 初始化Vue3项目并添加核心功能模块
新增项目基础结构,包括Vue3、Pinia、Element Plus等核心依赖 添加路由配置和用户认证状态管理 实现销售数据看板、客户画像、团队管理等核心功能模块 集成图表库和API请求工具,完成基础样式配置
This commit is contained in:
245
my-vue-app/src/views/senorManger/components/CenterOverview.vue
Normal file
245
my-vue-app/src/views/senorManger/components/CenterOverview.vue
Normal 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>
|
||||
407
my-vue-app/src/views/senorManger/components/CustomerDetail.vue
Normal file
407
my-vue-app/src/views/senorManger/components/CustomerDetail.vue
Normal 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>
|
||||
346
my-vue-app/src/views/senorManger/components/GroupComparison.vue
Normal file
346
my-vue-app/src/views/senorManger/components/GroupComparison.vue
Normal 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>
|
||||
192
my-vue-app/src/views/senorManger/components/GroupRanking.vue
Normal file
192
my-vue-app/src/views/senorManger/components/GroupRanking.vue
Normal 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>
|
||||
200
my-vue-app/src/views/senorManger/components/ProblemRanking.vue
Normal file
200
my-vue-app/src/views/senorManger/components/ProblemRanking.vue
Normal 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>
|
||||
@@ -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>
|
||||
1644
my-vue-app/src/views/senorManger/components/manager.vue
Normal file
1644
my-vue-app/src/views/senorManger/components/manager.vue
Normal file
File diff suppressed because it is too large
Load Diff
1214
my-vue-app/src/views/senorManger/seniorManager.vue
Normal file
1214
my-vue-app/src/views/senorManger/seniorManager.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user