feat(导航): 添加双击导航功能并优化数据展示
- 在GroupComparison组件中添加双击部门跳转到经理页面的功能 - 在secondTop组件中添加双击成员跳转到销售页面的功能 - 优化topOne组件中客户迫切问题排行榜的数据格式转换 - 在RankingList组件中增加展示条目并添加排序功能 - 在SalesTimelineWithTaskList组件中替换alert弹窗为自定义模态框 - 优化secondTop组件路由跳转逻辑,避免重复请求
This commit is contained in:
@@ -116,12 +116,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 发言内容弹窗 -->
|
||||||
|
<div v-if="showModal" class="modal-overlay" @click="closeModal">
|
||||||
|
<div class="modal-content" @click.stop>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>直播发言内容</h3>
|
||||||
|
<button class="close-btn" @click="closeModal">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div v-if="!modalMessages || modalMessages.length === 0" class="no-messages">
|
||||||
|
暂无发言内容
|
||||||
|
</div>
|
||||||
|
<div v-else class="messages-list">
|
||||||
|
<div v-for="(message, index) in modalMessages" :key="index" class="message-item">
|
||||||
|
<span class="message-number">{{ index + 1 }}.</span>
|
||||||
|
<span class="message-content">{{ message }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
// 弹窗相关的响应式数据
|
||||||
|
const showModal = ref(false);
|
||||||
|
const modalMessages = ref([]);
|
||||||
|
|
||||||
// 定义props
|
// 定义props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
@@ -303,18 +328,13 @@ const getHealthIndicator = (score) => {
|
|||||||
|
|
||||||
// 显示发言内容弹框
|
// 显示发言内容弹框
|
||||||
const showSpeakMessages = (speakMessages) => {
|
const showSpeakMessages = (speakMessages) => {
|
||||||
if (!speakMessages || speakMessages.length === 0) {
|
modalMessages.value = speakMessages || [];
|
||||||
alert('暂无发言内容');
|
showModal.value = true;
|
||||||
return;
|
};
|
||||||
}
|
|
||||||
|
const closeModal = () => {
|
||||||
// 格式化发言内容
|
showModal.value = false;
|
||||||
let content = '直播发言内容:\n\n';
|
modalMessages.value = [];
|
||||||
speakMessages.forEach((message, index) => {
|
|
||||||
content += `${index + 1}. ${message}\n\n`;
|
|
||||||
});
|
|
||||||
|
|
||||||
alert(content);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -1031,5 +1051,105 @@ $indigo: #4f46e5;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 弹窗样式 */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #6b7280;
|
||||||
|
padding: 0;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 20px;
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-messages {
|
||||||
|
text-align: center;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-number {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #3b82f6;
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
flex: 1;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
class="ranking-card"
|
class="ranking-card"
|
||||||
:class="getRankingClass(index)"
|
:class="getRankingClass(index)"
|
||||||
@click="$emit('select-group', group)"
|
@click="$emit('select-group', group)"
|
||||||
|
@dblclick="navigateToManager(group)"
|
||||||
>
|
>
|
||||||
<div class="rank-badge">{{ index + 1 }}</div>
|
<div class="rank-badge">{{ index + 1 }}</div>
|
||||||
<div class="group-info">
|
<div class="group-info">
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
groups: {
|
groups: {
|
||||||
@@ -65,6 +67,9 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['select-group', 'manager-change'])
|
const emit = defineEmits(['select-group', 'manager-change'])
|
||||||
|
|
||||||
|
// 路由实例
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
// 选中的高级经理
|
// 选中的高级经理
|
||||||
const selectedManager = ref('all')
|
const selectedManager = ref('all')
|
||||||
|
|
||||||
@@ -267,6 +272,17 @@ const getRankingClass = (index) => {
|
|||||||
return 'rank-other'
|
return 'rank-other'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理部门双击事件,跳转到经理页面
|
||||||
|
const navigateToManager = (group) => {
|
||||||
|
router.push({
|
||||||
|
path: '/senior-manager',
|
||||||
|
query: {
|
||||||
|
user_name: group.id,
|
||||||
|
user_level: 3
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 获取趋势图标
|
// 获取趋势图标
|
||||||
const getTrendIcon = (trend) => {
|
const getTrendIcon = (trend) => {
|
||||||
const icons = {
|
const icons = {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div>
|
<div v-if="!isRouteNavigation">
|
||||||
<!-- 用户下拉菜单 -->
|
<!-- 用户下拉菜单 -->
|
||||||
<UserDropdown />
|
<UserDropdown />
|
||||||
</div>
|
</div>
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
|
|
||||||
<div class="members-grid">
|
<div class="members-grid">
|
||||||
<div v-for="member in selectedGroup.members" :key="member.id" class="member-card"
|
<div v-for="member in selectedGroup.members" :key="member.id" class="member-card"
|
||||||
:class="getStatusClass(member.status)">
|
:class="getStatusClass(member.status)" @dblclick="navigateToSale(member.name)">
|
||||||
<div class="member-header">
|
<div class="member-header">
|
||||||
<div class="member-info">
|
<div class="member-info">
|
||||||
<h3 class="member-name">{{ member.name }}</h3>
|
<h3 class="member-name">{{ member.name }}</h3>
|
||||||
@@ -250,7 +250,7 @@ const centerData = ref({
|
|||||||
stage.endDate = stageEndDate.toISOString().split('T')[0];
|
stage.endDate = stageEndDate.toISOString().split('T')[0];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
// 保存营期
|
||||||
const saveCampSettings = async () => {
|
const saveCampSettings = async () => {
|
||||||
recalculateStageDates();
|
recalculateStageDates();
|
||||||
|
|
||||||
@@ -358,9 +358,6 @@ const centerData = ref({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 组别数据
|
// 组别数据
|
||||||
const groups = ref([])
|
const groups = ref([])
|
||||||
// loading 状态
|
// loading 状态
|
||||||
@@ -692,10 +689,32 @@ const conversionRateVsAverage = ref({})
|
|||||||
}
|
}
|
||||||
return statusMap[status] || '未知'
|
return statusMap[status] || '未知'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 双击成员卡片跳转到销售页面
|
||||||
|
const navigateToSale = (userName) => {
|
||||||
|
router.push({
|
||||||
|
path: '/sale',
|
||||||
|
query: {
|
||||||
|
user_name: userName,
|
||||||
|
user_level: '1'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
await CenterCampPeriodAdmin()
|
|
||||||
|
// 判断页面进入方式:如果是通过路由跳转进入(URL中有user_name和user_level参数),则不发送CenterCampPeriodAdmin请求
|
||||||
|
// 如果是直接登录进入页面,则发送请求
|
||||||
|
const currentQuery = router.currentRoute.value.query
|
||||||
|
const isFromRoute = currentQuery.fromRoute ||
|
||||||
|
sessionStorage.getItem('fromRoute') ||
|
||||||
|
(currentQuery.user_name && currentQuery.user_level)
|
||||||
|
|
||||||
|
if (!isFromRoute) {
|
||||||
|
// 直接登录进入页面时才调用CenterCampPeriodAdmin
|
||||||
|
await CenterCampPeriodAdmin()
|
||||||
|
}
|
||||||
// CenterCampPeriodAdmin中已经调用了recalculateStageDates,这里不需要重复调用
|
// CenterCampPeriodAdmin中已经调用了recalculateStageDates,这里不需要重复调用
|
||||||
await CenterOverallCenterPerformance()
|
await CenterOverallCenterPerformance()
|
||||||
await CenterTotalGroupCount()
|
await CenterTotalGroupCount()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="ranking-list">
|
<div class="ranking-list">
|
||||||
<div v-for="(item, index) in rankingData.slice(0, 4)" :key="item.id" class="ranking-item">
|
<div v-for="(item, index) in rankingData.slice(0, 8)" :key="item.id" class="ranking-item">
|
||||||
<div class="rank-number" :class="getRankClass(index)">
|
<div class="rank-number" :class="getRankClass(index)">
|
||||||
{{ index + 1 }}
|
{{ index + 1 }}
|
||||||
</div>
|
</div>
|
||||||
@@ -62,12 +62,15 @@ const rankingData = computed(() => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return rankList.map((item, index) => ({
|
// 转换数据格式并按total_deals从大到小排序
|
||||||
id: index + 1,
|
return rankList
|
||||||
name: item.center_leader,
|
.map((item, index) => ({
|
||||||
performance: item.total_deals,
|
id: index + 1,
|
||||||
average_deals_per_member: item.average_deals_per_member
|
name: item.center_leader,
|
||||||
}));
|
performance: item.total_deals,
|
||||||
|
average_deals_per_member: item.average_deals_per_member
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.performance - a.performance); // 按total_deals从大到小排序
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取全中心业绩排行榜数据
|
// 获取全中心业绩排行榜数据
|
||||||
@@ -79,6 +82,77 @@ async function getCenterSalesRank(data) {
|
|||||||
const res = await getCenterPerformanceRank(params);
|
const res = await getCenterPerformanceRank(params);
|
||||||
console.log('获取中心业绩排行榜:', res);
|
console.log('获取中心业绩排行榜:', res);
|
||||||
centerSalesRank.value = res.data;
|
centerSalesRank.value = res.data;
|
||||||
|
/**
|
||||||
|
* 0
|
||||||
|
:
|
||||||
|
{center_leader: "潘加俊", total_deals: 0, average_deals_per_member: 0}
|
||||||
|
average_deals_per_member
|
||||||
|
:
|
||||||
|
0
|
||||||
|
center_leader
|
||||||
|
:
|
||||||
|
"潘加俊"
|
||||||
|
total_deals
|
||||||
|
:
|
||||||
|
0
|
||||||
|
1
|
||||||
|
:
|
||||||
|
{center_leader: "张三丰", total_deals: 44, average_deals_per_member: 1}
|
||||||
|
average_deals_per_member
|
||||||
|
:
|
||||||
|
1
|
||||||
|
center_leader
|
||||||
|
:
|
||||||
|
"张三丰"
|
||||||
|
total_deals
|
||||||
|
:
|
||||||
|
44
|
||||||
|
2
|
||||||
|
:
|
||||||
|
{center_leader: "朱一航", total_deals: 0, average_deals_per_member: 0}
|
||||||
|
average_deals_per_member
|
||||||
|
:
|
||||||
|
0
|
||||||
|
center_leader
|
||||||
|
:
|
||||||
|
"朱一航"
|
||||||
|
total_deals
|
||||||
|
:
|
||||||
|
0
|
||||||
|
3
|
||||||
|
:
|
||||||
|
{center_leader: "程琦", total_deals: 0, average_deals_per_member: 0}
|
||||||
|
average_deals_per_member
|
||||||
|
:
|
||||||
|
0
|
||||||
|
center_leader
|
||||||
|
:
|
||||||
|
"程琦"
|
||||||
|
total_deals
|
||||||
|
:
|
||||||
|
0
|
||||||
|
4
|
||||||
|
:
|
||||||
|
{center_leader: "王卓琳", total_deals: 6, average_deals_per_member: 0}
|
||||||
|
average_deals_per_member
|
||||||
|
:
|
||||||
|
0
|
||||||
|
center_leader
|
||||||
|
:
|
||||||
|
"王卓琳"
|
||||||
|
total_deals
|
||||||
|
:
|
||||||
|
6
|
||||||
|
5
|
||||||
|
:
|
||||||
|
{center_leader: "伍晶晶", total_deals: 5, average_deals_per_member: 0}
|
||||||
|
average_deals_per_member
|
||||||
|
:
|
||||||
|
0
|
||||||
|
center_leader
|
||||||
|
:
|
||||||
|
"伍晶晶"
|
||||||
|
*/
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取全中心业绩排行榜失败:', error);
|
console.error('获取全中心业绩排行榜失败:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -520,7 +520,6 @@ async function getCustomerTypeRatio(data) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await getCustomerTypeDistribution(params)
|
const res = await getCustomerTypeDistribution(params)
|
||||||
console.log(1222222,res)
|
|
||||||
customerTypeRatio.value = res.data
|
customerTypeRatio.value = res.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取客户类型占比失败:", error);
|
console.error("获取客户类型占比失败:", error);
|
||||||
@@ -528,11 +527,20 @@ async function getCustomerTypeRatio(data) {
|
|||||||
}
|
}
|
||||||
// 客户迫切解决的问题排行榜
|
// 客户迫切解决的问题排行榜
|
||||||
const customerUrgency = ref({});
|
const customerUrgency = ref({});
|
||||||
|
const problemRankingData = ref([]);
|
||||||
|
|
||||||
async function getCustomerUrgency() {
|
async function getCustomerUrgency() {
|
||||||
try {
|
try {
|
||||||
const res = await getUrgentNeedToAddress()
|
const res = await getUrgentNeedToAddress()
|
||||||
console.log(1222222,res)
|
|
||||||
customerUrgency.value = res.data
|
customerUrgency.value = res.data
|
||||||
|
|
||||||
|
// 将API返回的数据转换为ProblemRanking组件需要的格式
|
||||||
|
if (res.data && res.data.company_urgent_issue_ratio) {
|
||||||
|
problemRankingData.value = Object.entries(res.data.company_urgent_issue_ratio).map(([name, value]) => ({
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
}));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取客户迫切解决的问题排行榜失败:", error);
|
console.error("获取客户迫切解决的问题排行榜失败:", error);
|
||||||
}
|
}
|
||||||
@@ -542,7 +550,6 @@ const levelTree = ref({});
|
|||||||
async function CusotomGetLevelTree() {
|
async function CusotomGetLevelTree() {
|
||||||
try {
|
try {
|
||||||
const res = await getLevelTree()
|
const res = await getLevelTree()
|
||||||
console.log(1222222,res)
|
|
||||||
levelTree.value = res.data
|
levelTree.value = res.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取级别树失败:", error);
|
console.error("获取级别树失败:", error);
|
||||||
|
|||||||
Reference in New Issue
Block a user