feat(销售驾驶舱): 优化销售时间线标题样式和布局调整
refactor(客户类型): 重构客户类型图表组件,增加数据校验和错误处理 fix(中心概览): 修正中心总业绩单位显示错误 style(路由导航): 添加路由导航时的面包屑和用户信息显示 chore: 移动客户类型组件至正确目录并更新引用路径
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sales-timeline-with-task-list">
|
<div class="sales-timeline-with-task-list">
|
||||||
|
<div class="timeline-title-section" style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<h2 style="font-size: 18px;margin: 0;">销售时间线</h2>
|
||||||
|
<span style="font-size: 14px;">客户转化全流程跟踪</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="sales-timeline">
|
<div class="sales-timeline">
|
||||||
<div class="timeline-container">
|
<div class="timeline-container">
|
||||||
<div class="timeline-line"></div>
|
<div class="timeline-line"></div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sales-dashboard">
|
<div class="sales-dashboard">
|
||||||
<!-- 页面加载状态 -->
|
<!-- 页面加载状态 -->
|
||||||
<Loading :visible="isPageLoading" text="正在加载数据..." />
|
<!-- <Loading :visible="isPageLoading" text="正在加载数据..." /> -->
|
||||||
<!-- 顶部导航栏 -->
|
<!-- 顶部导航栏 -->
|
||||||
<!-- 数据分析区域 - 独立占据整行 -->
|
<!-- 销售时间线区域 -->
|
||||||
<section class="analytics-section-full">
|
<section class="timeline-section">
|
||||||
<!-- 动态顶栏:根据是否有路由参数显示不同内容 -->
|
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
|
<!-- 动态顶栏:根据是否有路由参数显示不同内容 -->
|
||||||
<!-- 路由跳转时的顶栏:面包屑 + 姓名 -->
|
<!-- 路由跳转时的顶栏:面包屑 + 姓名 -->
|
||||||
<div v-if="isRouteNavigation" class="route-header">
|
<div v-if="isRouteNavigation" class="route-header">
|
||||||
<div class="breadcrumb">
|
<div class="breadcrumb">
|
||||||
@@ -21,31 +21,16 @@
|
|||||||
|
|
||||||
<!-- 自己登录时的顶栏:原有样式 -->
|
<!-- 自己登录时的顶栏:原有样式 -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
|
||||||
<h1 class="app-title">销售驾驶舱</h1>
|
<h1 class="app-title">销售驾驶舱</h1>
|
||||||
<div
|
<div
|
||||||
class="quick-stats"
|
class="quick-stats"
|
||||||
style="display: flex; align-items: center; gap: 30px"
|
style="display: flex; align-items: center; gap: 30px"
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
v-for="(stat, key) in MOCK_DATA.performance"
|
|
||||||
:key="key"
|
|
||||||
class="quick-stat-item"
|
|
||||||
>
|
|
||||||
<div class="stat-label">
|
|
||||||
{{ stat.label + ":" + stat.value.toLocaleString() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<UserDropdown />
|
<UserDropdown />
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</section>
|
|
||||||
<!-- 销售时间线区域 -->
|
|
||||||
<section class="timeline-section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>销售时间线</h2>
|
|
||||||
<p class="section-subtitle">客户转化全流程跟踪</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
<!-- 销售时间线加载状态 -->
|
<!-- 销售时间线加载状态 -->
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<span class="card-title">中心总业绩</span>
|
<span class="card-title">中心总业绩</span>
|
||||||
<span class="card-trend positive">{{ props.overallData.CenterPerformance?.center_monthly_vs_previous_deals }} vs 上期</span>
|
<span class="card-trend positive">{{ props.overallData.CenterPerformance?.center_monthly_vs_previous_deals }} vs 上期</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-value">{{ props.overallData.CenterPerformance.center_monthly_deal_count || '552,000' }} 元</div>
|
<div class="card-value">{{ props.overallData.CenterPerformance.center_monthly_deal_count || '552,000' }} 单</div>
|
||||||
<div class="card-subtitle">月目标完成率: {{ props.overallData.CenterPerformance?.center_monthly_target_completion_rate || '56%' }}</div>
|
<div class="card-subtitle">月目标完成率: {{ props.overallData.CenterPerformance?.center_monthly_target_completion_rate || '56%' }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, onBeforeUnmount, computed, watch } from 'vue';
|
import { ref, reactive, onMounted, onBeforeUnmount, computed, watch, nextTick } from 'vue';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
// 定义props
|
// 定义props
|
||||||
@@ -40,9 +40,11 @@ const chartData = computed(() => {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
let processedData = props.customerData.customer_type_distribution.map(item => ({
|
let processedData = props.customerData.customer_type_distribution
|
||||||
|
.filter(item => item && item.category) // 过滤掉无效数据
|
||||||
|
.map(item => ({
|
||||||
name: item.category,
|
name: item.category,
|
||||||
value: parseFloat(item.ratio.replace('%', '')) || 0
|
value: parseFloat((item.ratio || '0').replace('%', '')) || 0
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// 如果是地域类型,需要按省份进行数据整理
|
// 如果是地域类型,需要按省份进行数据整理
|
||||||
@@ -53,6 +55,11 @@ const chartData = computed(() => {
|
|||||||
// 提取省份名称,处理"山东省 临沂市 莒南县"这种空格分隔的格式
|
// 提取省份名称,处理"山东省 临沂市 莒南县"这种空格分隔的格式
|
||||||
let provinceName = item.name
|
let provinceName = item.name
|
||||||
|
|
||||||
|
// 检查item.name是否存在,避免null引用错误
|
||||||
|
if (!item.name || typeof item.name !== 'string') {
|
||||||
|
return // 跳过无效数据
|
||||||
|
}
|
||||||
|
|
||||||
// 处理空格分隔的地域数据格式(如:"山东省 临沂市 莒南县")
|
// 处理空格分隔的地域数据格式(如:"山东省 临沂市 莒南县")
|
||||||
if (item.name.includes(' ')) {
|
if (item.name.includes(' ')) {
|
||||||
const parts = item.name.split(' ')
|
const parts = item.name.split(' ')
|
||||||
@@ -170,7 +177,7 @@ const chartData = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const updateChart = () => {
|
const updateChart = () => {
|
||||||
if (!customerTypeChart) return;
|
if (!customerTypeChart || !customerTypeChartRef.value) return;
|
||||||
|
|
||||||
// 使用真实数据
|
// 使用真实数据
|
||||||
const currentData = chartData.value
|
const currentData = chartData.value
|
||||||
@@ -265,9 +272,21 @@ watch(chartData, () => {
|
|||||||
updateChart()
|
updateChart()
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
|
// 监听类别变化,确保切换时立即更新图表
|
||||||
|
watch(customerTypeCategory, () => {
|
||||||
|
nextTick(() => {
|
||||||
|
updateChart()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 使用nextTick确保DOM完全渲染后再初始化图表
|
||||||
|
nextTick(() => {
|
||||||
|
if (customerTypeChartRef.value) {
|
||||||
customerTypeChart = echarts.init(customerTypeChartRef.value);
|
customerTypeChart = echarts.init(customerTypeChartRef.value);
|
||||||
updateChart();
|
updateChart();
|
||||||
|
}
|
||||||
|
});
|
||||||
window.addEventListener('resize', resizeChart);
|
window.addEventListener('resize', resizeChart);
|
||||||
// 初始化时触发一次事件,加载默认数据
|
// 初始化时触发一次事件,加载默认数据
|
||||||
handleCategoryChange()
|
handleCategoryChange()
|
||||||
|
|||||||
@@ -4,6 +4,21 @@
|
|||||||
<header class="dashboard-header">
|
<header class="dashboard-header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<div class="logo-section">
|
<div class="logo-section">
|
||||||
|
<!-- 动态顶栏:根据是否有路由参数显示不同内容 -->
|
||||||
|
<!-- 路由跳转时的顶栏:面包屑 + 姓名 -->
|
||||||
|
<div v-if="isRouteNavigation" class="route-header">
|
||||||
|
<div class="breadcrumb">
|
||||||
|
<span class="breadcrumb-item" @click="goBack">团队管理</span>
|
||||||
|
<span class="breadcrumb-separator">></span>
|
||||||
|
<span class="breadcrumb-item current"> {{ routeUserName }}中心组长指挥台</span>
|
||||||
|
</div>
|
||||||
|
<div class="user-name">
|
||||||
|
{{ routeUserName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自己登录时的顶栏:原有样式 -->
|
||||||
|
<template v-else>
|
||||||
<div class="header-text">
|
<div class="header-text">
|
||||||
<h1>中心组长指挥台</h1>
|
<h1>中心组长指挥台</h1>
|
||||||
<p>统筹多组运营,优化资源配置,驱动业绩增长,实现团队协同发展。</p>
|
<p>统筹多组运营,优化资源配置,驱动业绩增长,实现团队协同发展。</p>
|
||||||
@@ -14,6 +29,8 @@
|
|||||||
<span class="stage-label">营期所属阶段:</span>
|
<span class="stage-label">营期所属阶段:</span>
|
||||||
<span class="stage-value">接数据</span>
|
<span class="stage-value">接数据</span>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<!-- 用户下拉菜单 -->
|
<!-- 用户下拉菜单 -->
|
||||||
<UserDropdown />
|
<UserDropdown />
|
||||||
@@ -178,6 +195,22 @@
|
|||||||
|
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 判断是否为路由导航(有路由参数)
|
||||||
|
const isRouteNavigation = computed(() => {
|
||||||
|
const routeUserName = router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name
|
||||||
|
return !!routeUserName
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取路由传递的用户名
|
||||||
|
const routeUserName = computed(() => {
|
||||||
|
return router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
router.go(-1)
|
||||||
|
}
|
||||||
// 中心整体概览
|
// 中心整体概览
|
||||||
const overallCenterPerformance = ref({
|
const overallCenterPerformance = ref({
|
||||||
CenterPerformance: {},
|
CenterPerformance: {},
|
||||||
@@ -282,7 +315,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 客户类型
|
// 客户类型
|
||||||
async function CenterCustomerType(distributionType = 'occupation') {
|
async function CenterCustomerType(distributionType = 'child_education') {
|
||||||
const params = getRequestParams()
|
const params = getRequestParams()
|
||||||
const hasParams = params.user_name
|
const hasParams = params.user_name
|
||||||
// 添加distribution_type参数
|
// 添加distribution_type参数
|
||||||
@@ -393,79 +426,6 @@ const conversionRateVsAverage = ref({})
|
|||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
groupPerformance.value = res.data
|
groupPerformance.value = res.data
|
||||||
/**
|
|
||||||
* "data": {
|
|
||||||
"department": "巅峰三部-刘东洋",
|
|
||||||
"group_details": [
|
|
||||||
{
|
|
||||||
"name": "徐小玉",
|
|
||||||
"today_performance": 0.0,
|
|
||||||
"monthly_performance": 0.0,
|
|
||||||
"conversion_rate_this_period": "0.00%",
|
|
||||||
"new_customers_this_period": 41,
|
|
||||||
"deals_this_period": 0,
|
|
||||||
"deals_this_month": 0,
|
|
||||||
"call_count_this_period": 0,
|
|
||||||
"rank": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "唐梦",
|
|
||||||
"today_performance": 0.0,
|
|
||||||
"monthly_performance": 0.0,
|
|
||||||
"conversion_rate_this_period": "0.00%",
|
|
||||||
"new_customers_this_period": 39,
|
|
||||||
"deals_this_period": 0,
|
|
||||||
"deals_this_month": 0,
|
|
||||||
"call_count_this_period": 0,
|
|
||||||
"rank": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "董富忠",
|
|
||||||
"today_performance": 0,
|
|
||||||
"monthly_performance": 6500.0,
|
|
||||||
"conversion_rate_this_period": "1.72%",
|
|
||||||
"new_customers_this_period": 58,
|
|
||||||
"deals_this_period": 1,
|
|
||||||
"deals_this_month": 1,
|
|
||||||
"call_count_this_period": 0,
|
|
||||||
"rank": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "王娟娟",
|
|
||||||
"today_performance": 0.0,
|
|
||||||
"monthly_performance": 0.0,
|
|
||||||
"conversion_rate_this_period": "0.00%",
|
|
||||||
"new_customers_this_period": 51,
|
|
||||||
"deals_this_period": 0,
|
|
||||||
"deals_this_month": 0,
|
|
||||||
"call_count_this_period": 0,
|
|
||||||
"rank": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "罗荣海",
|
|
||||||
"today_performance": 0.0,
|
|
||||||
"monthly_performance": 0.0,
|
|
||||||
"conversion_rate_this_period": "0.00%",
|
|
||||||
"new_customers_this_period": 0,
|
|
||||||
"deals_this_period": 0,
|
|
||||||
"deals_this_month": 0,
|
|
||||||
"call_count_this_period": 0,
|
|
||||||
"rank": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "代秀珍",
|
|
||||||
"today_performance": 0.0,
|
|
||||||
"monthly_performance": 0.0,
|
|
||||||
"conversion_rate_this_period": "0.00%",
|
|
||||||
"new_customers_this_period": 39,
|
|
||||||
"deals_this_period": 0,
|
|
||||||
"deals_this_month": 0,
|
|
||||||
"call_count_this_period": 0,
|
|
||||||
"rank": 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取团队排名失败:', error)
|
console.error('获取团队排名失败:', error)
|
||||||
@@ -480,35 +440,6 @@ const conversionRateVsAverage = ref({})
|
|||||||
const selectGroup = async (group) => {
|
const selectGroup = async (group) => {
|
||||||
selectedGroup.value = group
|
selectedGroup.value = group
|
||||||
console.log('选中的组别111:', group)
|
console.log('选中的组别111:', group)
|
||||||
|
|
||||||
// // 获取组名并调用API获取团队成员详情
|
|
||||||
// let departmentName = group.name
|
|
||||||
|
|
||||||
// // 如果有groupList数据,尝试找到完整的部门名称
|
|
||||||
// if (groupList.value && groupList.value.formal_plural) {
|
|
||||||
// const departmentKeys = Object.keys(groupList.value.formal_plural)
|
|
||||||
// console.log('所有部门名称:', departmentKeys)
|
|
||||||
// console.log('当前选中组别名称:', group.name)
|
|
||||||
|
|
||||||
// // 优先进行精确匹配
|
|
||||||
// let matchedDepartment = departmentKeys.find(key => key === group.name)
|
|
||||||
|
|
||||||
// // 如果精确匹配失败,尝试模糊匹配
|
|
||||||
// if (!matchedDepartment) {
|
|
||||||
// matchedDepartment = departmentKeys.find(key => {
|
|
||||||
// // 提取部门主要名称进行匹配(去掉经理名字部分)
|
|
||||||
// const mainName = key.split('-')[0] || key
|
|
||||||
// const groupMainName = group.name.split('-')[0] || group.name
|
|
||||||
// return mainName.includes(groupMainName) || groupMainName.includes(mainName)
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (matchedDepartment) {
|
|
||||||
// departmentName = matchedDepartment
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// console.log('选中的组别:', group.name, '-> 发送的部门名称:', departmentName)
|
|
||||||
const departmentName = group.name+'-'+group.leader
|
const departmentName = group.name+'-'+group.leader
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -575,15 +506,15 @@ const conversionRateVsAverage = ref({})
|
|||||||
return statusMap[status] || '未知'
|
return statusMap[status] || '未知'
|
||||||
}
|
}
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// await CenterOverallCenterPerformance()
|
await CenterOverallCenterPerformance()
|
||||||
// await CenterTotalGroupCount()
|
await CenterTotalGroupCount()
|
||||||
// await CenterConversionRate()
|
await CenterConversionRate()
|
||||||
// await CenterTotalCallCount()
|
await CenterTotalCallCount()
|
||||||
// await CenterNewCustomer()
|
await CenterNewCustomer()
|
||||||
// await CenterDepositConversionRate()
|
await CenterDepositConversionRate()
|
||||||
// await CenterCustomerType()
|
await CenterCustomerType()
|
||||||
// await CenterUrgentNeedToAddress()
|
await CenterUrgentNeedToAddress()
|
||||||
// await CenterConversionRateVsAverage()
|
await CenterConversionRateVsAverage()
|
||||||
await CenterSeniorManagerList()
|
await CenterSeniorManagerList()
|
||||||
await CenterGroupList('all') // 初始化加载全部高级经理数据
|
await CenterGroupList('all') // 初始化加载全部高级经理数据
|
||||||
})
|
})
|
||||||
@@ -1142,4 +1073,53 @@ const conversionRateVsAverage = ref({})
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 路由导航顶栏样式
|
||||||
|
.route-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
.breadcrumb-item {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&:not(.current) {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.current {
|
||||||
|
color: #1e293b;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-separator {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
color: #1e293b;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
315
my-vue-app/src/views/topOne/components/CustomerType.vue
Normal file
315
my-vue-app/src/views/topOne/components/CustomerType.vue
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="chart-header">
|
||||||
|
<h3>客户类型占比</h3>
|
||||||
|
<select v-model="customerTypeCategory" @change="handleCategoryChange" class="chart-select">
|
||||||
|
<option value="profession">职业</option>
|
||||||
|
<option value="childGrade">年级</option>
|
||||||
|
<option value="region">地域</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="chart-content">
|
||||||
|
<div ref="customerTypeChartRef" class="customer-type-chart"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, onBeforeUnmount, computed, watch } from 'vue';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
// 定义props
|
||||||
|
const props = defineProps({
|
||||||
|
customerData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义emit
|
||||||
|
const emit = defineEmits(['categoryChange'])
|
||||||
|
|
||||||
|
const customerTypeChartRef = ref(null);
|
||||||
|
let customerTypeChart = null;
|
||||||
|
|
||||||
|
const customerTypeCategory = ref('childGrade');
|
||||||
|
|
||||||
|
// 计算属性:将API数据转换为图表所需格式
|
||||||
|
const chartData = computed(() => {
|
||||||
|
if (!props.customerData || !props.customerData.customer_type_distribution) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let processedData = props.customerData.customer_type_distribution.map(item => ({
|
||||||
|
name: item.category,
|
||||||
|
value: parseFloat(item.ratio.replace('%', '')) || 0
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 如果是地域类型,需要按省份进行数据整理
|
||||||
|
if (customerTypeCategory.value === 'region') {
|
||||||
|
const provinceMap = new Map()
|
||||||
|
|
||||||
|
processedData.forEach(item => {
|
||||||
|
// 提取省份名称,处理"山东省 临沂市 莒南县"这种空格分隔的格式
|
||||||
|
let provinceName = item.name
|
||||||
|
|
||||||
|
// 处理空格分隔的地域数据格式(如:"山东省 临沂市 莒南县")
|
||||||
|
if (item.name.includes(' ')) {
|
||||||
|
const parts = item.name.split(' ')
|
||||||
|
provinceName = parts[0] // 取第一部分作为省份
|
||||||
|
}
|
||||||
|
// 处理横线分隔的格式
|
||||||
|
else if (item.name.includes('-')) {
|
||||||
|
provinceName = item.name.split('-')[0]
|
||||||
|
}
|
||||||
|
// 处理包含省字的格式
|
||||||
|
else if (item.name.includes('省') || item.name.includes('市') || item.name.includes('区')) {
|
||||||
|
// 如果已经包含省份标识,直接使用
|
||||||
|
if (item.name.includes('省') || item.name.includes('自治区') || item.name.endsWith('市')) {
|
||||||
|
provinceName = item.name
|
||||||
|
} else {
|
||||||
|
// 根据城市名推断省份
|
||||||
|
const cityToProvince = {
|
||||||
|
'北京': '北京市',
|
||||||
|
'上海': '上海市',
|
||||||
|
'天津': '天津市',
|
||||||
|
'重庆': '重庆市',
|
||||||
|
'广州': '广东省',
|
||||||
|
'深圳': '广东省',
|
||||||
|
'珠海': '广东省',
|
||||||
|
'佛山': '广东省',
|
||||||
|
'东莞': '广东省',
|
||||||
|
'中山': '广东省',
|
||||||
|
'杭州': '浙江省',
|
||||||
|
'宁波': '浙江省',
|
||||||
|
'温州': '浙江省',
|
||||||
|
'南京': '江苏省',
|
||||||
|
'苏州': '江苏省',
|
||||||
|
'无锡': '江苏省',
|
||||||
|
'常州': '江苏省',
|
||||||
|
'成都': '四川省',
|
||||||
|
'绵阳': '四川省',
|
||||||
|
'武汉': '湖北省',
|
||||||
|
'宜昌': '湖北省',
|
||||||
|
'西安': '陕西省',
|
||||||
|
'郑州': '河南省',
|
||||||
|
'洛阳': '河南省',
|
||||||
|
'济南': '山东省',
|
||||||
|
'青岛': '山东省',
|
||||||
|
'烟台': '山东省',
|
||||||
|
'潍坊': '山东省',
|
||||||
|
'临沂': '山东省',
|
||||||
|
'大连': '辽宁省',
|
||||||
|
'沈阳': '辽宁省',
|
||||||
|
'长春': '吉林省',
|
||||||
|
'哈尔滨': '黑龙江省',
|
||||||
|
'石家庄': '河北省',
|
||||||
|
'唐山': '河北省',
|
||||||
|
'太原': '山西省',
|
||||||
|
'呼和浩特': '内蒙古自治区',
|
||||||
|
'南宁': '广西壮族自治区',
|
||||||
|
'桂林': '广西壮族自治区',
|
||||||
|
'海口': '海南省',
|
||||||
|
'三亚': '海南省',
|
||||||
|
'昆明': '云南省',
|
||||||
|
'贵阳': '贵州省',
|
||||||
|
'拉萨': '西藏自治区',
|
||||||
|
'兰州': '甘肃省',
|
||||||
|
'西宁': '青海省',
|
||||||
|
'银川': '宁夏回族自治区',
|
||||||
|
'乌鲁木齐': '新疆维吾尔自治区',
|
||||||
|
'合肥': '安徽省',
|
||||||
|
'芜湖': '安徽省',
|
||||||
|
'福州': '福建省',
|
||||||
|
'厦门': '福建省',
|
||||||
|
'泉州': '福建省',
|
||||||
|
'南昌': '江西省',
|
||||||
|
'长沙': '湖南省',
|
||||||
|
'株洲': '湖南省',
|
||||||
|
// 港澳台地区
|
||||||
|
'香港': '香港特别行政区',
|
||||||
|
'澳门': '澳门特别行政区',
|
||||||
|
'台北': '台湾省',
|
||||||
|
'高雄': '台湾省',
|
||||||
|
'台中': '台湾省',
|
||||||
|
'台南': '台湾省',
|
||||||
|
'桃园': '台湾省',
|
||||||
|
'新竹': '台湾省',
|
||||||
|
'基隆': '台湾省',
|
||||||
|
'嘉义': '台湾省',
|
||||||
|
'台东': '台湾省',
|
||||||
|
'花莲': '台湾省',
|
||||||
|
'宜兰': '台湾省',
|
||||||
|
'屏东': '台湾省',
|
||||||
|
'彰化': '台湾省',
|
||||||
|
'南投': '台湾省',
|
||||||
|
'云林': '台湾省',
|
||||||
|
'苗栗': '台湾省',
|
||||||
|
'澎湖': '台湾省'
|
||||||
|
}
|
||||||
|
provinceName = cityToProvince[item.name] || item.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 聚合同一省份的数据
|
||||||
|
if (provinceMap.has(provinceName)) {
|
||||||
|
provinceMap.set(provinceName, provinceMap.get(provinceName) + item.value)
|
||||||
|
} else {
|
||||||
|
provinceMap.set(provinceName, item.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 转换为数组格式
|
||||||
|
processedData = Array.from(provinceMap.entries()).map(([name, value]) => ({
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedData
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateChart = () => {
|
||||||
|
if (!customerTypeChart) return;
|
||||||
|
|
||||||
|
// 使用真实数据
|
||||||
|
const currentData = chartData.value
|
||||||
|
|
||||||
|
if (!currentData || currentData.length === 0) {
|
||||||
|
// 如果没有数据,显示空状态
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
text: '暂无数据',
|
||||||
|
left: 'center',
|
||||||
|
top: 'middle',
|
||||||
|
textStyle: {
|
||||||
|
color: '#999',
|
||||||
|
fontSize: 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customerTypeChart.setOption(option, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: function(params) {
|
||||||
|
return params.name + '<br/>' + '占比: ' + params.value + '%' + ' (' + params.percent + '%)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orient: 'vertical',
|
||||||
|
left: 'left',
|
||||||
|
top: 'center',
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
name: '客户类型占比',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['40%', '70%'],
|
||||||
|
center: ['65%', '50%'],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
position: 'center'
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
fontSize: '16',
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
data: currentData.map((item, index) => ({
|
||||||
|
name: item.name,
|
||||||
|
value: item.value,
|
||||||
|
itemStyle: {
|
||||||
|
color: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#fc8452', '#9a60b4'][index % 7]
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
customerTypeChart.setOption(option, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initChart = () => {
|
||||||
|
if (!customerTypeChartRef.value) return;
|
||||||
|
customerTypeChart = echarts.init(customerTypeChartRef.value);
|
||||||
|
updateChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeChart = () => customerTypeChart?.resize();
|
||||||
|
|
||||||
|
// 处理类别选择变化
|
||||||
|
const handleCategoryChange = () => {
|
||||||
|
// 映射选择值到API参数
|
||||||
|
const distributionTypeMap = {
|
||||||
|
'profession': 'occupation',
|
||||||
|
'childGrade': 'child_education',
|
||||||
|
'region': 'territory'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知父组件选择变化
|
||||||
|
emit('categoryChange', distributionTypeMap[customerTypeCategory.value])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听数据变化
|
||||||
|
watch(chartData, () => {
|
||||||
|
updateChart()
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
customerTypeChart = echarts.init(customerTypeChartRef.value);
|
||||||
|
updateChart();
|
||||||
|
window.addEventListener('resize', resizeChart);
|
||||||
|
// 初始化时触发一次事件,加载默认数据
|
||||||
|
handleCategoryChange()
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
customerTypeChart?.dispose();
|
||||||
|
window.removeEventListener('resize', resizeChart);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.chart-container {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
height: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.chart-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 20px 16px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
h3 { margin: 0; color: #303133; font-size: 18px; font-weight: 600; }
|
||||||
|
}
|
||||||
|
.chart-content {
|
||||||
|
padding: 20px;
|
||||||
|
flex-grow: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.customer-type-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
.chart-select {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -149,7 +149,7 @@ import SalesProgress from "./components/SalesProgress.vue";
|
|||||||
import TaskList from "./components/TaskList.vue";
|
import TaskList from "./components/TaskList.vue";
|
||||||
import FunnelChart from "./components/FunnelChart.vue";
|
import FunnelChart from "./components/FunnelChart.vue";
|
||||||
import CustomerProfile from "./components/CustomerProfile.vue";
|
import CustomerProfile from "./components/CustomerProfile.vue";
|
||||||
import CustomerType from "../secondTop/components/CustomerType.vue";
|
import CustomerType from "./components/CustomerType.vue";
|
||||||
import ProblemRanking from "../secondTop/components/ProblemRanking.vue";
|
import ProblemRanking from "../secondTop/components/ProblemRanking.vue";
|
||||||
import RankingList from "./components/RankingList.vue";
|
import RankingList from "./components/RankingList.vue";
|
||||||
import PersonalSalesRanking from "./components/PersonalSalesRanking.vue";
|
import PersonalSalesRanking from "./components/PersonalSalesRanking.vue";
|
||||||
|
|||||||
Reference in New Issue
Block a user