重构筛选器组件,将原有的独立下拉框改为基于组织架构的层级联动选择(中心领导->高级经理->经理),并添加禁用状态逻辑。当未选择上级时,下级选择器将被禁用。同时移除注释掉的代码和调试日志。
175 lines
7.7 KiB
Vue
175 lines
7.7 KiB
Vue
<template>
|
|
<div class="dashboard-card table-section">
|
|
<div class="card-header">
|
|
<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" :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('dealRate')" class="sortable">成交率 <span class="sort-icon" :class="{ active: sortField === 'dealRate' }">{{ sortOrder === 'desc' ? '↓' : '↑' }}</span></th>
|
|
<th @click="sortBy('callDuration')" class="sortable">成交单数 <span class="sort-icon" :class="{ active: sortField === 'callDuration' }">{{ sortOrder === 'desc' ? '↓' : '↑' }}</span></th>
|
|
<th>加微率</th>
|
|
<th>入群率</th>
|
|
<th>表单填写率</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="person in filteredTableData" :key="person.id" @click="$emit('update:selectedPerson', person)" :class="{ selected: selectedPerson && selectedPerson.id === person.id }">
|
|
<td>
|
|
<div class="person-info">
|
|
<div class="person-avatar">{{ person.name.charAt(0) }}</div>
|
|
<div>
|
|
<div class="person-name">{{ person.name }}</div>
|
|
<div class="person-position">{{ person.position }}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="deal-rate">
|
|
<span class="rate-value" :class="getRateClass(person.dealRate)">{{ person.dealRate }}%</span>
|
|
<div class="rate-bar"><div class="rate-fill" :style="{ width: person.dealRate + '%', backgroundColor: getRateColor(person.dealRate) }"></div></div>
|
|
</div>
|
|
</td>
|
|
<td>{{ formatDuration(person.callDuration) }}</td>
|
|
<td>{{ person.callCount }}次</td>
|
|
<td>¥{{ person.dealAmount.toLocaleString() }}</td>
|
|
<td>{{ person.department }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from 'vue';
|
|
|
|
const props = defineProps({
|
|
tableData: { type: Array, required: true },
|
|
selectedPerson: { type: Object, default: null },
|
|
levelTree: { type: Object, default: () => ({}) }
|
|
});
|
|
defineEmits(['update:selectedPerson']);
|
|
|
|
const filters = ref({ centerLeader: '', advancedManager: '', manager: '', dealStatus: '' });
|
|
const sortField = ref('dealRate');
|
|
const sortOrder = ref('desc');
|
|
|
|
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 || [] : [];
|
|
});
|
|
|
|
const filteredTableData = computed(() => {
|
|
let filtered = props.tableData;
|
|
if (filters.value.centerLeader) filtered = filtered.filter(item => item.centerLeader === filters.value.centerLeader);
|
|
if (filters.value.advancedManager) filtered = filtered.filter(item => item.advancedManager === filters.value.advancedManager);
|
|
if (filters.value.manager) filtered = filtered.filter(item => item.manager === filters.value.manager);
|
|
return filtered.sort((a, b) => {
|
|
const aValue = a[sortField.value], bValue = b[sortField.value];
|
|
return sortOrder.value === 'desc' ? bValue - aValue : aValue - bValue;
|
|
});
|
|
});
|
|
|
|
const onCenterLeaderChange = () => {
|
|
filters.value.advancedManager = '';
|
|
filters.value.manager = '';
|
|
};
|
|
|
|
const onAdvancedManagerChange = () => {
|
|
filters.value.manager = '';
|
|
};
|
|
|
|
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 formatDuration = (minutes) => {
|
|
const h = Math.floor(minutes / 60), m = minutes % 60;
|
|
return h > 0 ? `${h}h${m}m` : `${m}m`;
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.table-section { height: 600px; }
|
|
.data-table-container { height: calc(100% - 60px); overflow-y: auto; padding: 24px; }
|
|
.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; }
|
|
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> |