feat:用户意向专业排行
This commit is contained in:
		
							parent
							
								
									1e347af4f6
								
							
						
					
					
						commit
						c1f5e00dd5
					
				
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.4 KiB | 
|  | @ -81,7 +81,7 @@ $item_title_content-height: calc(100% - 33px - 20px); | ||||||
| 
 | 
 | ||||||
| .item_title_content_def { | .item_title_content_def { | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   height: 1px; |   height: 100%; | ||||||
|   background: #fff; |   background: #fff; | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,155 +1,221 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { ref, onMounted } from 'vue'; | import { ref, onMounted } from 'vue'; | ||||||
| import { highSchoolRanking } from "@/api"; | import * as echarts from 'echarts'; | ||||||
|  | import { studentDistribution } from "@/api"; | ||||||
| 
 | 
 | ||||||
| interface RankingItem { | const chartRef = ref<HTMLElement | null>(null); | ||||||
|   rank: number; | let chart: echarts.ECharts | null = null; | ||||||
|   school: string; |  | ||||||
|   count: number; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| // 排行数据 | // 年份筛选选项 | ||||||
| const rankingData = ref<RankingItem[]>([]); | const yearOptions = ref([ | ||||||
|  |   { label: '年份筛选', value: 'all' } | ||||||
|  | ]); | ||||||
|  | const selectedYear = ref('all'); | ||||||
| 
 | 
 | ||||||
| // 获取排行数据 | // 数据 | ||||||
| const fetchRankingData = () => { | const chartData = ref({ | ||||||
|   highSchoolRanking().then(res => { |   years: ['2022', '2023', '2024', '2025', '2026', '2027'], | ||||||
|  |   values: [50, 200, 550, 850, 750, 900, 200] | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | // 获取数据 | ||||||
|  | const getChartData = () => { | ||||||
|  |   studentDistribution().then(res => { | ||||||
|     if (res.success) { |     if (res.success) { | ||||||
|       rankingData.value = res.data; |       // 实际项目中应该使用API返回的数据 | ||||||
|  |       // chartData.value = res.data; | ||||||
|  |       initChart(); | ||||||
|  |     } else { | ||||||
|  |       // 如果API调用失败,使用模拟数据 | ||||||
|  |       initChart(); | ||||||
|     } |     } | ||||||
|  |   }).catch(() => { | ||||||
|  |     // 如果发生错误,使用模拟数据 | ||||||
|  |     initChart(); | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | // 处理年份筛选变化 | ||||||
|  | const handleYearChange = () => { | ||||||
|  |   getChartData(); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const initChart = () => { | ||||||
|  |   if (!chartRef.value) return; | ||||||
|  |    | ||||||
|  |   chart = echarts.init(chartRef.value); | ||||||
|  |    | ||||||
|  |   const option = { | ||||||
|  |     grid: { | ||||||
|  |       top: '15%', | ||||||
|  |       left: '0', | ||||||
|  |       right: '5%', | ||||||
|  |       bottom: '0', | ||||||
|  |       containLabel: true | ||||||
|  |     }, | ||||||
|  |     xAxis: { | ||||||
|  |       type: 'category', | ||||||
|  |       data: chartData.value.years, | ||||||
|  |       axisLine: { | ||||||
|  |         lineStyle: { | ||||||
|  |           color: '#E0E0E0' | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       axisLabel: { | ||||||
|  |         color: '#99AABF', | ||||||
|  |         fontSize: 12 | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     yAxis: { | ||||||
|  |       type: 'value', | ||||||
|  |       name: '', | ||||||
|  |       max: 900, | ||||||
|  |       interval: 300, | ||||||
|  |       axisLabel: { | ||||||
|  |         color: '#99AABF', | ||||||
|  |         fontSize: 12 | ||||||
|  |       }, | ||||||
|  |       splitLine: { | ||||||
|  |         lineStyle: { | ||||||
|  |           type: 'dashed', | ||||||
|  |           color: '#E0E0E0' | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     series: [ | ||||||
|  |       { | ||||||
|  |         data: chartData.value.values, | ||||||
|  |         type: 'line', | ||||||
|  |         smooth: true, | ||||||
|  |         symbol: 'circle', | ||||||
|  |         symbolSize: 8, | ||||||
|  |         itemStyle: { | ||||||
|  |           color: '#fff', | ||||||
|  |           borderColor: '#4B96FF', | ||||||
|  |           borderWidth: 2 | ||||||
|  |         }, | ||||||
|  |         lineStyle: { | ||||||
|  |           color: '#4B96FF', | ||||||
|  |           width: 3 | ||||||
|  |         }, | ||||||
|  |         areaStyle: { | ||||||
|  |           color: { | ||||||
|  |             type: 'linear', | ||||||
|  |             x: 0, | ||||||
|  |             y: 0, | ||||||
|  |             x2: 0, | ||||||
|  |             y2: 1, | ||||||
|  |             colorStops: [ | ||||||
|  |               { | ||||||
|  |                 offset: 0, | ||||||
|  |                 color: 'rgba(75, 150, 255, 0.3)' | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 offset: 1, | ||||||
|  |                 color: 'rgba(75, 150, 255, 0.1)' | ||||||
|  |               } | ||||||
|  |             ] | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         markPoint: { | ||||||
|  |           symbol: 'circle', | ||||||
|  |           symbolSize: 10, | ||||||
|  |           itemStyle: { | ||||||
|  |             color: '#4B96FF' | ||||||
|  |           }, | ||||||
|  |           data: [ | ||||||
|  |             { coord: ['2025', 850] } | ||||||
|  |           ] | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ] | ||||||
|  |   }; | ||||||
|  |    | ||||||
|  |   chart.setOption(option); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // 监听窗口大小变化,重绘图表 | ||||||
|  | const handleResize = () => { | ||||||
|  |   if (chart) { | ||||||
|  |     chart.resize(); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
|   fetchRankingData(); |   getChartData(); | ||||||
|  |   window.addEventListener('resize', handleResize); | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | // 组件卸载时移除事件监听 | ||||||
|  | const onUnmounted = () => { | ||||||
|  |   window.removeEventListener('resize', handleResize); | ||||||
|  |   if (chart) { | ||||||
|  |     chart.dispose(); | ||||||
|  |     chart = null; | ||||||
|  |   } | ||||||
|  | }; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <div class="high-school-ranking"> |   <div class="student-distribution"> | ||||||
|     <div class="ranking-list"> |     <div class="chart-header"> | ||||||
|       <div class="ranking-header"> |       <div class="filters"> | ||||||
|         <div class="header-item rank">排名</div> |         <div class="filter-item"> | ||||||
|         <div class="header-item school">学校名称</div> |           <el-select v-model="selectedYear" size="small" class="filter-select" @change="handleYearChange"> | ||||||
|         <div class="header-item count">用户数</div> |             <el-option | ||||||
|       </div> |               v-for="item in yearOptions" | ||||||
|       <div class="ranking-body"> |               :key="item.value" | ||||||
|         <div v-for="(item, index) in rankingData" :key="index" class="ranking-item"> |               :label="item.label" | ||||||
|           <div class="item-rank" :class="{ 'top-rank': item.rank <= 3 }"> |               :value="item.value" | ||||||
|             <span class="rank-number">{{ item.rank }}</span> |             /> | ||||||
|           </div> |           </el-select> | ||||||
|           <div class="item-school">{{ item.school }}</div> |  | ||||||
|           <div class="item-count">{{ item.count }}人</div> |  | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  |     <div class="chart-container" ref="chartRef"></div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped lang="scss"> | <style scoped lang="scss"> | ||||||
| .high-school-ranking { | .student-distribution { | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   height: 100%; |   height: 100%; | ||||||
|   padding: 10px 0; |   position: relative; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .ranking-list { | .chart-header { | ||||||
|  |   position: absolute; | ||||||
|  |   right: 0; | ||||||
|  |   top: -35px; | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: space-between; | ||||||
|  |   align-items: center; | ||||||
|  |   margin-bottom: 15px; | ||||||
|  |    | ||||||
|  |   .filters { | ||||||
|  |     display: flex; | ||||||
|  |     gap: 10px; | ||||||
|  |      | ||||||
|  |     .filter-item { | ||||||
|  |       .filter-select { | ||||||
|  |         width: 100px; | ||||||
|  |          | ||||||
|  |         :deep(.el-input__wrapper) { | ||||||
|  |           background-color: #f5f7fa; | ||||||
|  |           box-shadow: none; | ||||||
|  |           border-radius: 4px; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         :deep(.el-input__inner) { | ||||||
|  |           font-size: 12px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .chart-container { | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   height: 100%; |   height: 100%; | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ranking-header { |  | ||||||
|   display: flex; |  | ||||||
|   padding: 10px 0; |  | ||||||
|   border-bottom: 1px solid #eee; |  | ||||||
|   font-weight: bold; |  | ||||||
|   color: #333; |  | ||||||
|   font-size: 14px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ranking-body { |  | ||||||
|   flex: 1; |  | ||||||
|   overflow-y: auto; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ranking-item { |  | ||||||
|   display: flex; |  | ||||||
|   padding: 12px 0; |  | ||||||
|   border-bottom: 1px solid #f5f5f5; |  | ||||||
|    |  | ||||||
|   &:last-child { |  | ||||||
|     border-bottom: none; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-item, .item-rank, .item-school, .item-count { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .rank { |  | ||||||
|   width: 60px; |  | ||||||
|   justify-content: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .school { |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .count { |  | ||||||
|   width: 80px; |  | ||||||
|   justify-content: flex-end; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .item-rank { |  | ||||||
|   width: 60px; |  | ||||||
|   justify-content: center; |  | ||||||
|    |  | ||||||
|   .rank-number { |  | ||||||
|     display: flex; |  | ||||||
|     align-items: center; |  | ||||||
|     justify-content: center; |  | ||||||
|     width: 24px; |  | ||||||
|     height: 24px; |  | ||||||
|     border-radius: 50%; |  | ||||||
|     background-color: #f5f5f5; |  | ||||||
|     font-size: 14px; |  | ||||||
|     color: #666; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   &.top-rank { |  | ||||||
|     .rank-number { |  | ||||||
|       background-color: #4B96FF; |  | ||||||
|       color: #fff; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     &:nth-child(1) .rank-number { |  | ||||||
|       background-color: #f5222d; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     &:nth-child(2) .rank-number { |  | ||||||
|       background-color: #fa8c16; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     &:nth-child(3) .rank-number { |  | ||||||
|       background-color: #52c41a; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .item-school { |  | ||||||
|   flex: 1; |  | ||||||
|   font-size: 14px; |  | ||||||
|   color: #333; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .item-count { |  | ||||||
|   width: 80px; |  | ||||||
|   justify-content: flex-end; |  | ||||||
|   font-size: 14px; |  | ||||||
|   color: #4B96FF; |  | ||||||
|   font-weight: bold; |  | ||||||
| } | } | ||||||
| </style>  | </style>  | ||||||
|  | @ -1,45 +1,213 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { ref, onMounted } from 'vue'; | import { ref, onMounted } from "vue"; | ||||||
| import { majorRanking } from "@/api"; | import { majorRanking, highSchoolRanking } from "@/api"; | ||||||
|  | import top1Img from "@/assets/img/zheke/top1.png"; | ||||||
|  | import top2Img from "@/assets/img/zheke/top2.png"; | ||||||
|  | import top3Img from "@/assets/img/zheke/top3.png"; | ||||||
|  | import top4Img from "@/assets/img/zheke/top4.png"; | ||||||
| 
 | 
 | ||||||
| interface RankingItem { | interface RankingItem { | ||||||
|  |   rank: number; | ||||||
|  |   name: string; | ||||||
|  |   count: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface MajorRankingItem { | ||||||
|   rank: number; |   rank: number; | ||||||
|   major: string; |   major: string; | ||||||
|   count: number; |   count: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | interface SchoolRankingItem { | ||||||
|  |   rank: number; | ||||||
|  |   school: string; | ||||||
|  |   count: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // 排行数据 | // 排行数据 | ||||||
| const rankingData = ref<RankingItem[]>([]); | const rankingData = ref<RankingItem[]>([]); | ||||||
| 
 | 
 | ||||||
|  | // 排行类型 | ||||||
|  | const rankingTypes = [ | ||||||
|  |   { label: "用户意向专业排行", value: "major" }, | ||||||
|  |   { label: "用户生源高中排行", value: "school" }, | ||||||
|  | ]; | ||||||
|  | const selectedRankingType = ref("major"); | ||||||
|  | 
 | ||||||
| // 获取排行数据 | // 获取排行数据 | ||||||
| const fetchRankingData = () => { | const fetchRankingData = () => { | ||||||
|   majorRanking().then(res => { |   if (selectedRankingType.value === "major") { | ||||||
|  |     majorRanking().then((res) => { | ||||||
|       if (res.success) { |       if (res.success) { | ||||||
|       rankingData.value = res.data; |         // 转换数据格式 | ||||||
|  |         rankingData.value = res.data.map((item: MajorRankingItem) => ({ | ||||||
|  |           rank: item.rank, | ||||||
|  |           name: item.major, | ||||||
|  |           count: item.count, | ||||||
|  |         })); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |   } else { | ||||||
|  |     highSchoolRanking().then((res) => { | ||||||
|  |       if (res.success) { | ||||||
|  |         // 转换数据格式 | ||||||
|  |         rankingData.value = res.data.map((item: SchoolRankingItem) => ({ | ||||||
|  |           rank: item.rank, | ||||||
|  |           name: item.school, | ||||||
|  |           count: item.count, | ||||||
|  |         })); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // 处理排行类型变化 | ||||||
|  | const handleRankingTypeChange = () => { | ||||||
|  |   fetchRankingData(); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // 计算最大用户数,用于进度条百分比计算 | ||||||
|  | const maxCount = ref(0); | ||||||
|  | const getProgressWidth = (count: number) => { | ||||||
|  |   if (maxCount.value === 0) return "0%"; | ||||||
|  |   return `${(count / maxCount.value) * 100}%`; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // 监听数据变化,计算最大值 | ||||||
|  | const updateMaxCount = () => { | ||||||
|  |   if (rankingData.value.length > 0) { | ||||||
|  |     maxCount.value = Math.max(...rankingData.value.map((item) => item.count)); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // 模拟数据 | ||||||
|  | const initMockData = () => { | ||||||
|  |   const mockData = []; | ||||||
|  |   for (let i = 1; i <= 10; i++) { | ||||||
|  |     mockData.push({ | ||||||
|  |       rank: i, | ||||||
|  |       name: | ||||||
|  |         selectedRankingType.value === "major" ? "视觉传达设计" : "视觉传达设计", | ||||||
|  |       count: 3954, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |   rankingData.value = mockData; | ||||||
|  |   updateMaxCount(); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // 年份筛选选项 | ||||||
|  | const yearOptions = ref([{ label: "年份筛选", value: "all" }]); | ||||||
|  | 
 | ||||||
|  | // 学院筛选 | ||||||
|  | const selectedYear = ref("all"); | ||||||
|  | 
 | ||||||
|  | // 处理年份筛选变化 | ||||||
|  | const handleYearChange = () => { | ||||||
|  |   console.log("年份筛选"); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // 获取排名图片 | ||||||
|  | const getRankImage = (rank: number) => { | ||||||
|  |   switch (rank) { | ||||||
|  |     case 1: return top1Img; | ||||||
|  |     case 2: return top2Img; | ||||||
|  |     case 3: return top3Img; | ||||||
|  |     default: return top4Img; | ||||||
|  |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
|  |   // 获取真实数据 | ||||||
|   fetchRankingData(); |   fetchRankingData(); | ||||||
|  | 
 | ||||||
|  |   // 如果没有真实数据,使用模拟数据 | ||||||
|  |   setTimeout(() => { | ||||||
|  |     if (rankingData.value.length === 0) { | ||||||
|  |       initMockData(); | ||||||
|  |     } else { | ||||||
|  |       updateMaxCount(); | ||||||
|  |     } | ||||||
|  |   }, 500); | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <div class="major-ranking"> |   <div class="ranking-container"> | ||||||
|  |     <!-- 排行类型切换 --> | ||||||
|  |     <div class="ranking-tabs"> | ||||||
|  |       <div | ||||||
|  |         v-for="type in rankingTypes" | ||||||
|  |         :key="type.value" | ||||||
|  |         class="tab-item" | ||||||
|  |         :class="{ active: selectedRankingType === type.value }" | ||||||
|  |         @click=" | ||||||
|  |           selectedRankingType = type.value; | ||||||
|  |           handleRankingTypeChange(); | ||||||
|  |         " | ||||||
|  |       > | ||||||
|  |         {{ type.label }} | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <!-- 排行榜内容 --> | ||||||
|     <div class="ranking-list"> |     <div class="ranking-list"> | ||||||
|       <div class="ranking-header"> |       <!-- 标题装饰 --> | ||||||
|         <div class="header-item rank">排名</div> |       <div class="ranking-title"> | ||||||
|         <div class="header-item major">专业名称</div> |         <img class="title-bg" src="@/assets/img/zheke/title-bg.png" alt="" /> | ||||||
|         <div class="header-item count">用户数</div> |         <img | ||||||
|  |           class="title-line" | ||||||
|  |           src="@/assets/img/zheke/title-line.png" | ||||||
|  |           alt="" | ||||||
|  |         /> | ||||||
|  |         <div class="title-inner"> | ||||||
|  |           {{ | ||||||
|  |             selectedRankingType === "major" | ||||||
|  |               ? "用户意向专业排行" | ||||||
|  |               : "用户生源高中排行" | ||||||
|  |           }} | ||||||
|         </div> |         </div> | ||||||
|       <div class="ranking-body"> |         <!-- 学院筛选 --> | ||||||
|         <div v-for="(item, index) in rankingData" :key="index" class="ranking-item"> |         <div class="filter-item"> | ||||||
|           <div class="item-rank" :class="{ 'top-rank': item.rank <= 3 }"> |           <el-select | ||||||
|             <span class="rank-number">{{ item.rank }}</span> |             v-model="selectedYear" | ||||||
|  |             size="small" | ||||||
|  |             class="filter-select" | ||||||
|  |             @change="handleYearChange" | ||||||
|  |           > | ||||||
|  |             <el-option | ||||||
|  |               v-for="item in yearOptions" | ||||||
|  |               :key="item.value" | ||||||
|  |               :label="item.label" | ||||||
|  |               :value="item.value" | ||||||
|  |             /> | ||||||
|  |           </el-select> | ||||||
|         </div> |         </div> | ||||||
|           <div class="item-major">{{ item.major }}</div> |       </div> | ||||||
|           <div class="item-count">{{ item.count }}人</div> |       <div | ||||||
|  |         v-for="(item, index) in rankingData" | ||||||
|  |         :key="index" | ||||||
|  |         class="ranking-item" | ||||||
|  |       > | ||||||
|  |         <div class="item-content"> | ||||||
|  |           <div | ||||||
|  |             class="rank-badge" | ||||||
|  |             :class="`topItem-${item.rank <= 3 ? item.rank : 'other'}`" | ||||||
|  |           > | ||||||
|  |             <img  | ||||||
|  |               :src="getRankImage(item.rank)"  | ||||||
|  |               :alt="`top${item.rank}`" | ||||||
|  |               class="rank-bg" | ||||||
|  |             /> | ||||||
|  |             <span class="rank-text">TOP {{ item.rank }}</span> | ||||||
|  |           </div> | ||||||
|  |           <div class="item-name">{{ item.name }}</div> | ||||||
|  |           <div class="item-count">{{ item.count.toLocaleString() }}人</div> | ||||||
|  |         </div> | ||||||
|  |         <div class="progress-bar"> | ||||||
|  |           <div | ||||||
|  |             class="progress-inner" | ||||||
|  |             :style="{ width: getProgressWidth(item.count) }" | ||||||
|  |           ></div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  | @ -47,112 +215,181 @@ onMounted(() => { | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped lang="scss"> | <style scoped lang="scss"> | ||||||
| .major-ranking { | .ranking-container { | ||||||
|   width: 100%; |  | ||||||
|   height: 100%; |  | ||||||
|   padding: 10px 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ranking-list { |  | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   height: 100%; |   height: 100%; | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .ranking-header { | .ranking-title { | ||||||
|   display: flex; |   height: 33px; | ||||||
|   padding: 10px 0; |   line-height: 33px; | ||||||
|   border-bottom: 1px solid #eee; |   width: 100%; | ||||||
|   font-weight: bold; |  | ||||||
|   color: #333; |   color: #333; | ||||||
|   font-size: 14px; |   text-align: left; | ||||||
|  |   position: relative; | ||||||
|  |   display: flex; | ||||||
|  |   align-items: center; | ||||||
|  |   padding-left: 41px; | ||||||
|  |   background: transparent; | ||||||
|  |   font-weight: bold; | ||||||
|  |   font-size: 20px; | ||||||
|  |   margin-bottom: 20px; | ||||||
|  | 
 | ||||||
|  |   .title-bg { | ||||||
|  |     height: 33px; | ||||||
|  |     position: absolute; | ||||||
|  |     left: 0; | ||||||
|  |     top: 0; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| .ranking-body { |   .title-line { | ||||||
|  |     height: 2px; | ||||||
|  |     position: absolute; | ||||||
|  |     left: 0; | ||||||
|  |     bottom: 2px; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .title-inner { | ||||||
|  |     font-weight: 600; | ||||||
|  |     color: #393d44; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .filter-item { | ||||||
|  |     position: absolute; | ||||||
|  |     right: 0; | ||||||
|  |     bottom: 5px; | ||||||
|  |     font-weight: normal; | ||||||
|  |     .filter-select { | ||||||
|  |       width: 100px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .ranking-tabs { | ||||||
|  |   display: flex; | ||||||
|  |   border-bottom: 1px solid #e6e9f0; | ||||||
|  |   margin-bottom: 10px; | ||||||
|  | 
 | ||||||
|  |   .tab-item { | ||||||
|  |     padding: 10px 15px; | ||||||
|  |     font-size: 14px; | ||||||
|  |     color: #666; | ||||||
|  |     cursor: pointer; | ||||||
|  |     position: relative; | ||||||
|  | 
 | ||||||
|  |     &.active { | ||||||
|  |       color: #4b96ff; | ||||||
|  |       font-weight: 500; | ||||||
|  | 
 | ||||||
|  |       &::after { | ||||||
|  |         content: ""; | ||||||
|  |         position: absolute; | ||||||
|  |         bottom: -1px; | ||||||
|  |         left: 0; | ||||||
|  |         width: 100%; | ||||||
|  |         height: 2px; | ||||||
|  |         background-color: #4b96ff; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .ranking-list { | ||||||
|   flex: 1; |   flex: 1; | ||||||
|   overflow-y: auto; |   overflow-y: auto; | ||||||
|  |   padding: 5px 0; | ||||||
|  | 
 | ||||||
|  |   /* 隐藏滚动条 */ | ||||||
|  |   &::-webkit-scrollbar { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /* Firefox */ | ||||||
|  |   scrollbar-width: none; | ||||||
|  | 
 | ||||||
|  |   /* IE */ | ||||||
|  |   -ms-overflow-style: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .ranking-item { | .ranking-item { | ||||||
|   display: flex; |   display: flex; | ||||||
|   padding: 12px 0; |   flex-direction: column; | ||||||
|   border-bottom: 1px solid #f5f5f5; |   margin-bottom: 23px; | ||||||
|  |   // border-bottom: 1px solid #f0f2f5; | ||||||
| 
 | 
 | ||||||
|   &:last-child { |   &:last-child { | ||||||
|     border-bottom: none; |     border-bottom: none; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .header-item, .item-rank, .item-major, .item-count { | .rank-badge { | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .rank { |  | ||||||
|   width: 60px; |   width: 60px; | ||||||
|   justify-content: center; |   height: 30px; | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .major { |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .count { |  | ||||||
|   width: 80px; |  | ||||||
|   justify-content: flex-end; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .item-rank { |  | ||||||
|   width: 60px; |  | ||||||
|   justify-content: center; |  | ||||||
|    |  | ||||||
|   .rank-number { |  | ||||||
|   display: flex; |   display: flex; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   justify-content: center; |   justify-content: center; | ||||||
|     width: 24px; |   font-size: 16px; | ||||||
|     height: 24px; |   font-weight: bold; | ||||||
|     border-radius: 50%; |   position: relative;  | ||||||
|     background-color: #f5f5f5; | 
 | ||||||
|     font-size: 14px; |   .rank-bg { | ||||||
|     color: #666; |     position: absolute;  | ||||||
|  |     top: 0; | ||||||
|  |     left: 0; | ||||||
|  |     width: 100%; | ||||||
|  |     height: 100%; | ||||||
|  |     border-radius: 2px; | ||||||
|  |     object-fit: cover; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &.top-rank { |   .rank-text { | ||||||
|     .rank-number { |     position: relative; // 确保文字在图片上方 | ||||||
|       background-color: #4B96FF; |     z-index: 1; | ||||||
|       color: #fff; |     color: white; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|     &:nth-child(1) .rank-number { |   &.topItem-other { | ||||||
|       background-color: #f5222d; |     .rank-text { | ||||||
|     } |       color: #545966; | ||||||
|      |  | ||||||
|     &:nth-child(2) .rank-number { |  | ||||||
|       background-color: #fa8c16; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     &:nth-child(3) .rank-number { |  | ||||||
|       background-color: #52c41a; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .item-major { | .item-content { | ||||||
|   flex: 1; |   flex: 1; | ||||||
|   font-size: 14px; |   display: flex; | ||||||
|  |   align-items: center; | ||||||
|  |   margin-bottom: 10px; // 添加固定的底部间距 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .item-name { | ||||||
|  |   font-size: 16px; | ||||||
|   color: #333; |   color: #333; | ||||||
|   white-space: nowrap; |   white-space: nowrap; | ||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|   text-overflow: ellipsis; |   text-overflow: ellipsis; | ||||||
|  |   margin-left: 27px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .item-count { | .item-count { | ||||||
|   width: 80px; |   font-size: 16px; | ||||||
|   justify-content: flex-end; |   color: #2D2E30; | ||||||
|   font-size: 14px; |   font-weight: 400; | ||||||
|   color: #4B96FF; |   margin-left: auto; | ||||||
|   font-weight: bold; | } | ||||||
|  | 
 | ||||||
|  | .progress-bar { | ||||||
|  |   height: 6px; | ||||||
|  |   background-color: #f0f2f5; | ||||||
|  |   border-radius: 3px; | ||||||
|  |   overflow: hidden; | ||||||
|  | 
 | ||||||
|  |   .progress-inner { | ||||||
|  |     height: 100%; | ||||||
|  |     background-color: #4b96ff; | ||||||
|  |     border-radius: 3px; | ||||||
|  |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  | @ -1,155 +0,0 @@ | ||||||
| <script setup lang="ts"> |  | ||||||
| import { ref, onMounted } from 'vue'; |  | ||||||
| import { highSchoolRanking } from "@/api"; |  | ||||||
| 
 |  | ||||||
| interface RankingItem { |  | ||||||
|   rank: number; |  | ||||||
|   school: string; |  | ||||||
|   count: number; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 排行数据 |  | ||||||
| const rankingData = ref<RankingItem[]>([]); |  | ||||||
| 
 |  | ||||||
| // 获取排行数据 |  | ||||||
| const fetchRankingData = () => { |  | ||||||
|   highSchoolRanking().then(res => { |  | ||||||
|     if (res.success) { |  | ||||||
|       rankingData.value = res.data; |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| onMounted(() => { |  | ||||||
|   fetchRankingData(); |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <template> |  | ||||||
|   <div class="high-school-ranking"> |  | ||||||
|     <div class="ranking-list"> |  | ||||||
|       <div class="ranking-header"> |  | ||||||
|         <div class="header-item rank">排名</div> |  | ||||||
|         <div class="header-item school">学校名称</div> |  | ||||||
|         <div class="header-item count">用户数</div> |  | ||||||
|       </div> |  | ||||||
|       <div class="ranking-body"> |  | ||||||
|         <div v-for="(item, index) in rankingData" :key="index" class="ranking-item"> |  | ||||||
|           <div class="item-rank" :class="{ 'top-rank': item.rank <= 3 }"> |  | ||||||
|             <span class="rank-number">{{ item.rank }}</span> |  | ||||||
|           </div> |  | ||||||
|           <div class="item-school">{{ item.school }}</div> |  | ||||||
|           <div class="item-count">{{ item.count }}人</div> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <style scoped lang="scss"> |  | ||||||
| .high-school-ranking { |  | ||||||
|   width: 100%; |  | ||||||
|   height: 100%; |  | ||||||
|   padding: 10px 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ranking-list { |  | ||||||
|   width: 100%; |  | ||||||
|   height: 100%; |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ranking-header { |  | ||||||
|   display: flex; |  | ||||||
|   padding: 10px 0; |  | ||||||
|   border-bottom: 1px solid #eee; |  | ||||||
|   font-weight: bold; |  | ||||||
|   color: #333; |  | ||||||
|   font-size: 14px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ranking-body { |  | ||||||
|   flex: 1; |  | ||||||
|   overflow-y: auto; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ranking-item { |  | ||||||
|   display: flex; |  | ||||||
|   padding: 12px 0; |  | ||||||
|   border-bottom: 1px solid #f5f5f5; |  | ||||||
|    |  | ||||||
|   &:last-child { |  | ||||||
|     border-bottom: none; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-item, .item-rank, .item-school, .item-count { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .rank { |  | ||||||
|   width: 60px; |  | ||||||
|   justify-content: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .school { |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .count { |  | ||||||
|   width: 80px; |  | ||||||
|   justify-content: flex-end; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .item-rank { |  | ||||||
|   width: 60px; |  | ||||||
|   justify-content: center; |  | ||||||
|    |  | ||||||
|   .rank-number { |  | ||||||
|     display: flex; |  | ||||||
|     align-items: center; |  | ||||||
|     justify-content: center; |  | ||||||
|     width: 24px; |  | ||||||
|     height: 24px; |  | ||||||
|     border-radius: 50%; |  | ||||||
|     background-color: #f5f5f5; |  | ||||||
|     font-size: 14px; |  | ||||||
|     color: #666; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   &.top-rank { |  | ||||||
|     .rank-number { |  | ||||||
|       background-color: #4B96FF; |  | ||||||
|       color: #fff; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     &:nth-child(1) .rank-number { |  | ||||||
|       background-color: #f5222d; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     &:nth-child(2) .rank-number { |  | ||||||
|       background-color: #fa8c16; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     &:nth-child(3) .rank-number { |  | ||||||
|       background-color: #52c41a; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .item-school { |  | ||||||
|   flex: 1; |  | ||||||
|   font-size: 14px; |  | ||||||
|   color: #333; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .item-count { |  | ||||||
|   width: 80px; |  | ||||||
|   justify-content: flex-end; |  | ||||||
|   font-size: 14px; |  | ||||||
|   color: #4B96FF; |  | ||||||
|   font-weight: bold; |  | ||||||
| } |  | ||||||
| </style>  |  | ||||||
|  | @ -1,119 +0,0 @@ | ||||||
| <script setup lang="ts"> |  | ||||||
| import { ref, onMounted } from 'vue'; |  | ||||||
| import { commonQuestions } from "@/api"; |  | ||||||
| 
 |  | ||||||
| interface QuestionItem { |  | ||||||
|   id: number; |  | ||||||
|   question: string; |  | ||||||
|   count: number; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 问题数据 |  | ||||||
| const questionData = ref<QuestionItem[]>([]); |  | ||||||
| 
 |  | ||||||
| // 获取问题数据 |  | ||||||
| const fetchQuestionData = () => { |  | ||||||
|   commonQuestions().then(res => { |  | ||||||
|     if (res.success) { |  | ||||||
|       questionData.value = res.data; |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| onMounted(() => { |  | ||||||
|   fetchQuestionData(); |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <template> |  | ||||||
|   <div class="common-questions"> |  | ||||||
|     <div class="question-list"> |  | ||||||
|       <div v-for="(item, index) in questionData" :key="index" class="question-item"> |  | ||||||
|         <div class="question-rank">{{ index + 1 }}</div> |  | ||||||
|         <div class="question-content"> |  | ||||||
|           <div class="question-text">{{ item.question }}</div> |  | ||||||
|           <div class="question-count"> |  | ||||||
|             <span class="count-value">{{ item.count }}</span> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <style scoped lang="scss"> |  | ||||||
| .common-questions { |  | ||||||
|   width: 100%; |  | ||||||
|   height: 100%; |  | ||||||
|   padding: 10px 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .question-list { |  | ||||||
|   width: 100%; |  | ||||||
|   height: 100%; |  | ||||||
|   overflow-y: auto; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .question-item { |  | ||||||
|   display: flex; |  | ||||||
|   padding: 15px 0; |  | ||||||
|   border-bottom: 1px solid #f5f5f5; |  | ||||||
|    |  | ||||||
|   &:last-child { |  | ||||||
|     border-bottom: none; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .question-rank { |  | ||||||
|   width: 24px; |  | ||||||
|   height: 24px; |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: center; |  | ||||||
|   background-color: #f5f5f5; |  | ||||||
|   border-radius: 50%; |  | ||||||
|   margin-right: 15px; |  | ||||||
|   font-size: 14px; |  | ||||||
|   color: #666; |  | ||||||
|   flex-shrink: 0; |  | ||||||
|    |  | ||||||
|   // 前三名使用不同颜色 |  | ||||||
|   .question-item:nth-child(1) & { |  | ||||||
|     background-color: #f5222d; |  | ||||||
|     color: #fff; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .question-item:nth-child(2) & { |  | ||||||
|     background-color: #fa8c16; |  | ||||||
|     color: #fff; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   .question-item:nth-child(3) & { |  | ||||||
|     background-color: #52c41a; |  | ||||||
|     color: #fff; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .question-content { |  | ||||||
|   flex: 1; |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .question-text { |  | ||||||
|   font-size: 14px; |  | ||||||
|   color: #333; |  | ||||||
|   line-height: 1.5; |  | ||||||
|   margin-bottom: 5px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .question-count { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: #999; |  | ||||||
|    |  | ||||||
|   .count-value { |  | ||||||
|     color: #4B96FF; |  | ||||||
|     font-weight: bold; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style>  |  | ||||||
|  | @ -1,158 +0,0 @@ | ||||||
| <script setup lang="ts"> |  | ||||||
| import { ref, onMounted } from 'vue'; |  | ||||||
| import { majorRanking } from "@/api"; |  | ||||||
| 
 |  | ||||||
| interface RankingItem { |  | ||||||
|   rank: number; |  | ||||||
|   major: string; |  | ||||||
|   count: number; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 排行数据 |  | ||||||
| const rankingData = ref<RankingItem[]>([]); |  | ||||||
| 
 |  | ||||||
| // 获取排行数据 |  | ||||||
| const fetchRankingData = () => { |  | ||||||
|   majorRanking().then(res => { |  | ||||||
|     if (res.success) { |  | ||||||
|       rankingData.value = res.data; |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| onMounted(() => { |  | ||||||
|   fetchRankingData(); |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <template> |  | ||||||
|   <div class="major-ranking"> |  | ||||||
|     <div class="ranking-list"> |  | ||||||
|       <div class="ranking-header"> |  | ||||||
|         <div class="header-item rank">排名</div> |  | ||||||
|         <div class="header-item major">专业名称</div> |  | ||||||
|         <div class="header-item count">用户数</div> |  | ||||||
|       </div> |  | ||||||
|       <div class="ranking-body"> |  | ||||||
|         <div v-for="(item, index) in rankingData" :key="index" class="ranking-item"> |  | ||||||
|           <div class="item-rank" :class="{ 'top-rank': item.rank <= 3 }"> |  | ||||||
|             <span class="rank-number">{{ item.rank }}</span> |  | ||||||
|           </div> |  | ||||||
|           <div class="item-major">{{ item.major }}</div> |  | ||||||
|           <div class="item-count">{{ item.count }}人</div> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <style scoped lang="scss"> |  | ||||||
| .major-ranking { |  | ||||||
|   width: 100%; |  | ||||||
|   height: 100%; |  | ||||||
|   padding: 10px 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ranking-list { |  | ||||||
|   width: 100%; |  | ||||||
|   height: 100%; |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ranking-header { |  | ||||||
|   display: flex; |  | ||||||
|   padding: 10px 0; |  | ||||||
|   border-bottom: 1px solid #eee; |  | ||||||
|   font-weight: bold; |  | ||||||
|   color: #333; |  | ||||||
|   font-size: 14px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ranking-body { |  | ||||||
|   flex: 1; |  | ||||||
|   overflow-y: auto; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ranking-item { |  | ||||||
|   display: flex; |  | ||||||
|   padding: 12px 0; |  | ||||||
|   border-bottom: 1px solid #f5f5f5; |  | ||||||
|    |  | ||||||
|   &:last-child { |  | ||||||
|     border-bottom: none; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-item, .item-rank, .item-major, .item-count { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .rank { |  | ||||||
|   width: 60px; |  | ||||||
|   justify-content: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .major { |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .count { |  | ||||||
|   width: 80px; |  | ||||||
|   justify-content: flex-end; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .item-rank { |  | ||||||
|   width: 60px; |  | ||||||
|   justify-content: center; |  | ||||||
|    |  | ||||||
|   .rank-number { |  | ||||||
|     display: flex; |  | ||||||
|     align-items: center; |  | ||||||
|     justify-content: center; |  | ||||||
|     width: 24px; |  | ||||||
|     height: 24px; |  | ||||||
|     border-radius: 50%; |  | ||||||
|     background-color: #f5f5f5; |  | ||||||
|     font-size: 14px; |  | ||||||
|     color: #666; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   &.top-rank { |  | ||||||
|     .rank-number { |  | ||||||
|       background-color: #4B96FF; |  | ||||||
|       color: #fff; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     &:nth-child(1) .rank-number { |  | ||||||
|       background-color: #f5222d; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     &:nth-child(2) .rank-number { |  | ||||||
|       background-color: #fa8c16; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     &:nth-child(3) .rank-number { |  | ||||||
|       background-color: #52c41a; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .item-major { |  | ||||||
|   flex: 1; |  | ||||||
|   font-size: 14px; |  | ||||||
|   color: #333; |  | ||||||
|   white-space: nowrap; |  | ||||||
|   overflow: hidden; |  | ||||||
|   text-overflow: ellipsis; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .item-count { |  | ||||||
|   width: 80px; |  | ||||||
|   justify-content: flex-end; |  | ||||||
|   font-size: 14px; |  | ||||||
|   color: #4B96FF; |  | ||||||
|   font-weight: bold; |  | ||||||
| } |  | ||||||
| </style>  |  | ||||||
|  | @ -1,178 +0,0 @@ | ||||||
| <script setup lang="ts"> |  | ||||||
| import { ref, onMounted } from 'vue'; |  | ||||||
| import * as echarts from 'echarts'; |  | ||||||
| import { averageDuration } from "@/api"; |  | ||||||
| 
 |  | ||||||
| const chartRef = ref<HTMLElement | null>(null); |  | ||||||
| let chart: echarts.ECharts | null = null; |  | ||||||
| 
 |  | ||||||
| // 过滤选项 |  | ||||||
| const filterOptions = [ |  | ||||||
|   { value: 'all', label: '全部来源' }, |  | ||||||
|   { value: 'web', label: '网页端' }, |  | ||||||
|   { value: 'mobile', label: '移动端' } |  | ||||||
| ]; |  | ||||||
| const selectedFilter = ref(filterOptions[0].value); |  | ||||||
| 
 |  | ||||||
| // 图表数据 |  | ||||||
| const chartData = ref({ |  | ||||||
|   days: [] as string[], |  | ||||||
|   durations: [] as number[] |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| // 获取图表数据 |  | ||||||
| const getChartData = () => { |  | ||||||
|   averageDuration().then(res => { |  | ||||||
|     if (res.success) { |  | ||||||
|       chartData.value = res.data; |  | ||||||
|       initChart(); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const initChart = () => { |  | ||||||
|   if (!chartRef.value) return; |  | ||||||
|    |  | ||||||
|   chart = echarts.init(chartRef.value); |  | ||||||
|    |  | ||||||
|   const option = { |  | ||||||
|     grid: { |  | ||||||
|       top: '10%', |  | ||||||
|       left: '3%', |  | ||||||
|       right: '4%', |  | ||||||
|       bottom: '15%', |  | ||||||
|       containLabel: true |  | ||||||
|     }, |  | ||||||
|     tooltip: { |  | ||||||
|       trigger: 'axis', |  | ||||||
|       axisPointer: { |  | ||||||
|         type: 'shadow' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     xAxis: { |  | ||||||
|       type: 'category', |  | ||||||
|       data: chartData.value.days, |  | ||||||
|       axisLine: { |  | ||||||
|         lineStyle: { |  | ||||||
|           color: '#E0E0E0' |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       axisLabel: { |  | ||||||
|         color: '#999999' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     yAxis: { |  | ||||||
|       type: 'value', |  | ||||||
|       name: '分钟', |  | ||||||
|       nameTextStyle: { |  | ||||||
|         color: '#999999' |  | ||||||
|       }, |  | ||||||
|       min: 0, |  | ||||||
|       max: 100, |  | ||||||
|       interval: 20, |  | ||||||
|       splitLine: { |  | ||||||
|         lineStyle: { |  | ||||||
|           type: 'dashed', |  | ||||||
|           color: '#E0E0E0' |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       axisLabel: { |  | ||||||
|         color: '#999999' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     series: [ |  | ||||||
|       { |  | ||||||
|         name: '平均时长', |  | ||||||
|         type: 'bar', |  | ||||||
|         barWidth: '40%', |  | ||||||
|         data: chartData.value.durations, |  | ||||||
|         itemStyle: { |  | ||||||
|           color: '#4B96FF' |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     ] |  | ||||||
|   }; |  | ||||||
|    |  | ||||||
|   chart.setOption(option); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // 监听窗口大小变化,重绘图表 |  | ||||||
| const handleResize = () => { |  | ||||||
|   if (chart) { |  | ||||||
|     chart.resize(); |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // 处理过滤器变化 |  | ||||||
| const handleFilterChange = () => { |  | ||||||
|   // 这里可以根据选择的过滤条件重新获取数据并更新图表 |  | ||||||
|   console.log('Filter changed to:', selectedFilter.value); |  | ||||||
|   getChartData(); // 简单实现,实际应该传入过滤参数 |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| onMounted(() => { |  | ||||||
|   getChartData(); |  | ||||||
|   window.addEventListener('resize', handleResize); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| // 组件卸载时移除事件监听 |  | ||||||
| const onUnmounted = () => { |  | ||||||
|   window.removeEventListener('resize', handleResize); |  | ||||||
|   if (chart) { |  | ||||||
|     chart.dispose(); |  | ||||||
|     chart = null; |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <template> |  | ||||||
|   <div class="average-duration"> |  | ||||||
|     <div class="filter-container"> |  | ||||||
|       <div class="filter-label">数据来源:</div> |  | ||||||
|       <el-select v-model="selectedFilter" placeholder="选择来源" size="small" @change="handleFilterChange"> |  | ||||||
|         <el-option |  | ||||||
|           v-for="item in filterOptions" |  | ||||||
|           :key="item.value" |  | ||||||
|           :label="item.label" |  | ||||||
|           :value="item.value" |  | ||||||
|         /> |  | ||||||
|       </el-select> |  | ||||||
|     </div> |  | ||||||
|     <div class="chart-container" ref="chartRef"></div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <style scoped lang="scss"> |  | ||||||
| .average-duration { |  | ||||||
|   width: 100%; |  | ||||||
|   height: 100%; |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .filter-container { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   padding: 10px 0; |  | ||||||
|    |  | ||||||
|   .filter-label { |  | ||||||
|     font-size: 14px; |  | ||||||
|     color: #666; |  | ||||||
|     margin-right: 10px; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   :deep(.el-input__wrapper) { |  | ||||||
|     background-color: #f5f7fa; |  | ||||||
|     box-shadow: none; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   :deep(.el-select .el-input) { |  | ||||||
|     width: 120px; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .chart-container { |  | ||||||
|   flex: 1; |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
| </style>  |  | ||||||
|  | @ -6,17 +6,15 @@ const ContentHeader = defineAsyncComponent(() => import("./content-header.vue")) | ||||||
| // 导入左侧组件 | // 导入左侧组件 | ||||||
| const LeftTop = defineAsyncComponent(() => import("./LeftTop.vue")); | const LeftTop = defineAsyncComponent(() => import("./LeftTop.vue")); | ||||||
| const LeftCenter = defineAsyncComponent(() => import("./LeftCenter.vue")); | const LeftCenter = defineAsyncComponent(() => import("./LeftCenter.vue")); | ||||||
| const AverageDuration = defineAsyncComponent(() => import("./average-duration.vue")); | const LeftBottom = defineAsyncComponent(() => import("./LeftBottom.vue")); | ||||||
| const LeftBottom = defineAsyncComponent(() => import("./components/LeftBottom.vue")); | 
 | ||||||
| 
 | 
 | ||||||
| // 导入中间组件 | // 导入中间组件 | ||||||
| const CenterTop = defineAsyncComponent(() => import("./components/CenterTop.vue")); | const CenterTop = defineAsyncComponent(() => import("./components/CenterTop.vue")); | ||||||
| const CenterBottom = defineAsyncComponent(() => import("./components/CenterBottom.vue")); | const CenterBottom = defineAsyncComponent(() => import("./components/CenterBottom.vue")); | ||||||
| 
 | 
 | ||||||
| // 导入右侧组件 | // 导入右侧组件 | ||||||
| const RightTop = defineAsyncComponent(() => import("./components/RightTop.vue")); | const RightCenter = defineAsyncComponent(() => import("./RightCenter.vue")); | ||||||
| const RightCenter = defineAsyncComponent(() => import("./components/RightCenter.vue")); |  | ||||||
| const RightBottom = defineAsyncComponent(() => import("./components/RightBottom.vue")); |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  | @ -33,35 +31,24 @@ const RightBottom = defineAsyncComponent(() => import("./components/RightBottom. | ||||||
|           <LeftCenter /> |           <LeftCenter /> | ||||||
|         </ItemWrap> |         </ItemWrap> | ||||||
|          |          | ||||||
|         <!-- <ItemWrap title="学生用户高考年份统计" class="left-contetn-bottom module-item"> |         <ItemWrap title="学生用户高考年份统计" class="left-contetn-bottom module-item"> | ||||||
|           <LeftBottom /> |           <LeftBottom /> | ||||||
|         </ItemWrap> --> |         </ItemWrap> | ||||||
|       </div> |       </div> | ||||||
|        |        | ||||||
|       <!-- 中间内容区域 --> |       <!-- 中间内容区域 --> | ||||||
|       <div class="center-content"> |       <div class="center-content"> | ||||||
|         <ItemWrap class="module-item map-container"> |  | ||||||
|         <CenterTop /> |         <CenterTop /> | ||||||
|         </ItemWrap> |         <!-- <ItemWrap title="高频咨询问题" class="module-item"> | ||||||
|          |  | ||||||
|         <ItemWrap title="高频咨询问题" class="module-item"> |  | ||||||
|           <CenterBottom /> |           <CenterBottom /> | ||||||
|         </ItemWrap> |         </ItemWrap> --> | ||||||
|       </div> |       </div> | ||||||
|        |        | ||||||
|       <!-- 右侧内容区域 --> |       <!-- 右侧内容区域 --> | ||||||
|       <div class="right-content"> |       <div class="right-content"> | ||||||
|         <ItemWrap title="平均时长趋势" class="module-item"> |         <ItemWrap class="right-content-top module-item"> | ||||||
|           <RightTop /> |  | ||||||
|         </ItemWrap> |  | ||||||
|          |  | ||||||
|         <ItemWrap title="用户量属专业排行" class="module-item"> |  | ||||||
|           <RightCenter /> |           <RightCenter /> | ||||||
|         </ItemWrap> |         </ItemWrap> | ||||||
|          |  | ||||||
|         <ItemWrap title="常见咨询问题" class="module-item"> |  | ||||||
|           <RightBottom /> |  | ||||||
|         </ItemWrap> |  | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|  | @ -87,7 +74,7 @@ const RightBottom = defineAsyncComponent(() => import("./components/RightBottom. | ||||||
| .left-content, .right-content { | .left-content, .right-content { | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   width: 25%; |   width: 500px; | ||||||
|   gap: 10px; |   gap: 10px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -110,6 +97,10 @@ const RightBottom = defineAsyncComponent(() => import("./components/RightBottom. | ||||||
|   height: 240px; |   height: 240px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .right-content-top { | ||||||
|  |   height: 710px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // .module-item { | // .module-item { | ||||||
| //   height: 100%; | //   height: 100%; | ||||||
| //   flex: 1; | //   flex: 1; | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue