Files
DJKB/my-vue-app/src/views/topOne/components/DetailedDataTable.vue
lbw_9527443 5e29aa77d6 feat(视图组件): 添加指标提示功能
在secondTop.vue和DetailedDataTable.vue中添加工具提示组件,当用户悬停在指标标签上时显示计算方式的详细说明
2025-08-22 21:59:58 +08:00

307 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="dashboard-card table-section">
<div class="card-header" style="margin-left: 1rem;">
<h3>详细数据表格</h3>
</div>
<div class="data-table-container">
<!-- 筛选器 -->
<div class="table-filters">
<div class="filter-group">
<label>中心领导:</label>
<select v-model="filters.centerLeader" @change="onCenterLeaderChange">
<option value="">全部中心领导</option>
<option v-for="leader in centerLeaders" :key="leader.name" :value="leader.name">
{{ leader.name }}
</option>
</select>
</div>
<div class="filter-group">
<label>高级经理:</label>
<select v-model="filters.advancedManager" @change="onAdvancedManagerChange" :disabled="!filters.centerLeader">
<option value="">全部高级经理</option>
<option v-for="manager in availableAdvancedManagers" :key="manager.name" :value="manager.name">
{{ manager.name }}
</option>
</select>
</div>
<div class="filter-group">
<label>经理:</label>
<select v-model="filters.manager" @change="onManagerChange" :disabled="!filters.advancedManager">
<option value="">全部经理</option>
<option v-for="manager in availableManagers" :key="manager.name" :value="manager.name">
{{ manager.name }}
</option>
</select>
</div>
</div>
<!-- 数据表格 -->
<div class="data-table">
<table>
<thead>
<tr>
<th>人员</th>
<th @click="sortBy('conversion_rate')" class="sortable">成交率 <i class="info-icon" @mouseenter="showTooltip($event, 'conversionRate')" @mouseleave="hideTooltip" @click.stop></i> <span class="sort-icon" :class="{ active: sortField === 'conversion_rate' }">{{ sortOrder === 'desc' ? '↓' : '↑' }}</span></th>
<th @click="sortBy('total_deals')" class="sortable">成交单数 <i class="info-icon" @mouseenter="showTooltip($event, 'totalDeals')" @mouseleave="hideTooltip" @click.stop></i> <span class="sort-icon" :class="{ active: sortField === 'total_deals' }">{{ sortOrder === 'desc' ? '↓' : '↑' }}</span></th>
<th>加微率 <i class="info-icon" @mouseenter="showTooltip($event, 'plusVRate')" @mouseleave="hideTooltip"></i></th>
<th>入群率 <i class="info-icon" @mouseenter="showTooltip($event, 'groupRate')" @mouseleave="hideTooltip"></i></th>
<th>表单填写率 <i class="info-icon" @mouseenter="showTooltip($event, 'formFillingRate')" @mouseleave="hideTooltip"></i></th>
</tr>
</thead>
<tbody>
<tr v-for="(person, index) in displayTableData" :key="index" @click="$emit('update:selectedPerson', person)" @dblclick="handlePersonDoubleClick(person)" :class="{ selected: selectedPerson === person }">
<td>
<div class="person-info">
<div class="person-avatar">{{ (person.sale_name || person.leader_name).charAt(0) }}</div>
<div>
<div class="person-name">{{ person.sale_name || person.leader_name }}</div>
</div>
</div>
</td>
<td>
<div class="deal-rate">
<span class="rate-value" :class="getRateClass(parseFloat(person.conversion_rate))">{{ person.conversion_rate }}</span>
<div class="rate-bar"><div class="rate-fill" :style="{ width: parseFloat(person.conversion_rate) + '%', backgroundColor: getRateColor(parseFloat(person.conversion_rate)) }"></div></div>
</div>
</td>
<td>{{ person.total_deals }}</td>
<td>{{ person.plus_v_rate }}</td>
<td>{{ person.group_rate }}</td>
<td>{{ person.form_filling_rate }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Tooltip 组件 -->
<Tooltip
:visible="tooltip.visible"
:x="tooltip.x"
:y="tooltip.y"
:title="tooltip.title"
:description="tooltip.description"
/>
</div>
</template>
<script setup>
import { ref, computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import Tooltip from '@/components/Tooltip.vue';
const props = defineProps({
tableData: { type: Array, required: true },
selectedPerson: { type: Object, default: null },
levelTree: { type: Object, default: () => ({}) }
});
const emit = defineEmits(['update:selectedPerson', 'filter-change']);
const router = useRouter();
const filters = ref({ centerLeader: '', advancedManager: '', manager: '', dealStatus: '' });
const sortField = ref('conversion_rate');
const sortOrder = ref('desc');
// Tooltip 状态管理
const tooltip = reactive({
visible: false,
x: 0,
y: 0,
title: '',
description: ''
});
// 指标计算方式描述
const metricDescriptions = {
conversionRate: {
title: '成交率计算方式',
description: '成交单数 ÷ 总线索数 × 100%,反映销售人员将潜在客户转化为实际成交的能力。'
},
totalDeals: {
title: '成交单数计算方式',
description: '统计销售人员在选定时间范围内成功签约的订单总数,包括所有已确认的成交订单。'
},
plusVRate: {
title: '加微率计算方式',
description: '成功添加微信的客户数 ÷ 总接触客户数 × 100%,反映客户对销售人员的初步信任度。'
},
groupRate: {
title: '入群率计算方式',
description: '成功邀请进入微信群的客户数 ÷ 已添加微信的客户数 × 100%,反映客户的参与积极性。'
},
formFillingRate: {
title: '表单填写率计算方式',
description: '完成表单填写的客户数 ÷ 总邀请填写表单的客户数 × 100%,反映客户的配合度和意向强度。'
}
};
// 显示工具提示
const showTooltip = (event, metricType) => {
const metric = metricDescriptions[metricType];
if (metric) {
tooltip.title = metric.title;
tooltip.description = metric.description;
tooltip.x = event.clientX;
tooltip.y = event.clientY;
tooltip.visible = true;
}
};
// 隐藏工具提示
const hideTooltip = () => {
tooltip.visible = false;
};
const centerLeaders = computed(() => {
return props.levelTree?.level_tree?.center_leaders || [];
});
const availableAdvancedManagers = computed(() => {
if (!filters.value.centerLeader) return [];
const leader = centerLeaders.value.find(l => l.name === filters.value.centerLeader);
return leader ? leader.advanced_managers || [] : [];
});
const availableManagers = computed(() => {
if (!filters.value.advancedManager) return [];
const manager = availableAdvancedManagers.value.find(m => m.name === filters.value.advancedManager);
return manager ? manager.managers || [] : [];
});
// 显示表格数据直接使用API返回的数据
const displayTableData = computed(() => {
if (!props.tableData || !Array.isArray(props.tableData)) return [];
return props.tableData.sort((a, b) => {
let aValue = a[sortField.value];
let bValue = b[sortField.value];
// 处理百分比字符串
if (typeof aValue === 'string' && aValue.includes('%')) {
aValue = parseFloat(aValue.replace('%', ''));
}
if (typeof bValue === 'string' && bValue.includes('%')) {
bValue = parseFloat(bValue.replace('%', ''));
}
return sortOrder.value === 'desc' ? bValue - aValue : aValue - bValue;
});
});
const onCenterLeaderChange = () => {
filters.value.advancedManager = '';
filters.value.manager = '';
emitFilterChange();
};
const onAdvancedManagerChange = () => {
filters.value.manager = '';
emitFilterChange();
};
const onManagerChange = () => {
emitFilterChange();
};
const emitFilterChange = () => {
const filterParams = {};
if (filters.value.centerLeader) {
filterParams.center_leader = filters.value.centerLeader;
}
if (filters.value.advancedManager) {
filterParams.team_leader = filters.value.advancedManager;
}
if (filters.value.manager) {
filterParams.group_leader = filters.value.manager;
}
emit('filter-change', filterParams);
};
const sortBy = (field) => {
if (sortField.value === field) sortOrder.value = sortOrder.value === 'desc' ? 'asc' : 'desc';
else { sortField.value = field; sortOrder.value = 'desc'; }
};
const getRateClass = (rate) => {
if (rate >= 80) return 'high'; if (rate >= 60) return 'medium'; return 'low';
};
const getRateColor = (rate) => {
if (rate >= 80) return '#4CAF50'; if (rate >= 60) return '#FF9800'; return '#f44336';
};
// 处理人员双击事件
const handlePersonDoubleClick = (person) => {
let userLevel = 4; // 默认级别
const userName = person.sale_name || person.leader_name;
// 根据筛选器选择状态确定user_level
if (filters.value.manager) {
userLevel = 1; // 选中经理
} else if (filters.value.advancedManager) {
userLevel = 2; // 选中高级经理
} else if (filters.value.centerLeader) {
userLevel = 3; // 选中中心领导
}
// 如果什么都没选userLevel保持为4
// 根据user_level确定跳转路径
let targetPath = '';
switch (userLevel) {
case 4:
targetPath = '/second-top';
break;
case 3:
targetPath = '/senior-manager';
break;
case 2:
targetPath = '/manager';
break;
case 1:
targetPath = '/sale';
break;
default:
targetPath = '/second-top';
}
// 路由跳转
router.push({
path: targetPath,
query: {
user_name: userName,
user_level: userLevel
}
});
};
</script>
<style scoped>
.table-section { height: 600px; }
.data-table-container { height: calc(100% - 60px); overflow-y: auto; padding: 0.5rem; }
.table-filters { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; padding: 16px; background: #f8fafc; border-radius: 8px; }
.filter-group { display: flex; flex-direction: column; gap: 4px; }
.filter-group label { font-size: 12px; font-weight: 600; color: #4a5568; }
.filter-group select { padding: 8px 12px; border: 1px solid #e2e8f0; border-radius: 6px; }
.filter-group select:disabled { background-color: #f7fafc; color: #a0aec0; cursor: not-allowed; }
.data-table { overflow-x: auto; border-radius: 8px; border: 1px solid #e2e8f0; }
table { width: 100%; border-collapse: collapse; }
th { background: #f7fafc; padding: 12px 16px; text-align: left; font-weight: 600; color: #4a5568; border-bottom: 1px solid #e2e8f0; font-size: 12px; }
th.sortable { cursor: pointer; }
.sort-icon { margin-left: 4px; opacity: 0.5; }
.sort-icon.active { opacity: 1; color: #4299e1; }
.info-icon { margin-left: 4px; color: #666; cursor: pointer; font-style: normal; font-size: 12px; transition: color 0.2s; }
.info-icon:hover { color: #409eff; }
td { padding: 16px; border-bottom: 1px solid #f1f5f9; vertical-align: middle; }
tr { cursor: pointer; transition: background-color 0.2s ease; }
tr:hover { background-color: #f8fafc; }
tr.selected { background-color: #e0f2fe; border-left: 4px solid #0ea5e9; }
.person-info { display: flex; align-items: center; gap: 12px; }
.person-avatar { width: 40px; height: 40px; border-radius: 50%; background: #4299e1; color: white; display: flex; align-items: center; justify-content: center; font-weight: 600; }
.person-name { font-weight: 600; }
.person-position { font-size: 12px; color: #718096; }
.deal-rate { min-width: 80px; }
.rate-value { font-weight: 600; }
.rate-value.high { color: #4CAF50; }
.rate-value.medium { color: #FF9800; }
.rate-value.low { color: #f44336; }
.rate-bar { height: 4px; background: #edf2f7; border-radius: 2px; overflow: hidden; }
.rate-fill { height: 100%; }
</style>