feat: 添加路由权限控制和高级经理数据动态加载

refactor(router): 实现基于用户等级的路由守卫
feat(secondTop): 动态加载高级经理列表数据
fix(SalesTimelineWithTaskList): 修正默认职业和教育显示
perf(CustomerType): 优化图表数据绑定和交互
chore: 更新API基础路径配置
This commit is contained in:
2025-08-15 14:05:24 +08:00
parent 814961d84a
commit 88a73f5b52
6 changed files with 202 additions and 78 deletions

View File

@@ -5,6 +5,7 @@ import SeniorManager from '@/views/senorManger/seniorManager.vue'
import TopOne from '@/views/topOne/topone.vue' import TopOne from '@/views/topOne/topone.vue'
import Login from '@/views/login/login.vue' import Login from '@/views/login/login.vue'
import SecondTop from '@/views/secondTop/secondTop.vue' import SecondTop from '@/views/secondTop/secondTop.vue'
import { useUserStore } from '@/stores/user.js'
const routes = [ const routes = [
{ {
@@ -15,32 +16,38 @@ const routes = [
{ {
path: '/login', path: '/login',
name: 'Login', name: 'Login',
component: Login component: Login,
meta: { requiresAuth: false }
}, },
{ {
path: '/sale', path: '/sale',
name: 'Sale', name: 'Sale',
component: Sale component: Sale,
meta: { requiresAuth: true, minLevel: 1 }
}, },
{ {
path: '/manager', path: '/manager',
name: 'Manager', name: 'Manager',
component: Manager component: Manager,
meta: { requiresAuth: true, minLevel: 2 }
}, },
{ {
path: '/senior-manager', path: '/senior-manager',
name: 'SeniorManager', name: 'SeniorManager',
component: SeniorManager component: SeniorManager,
meta: { requiresAuth: true, minLevel: 3 }
}, },
{ {
path: '/second-top', path: '/second-top',
name: 'SecondTop', name: 'SecondTop',
component: SecondTop component: SecondTop,
meta: { requiresAuth: true, minLevel: 4 }
}, },
{ {
path: '/top', path: '/top',
name: 'Top', name: 'Top',
component: TopOne component: TopOne,
meta: { requiresAuth: true, minLevel: 5 }
} }
] ]
@@ -49,4 +56,40 @@ const router = createRouter({
routes routes
}) })
// 路由守卫
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
// 如果路由不需要认证,直接通过
if (!to.meta.requiresAuth) {
next()
return
}
// 检查是否已登录
if (!userStore.isLoggedIn || !userStore.userInfo) {
next('/login')
return
}
// 检查用户等级权限
const userLevel = userStore.userInfo.user_level
const requiredLevel = to.meta.minLevel
// 用户等级必须大于等于所需等级(等级高的可以访问等级低的页面)
if (userLevel >= requiredLevel) {
next()
} else {
// 权限不足,重定向到对应等级的默认页面
const defaultRoutes = {
1: '/sale',
2: '/manager',
3: '/senior-manager',
4: '/second-top',
5: '/top'
}
next(defaultRoutes[userLevel] || '/sale')
}
})
export default router export default router

View File

@@ -5,7 +5,7 @@ import { useUserStore } from '@/stores/user'
// 创建axios实例 // 创建axios实例
const service = axios.create({ const service = axios.create({
baseURL: 'http://192.168.15.51:8890' || '', // API基础路径支持完整URL baseURL: 'http://192.168.15.53:8890' || '', // API基础路径支持完整URL
timeout: 100000, // 请求超时时间 timeout: 100000, // 请求超时时间
headers: { headers: {
'Content-Type': 'application/json;charset=UTF-8' 'Content-Type': 'application/json;charset=UTF-8'

View File

@@ -56,8 +56,8 @@
</div> </div>
<div class="item-footer"> <div class="item-footer">
<div class="profession-education"> <div class="profession-education">
<span class="profession">{{ item.profession || '公务员' }}</span> <span class="profession">{{ item.profession||'未知' }}</span>
<span class="education">{{ item.education || '高中' }}</span> <span class="education">{{ item.education||'未知' }}</span>
<span class="course-attendance"> <span class="course-attendance">
{{ getAttendedLessons(item.class_situation,item.class_num) }} {{ getAttendedLessons(item.class_situation,item.class_num) }}
</span> </span>

View File

@@ -2,8 +2,7 @@
<div class="chart-container"> <div class="chart-container">
<div class="chart-header"> <div class="chart-header">
<h3>客户类型占比</h3> <h3>客户类型占比</h3>
<select v-model="customerTypeCategory" @change="updateChart" class="chart-select"> <select v-model="customerTypeCategory" @change="handleCategoryChange" class="chart-select">
<option value="age">年龄</option>
<option value="profession">职业</option> <option value="profession">职业</option>
<option value="childGrade">孩子年级</option> <option value="childGrade">孩子年级</option>
<option value="region">地域</option> <option value="region">地域</option>
@@ -16,30 +15,88 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'; import { ref, reactive, onMounted, onBeforeUnmount, computed, watch } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
// 定义props
const props = defineProps({
customerData: {
type: Object,
default: () => ({})
}
})
// 定义emit
const emit = defineEmits(['categoryChange'])
const customerTypeChartRef = ref(null); const customerTypeChartRef = ref(null);
let customerTypeChart = null; let customerTypeChart = null;
const customerTypeCategory = ref('age'); const customerTypeCategory = ref('profession');
const customerTypeData = reactive({
age: [{ value: 120, name: '18-25岁' }, { value: 200, name: '26-35岁' }, { value: 150, name: '36-45岁' }, { value: 80, name: '46-55岁' }, { value: 50, name: '55岁以上' }], // 计算属性将API数据转换为图表所需格式
profession: [{ value: 180, name: '企业管理者' }, { value: 120, name: '教师' }, { value: 100, name: '医生' }, { value: 90, name: '工程师' }, { value: 110, name: '其他' }], const chartData = computed(() => {
childGrade: [{ value: 80, name: '幼儿园' }, { value: 150, name: '小学' }, { value: 180, name: '初中' }, { value: 120, name: '高中' }, { value: 70, name: '大学' }], if (!props.customerData || !props.customerData.customer_type_distribution) {
region: [{ value: 200, name: '北京' }, { value: 150, name: '上海' }, { value: 120, name: '广州' }, { value: 100, name: '深圳' }, { value: 130, name: '其他' }] return []
}); }
return props.customerData.customer_type_distribution.map(item => ({
name: item.category,
value: parseFloat(item.ratio.replace('%', '')) || 0
}))
})
const updateChart = () => { const updateChart = () => {
if (!customerTypeChart) return; if (!customerTypeChart) return;
const currentData = customerTypeData[customerTypeCategory.value];
// 使用真实数据
const currentData = chartData.value
if (!currentData || currentData.length === 0) {
// 如果没有数据,显示空状态
const option = { const option = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, title: {
text: '暂无数据',
left: 'center',
top: 'middle',
textStyle: {
color: '#999',
fontSize: 16
}
}
}
customerTypeChart.setOption(option, true)
return
}
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: function(params) {
return params[0].name + '<br/>' + params[0].seriesName + ': ' + params[0].value + '%'
}
},
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: currentData.map(item => item.name), axisTick: { alignWithLabel: true } }, xAxis: {
yAxis: { type: 'value' }, type: 'category',
data: currentData.map(item => item.name),
axisTick: { alignWithLabel: true },
axisLabel: {
interval: 0,
rotate: currentData.length > 5 ? 45 : 0
}
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '{value}%'
}
},
series: [{ series: [{
name: '客户数量', type: 'bar', barWidth: '60%', name: '占比',
type: 'bar',
barWidth: '60%',
data: currentData.map(item => item.value), data: currentData.map(item => item.value),
itemStyle: { itemStyle: {
color: (params) => ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de'][params.dataIndex % 5] color: (params) => ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de'][params.dataIndex % 5]
@@ -57,9 +114,30 @@ const initChart = () => {
const resizeChart = () => customerTypeChart?.resize(); 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(() => { onMounted(() => {
initChart(); customerTypeChart = echarts.init(customerTypeChartRef.value);
updateChart();
window.addEventListener('resize', resizeChart); window.addEventListener('resize', resizeChart);
// 初始化时触发一次事件,加载默认数据
handleCategoryChange()
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@@ -56,6 +56,10 @@ const props = defineProps({
groups: { groups: {
type: Array, type: Array,
required: true required: true
},
seniorManagerData: {
type: Object,
default: () => ({})
} }
}) })
@@ -65,13 +69,21 @@ const emit = defineEmits(['select-group', 'manager-change'])
const selectedManager = ref('all') const selectedManager = ref('all')
// 高级经理列表 // 高级经理列表
const seniorManagers = ref([ const seniorManagers = computed(() => {
if (props.seniorManagerData && props.seniorManagerData.center_advanced_managers) {
return props.seniorManagerData.center_advanced_managers.map((name, index) => ({
id: `manager_${index}`,
name: name
}))
}
return [
{ id: 'manager1', name: '张经理' }, { id: 'manager1', name: '张经理' },
{ id: 'manager2', name: '李经理' }, { id: 'manager2', name: '李经理' },
{ id: 'manager3', name: '王经理' }, { id: 'manager3', name: '王经理' },
{ id: 'manager4', name: '刘经理' }, { id: 'manager4', name: '刘经理' },
{ id: 'manager5', name: '陈经理' } { id: 'manager5', name: '陈经理' }
]) ]
})
// 处理经理选择变化 // 处理经理选择变化
const handleManagerChange = () => { const handleManagerChange = () => {

View File

@@ -36,7 +36,7 @@
</div> </div>
<div class="BB-section"> <div class="BB-section">
<!--客户类型占比--> <!--客户类型占比-->
<CustomerType /> <CustomerType :customer-data="customerTypeDistribution" @category-change="handleCustomerTypeChange" />
<!-- 优秀录音 --> <!-- 优秀录音 -->
<GoodMusic /> <GoodMusic />
<!-- 客户问题排行 --> <!-- 客户问题排行 -->
@@ -51,7 +51,7 @@
<!-- Right Section - Group Comparison --> <!-- Right Section - Group Comparison -->
<div class="right-section"> <div class="right-section">
<GroupComparison :groups="groups" @select-group="selectGroup" /> <GroupComparison :groups="groups" :senior-manager-data="seniorManagerList" @select-group="selectGroup" />
</div> </div>
</div> </div>
@@ -285,18 +285,25 @@
} }
} }
// 客户类型 // 客户类型
async function CenterCustomerType() { async function CenterCustomerType(distributionType = 'occupation') {
const params = getRequestParams() const params = getRequestParams()
const hasParams = params.user_name const hasParams = params.user_name
// 添加distribution_type参数
const requestParams = hasParams ? { ...params, distribution_type: distributionType } : { distribution_type: distributionType }
try { try {
const res = await getCustomerTypeDistribution(hasParams ? params : undefined) const res = await getCustomerTypeDistribution(requestParams)
if (res.code === 200) { if (res.code === 200) {
customerTypeDistribution.value = res.data customerTypeDistribution.value = res.data
} }
} catch (error) { } catch (error) {
console.error('获取中心整体概览失败:', error) console.error('获取客户类型分布失败:', error)
} }
} }
// 处理客户类型选择变化
const handleCustomerTypeChange = (distributionType) => {
CenterCustomerType(distributionType)
}
// 客户迫切解决的问题 // 客户迫切解决的问题
async function CenterUrgentNeedToAddress() { async function CenterUrgentNeedToAddress() {
const params = getRequestParams() const params = getRequestParams()
@@ -305,46 +312,29 @@
const res = await getUrgentNeedToAddress(hasParams ? params : undefined) const res = await getUrgentNeedToAddress(hasParams ? params : undefined)
if (res.code === 200) { if (res.code === 200) {
urgentNeedToAddress.value = res.data urgentNeedToAddress.value = res.data
}
} catch (error) {
console.error('获取中心整体概览失败:', error)
}
}
// 综合排名---高级经理列表
const seniorManagerList = ref([])
async function CenterSeniorManagerList() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const res = await getCenterAdvancedManagerList(hasParams ? params : undefined)
if (res.code === 200) {
seniorManagerList.value = res.data
/** /**
* data * "data": {
: "user_name": "刘瑞",
{user_name: "刘瑞", user_level: 4,…} "user_level": 4,
center_urgent_issue_counts "center_advanced_managers": [
: "陈盼良"
{成绩提升: 1, 少玩手机: 1, 回归学校: 0, 心理健康: 0} ]
回归学校 }
:
0
少玩手机
:
1
心理健康
:
0
成绩提升
:
1
center_urgent_issue_ratio
:
{成绩提升: "50.00%", 少玩手机: "50.00%", 回归学校: "0.00%", 心理健康: "0.00%"}
回归学校
:
"0.00%"
少玩手机
:
"50.00%"
心理健康
:
"0.00%"
成绩提升
:
"50.00%"
user_level
:
4
user_name
:
"刘瑞"
*/ */
} }
} catch (error) { } catch (error) {
@@ -396,6 +386,7 @@ user_name
await CenterDepositConversionRate() await CenterDepositConversionRate()
await CenterCustomerType() await CenterCustomerType()
await CenterUrgentNeedToAddress() await CenterUrgentNeedToAddress()
await CenterSeniorManagerList()
}) })