307 lines
12 KiB
Vue
307 lines
12 KiB
Vue
<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> |