feat:用户意向专业排行

This commit is contained in:
JiXinHui 2025-08-15 15:56:13 +08:00
parent 1e347af4f6
commit c1f5e00dd5
13 changed files with 538 additions and 854 deletions

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

View File

@ -81,7 +81,7 @@ $item_title_content-height: calc(100% - 33px - 20px);
.item_title_content_def {
width: 100%;
height: 1px;
height: 100%;
background: #fff;
}
</style>

View File

@ -1,155 +1,221 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { highSchoolRanking } from "@/api";
import * as echarts from 'echarts';
import { studentDistribution } from "@/api";
interface RankingItem {
rank: number;
school: string;
count: number;
}
const chartRef = ref<HTMLElement | null>(null);
let chart: echarts.ECharts | null = null;
//
const rankingData = ref<RankingItem[]>([]);
//
const yearOptions = ref([
{ label: '年份筛选', value: 'all' }
]);
const selectedYear = ref('all');
//
const fetchRankingData = () => {
highSchoolRanking().then(res => {
//
const chartData = ref({
years: ['2022', '2023', '2024', '2025', '2026', '2027'],
values: [50, 200, 550, 850, 750, 900, 200]
});
//
const getChartData = () => {
studentDistribution().then(res => {
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(() => {
fetchRankingData();
getChartData();
window.addEventListener('resize', handleResize);
});
//
const onUnmounted = () => {
window.removeEventListener('resize', handleResize);
if (chart) {
chart.dispose();
chart = null;
}
};
</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 class="student-distribution">
<div class="chart-header">
<div class="filters">
<div class="filter-item">
<el-select 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>
<div class="chart-container" ref="chartRef"></div>
</div>
</template>
<style scoped lang="scss">
.high-school-ranking {
.student-distribution {
width: 100%;
height: 100%;
padding: 10px 0;
position: relative;
}
.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 {
.chart-header {
position: absolute;
right: 0;
top: -35px;
display: flex;
justify-content: space-between;
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;
margin-bottom: 15px;
.rank-number {
.filters {
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;
}
gap: 10px;
&:nth-child(1) .rank-number {
background-color: #f5222d;
}
&:nth-child(2) .rank-number {
background-color: #fa8c16;
}
&:nth-child(3) .rank-number {
background-color: #52c41a;
.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;
}
}
}
}
}
.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;
.chart-container {
width: 100%;
height: 100%;
}
</style>

View File

@ -1,45 +1,213 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { majorRanking } from "@/api";
import { ref, onMounted } from "vue";
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 {
rank: number;
name: string;
count: number;
}
interface MajorRankingItem {
rank: number;
major: string;
count: number;
}
interface SchoolRankingItem {
rank: number;
school: string;
count: number;
}
//
const rankingData = ref<RankingItem[]>([]);
//
const rankingTypes = [
{ label: "用户意向专业排行", value: "major" },
{ label: "用户生源高中排行", value: "school" },
];
const selectedRankingType = ref("major");
//
const fetchRankingData = () => {
majorRanking().then(res => {
if (res.success) {
rankingData.value = res.data;
}
});
if (selectedRankingType.value === "major") {
majorRanking().then((res) => {
if (res.success) {
//
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(() => {
//
fetchRankingData();
// 使
setTimeout(() => {
if (rankingData.value.length === 0) {
initMockData();
} else {
updateMaxCount();
}
}, 500);
});
</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 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 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="ranking-list">
<!-- 标题装饰 -->
<div class="ranking-title">
<img class="title-bg" src="@/assets/img/zheke/title-bg.png" alt="" />
<img
class="title-line"
src="@/assets/img/zheke/title-line.png"
alt=""
/>
<div class="title-inner">
{{
selectedRankingType === "major"
? "用户意向专业排行"
: "用户生源高中排行"
}}
</div>
<!-- 学院筛选 -->
<div class="filter-item">
<el-select
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
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-major">{{ item.major }}</div>
<div class="item-count">{{ item.count }}</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>
@ -47,112 +215,181 @@ onMounted(() => {
</template>
<style scoped lang="scss">
.major-ranking {
width: 100%;
height: 100%;
padding: 10px 0;
}
.ranking-list {
.ranking-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.ranking-header {
display: flex;
padding: 10px 0;
border-bottom: 1px solid #eee;
font-weight: bold;
.ranking-title {
height: 33px;
line-height: 33px;
width: 100%;
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;
}
.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-body {
.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;
overflow-y: auto;
padding: 5px 0;
/* 隐藏滚动条 */
&::-webkit-scrollbar {
display: none;
}
/* Firefox */
scrollbar-width: none;
/* IE */
-ms-overflow-style: none;
}
.ranking-item {
display: flex;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
flex-direction: column;
margin-bottom: 23px;
// border-bottom: 1px solid #f0f2f5;
&:last-child {
border-bottom: none;
}
}
.header-item, .item-rank, .item-major, .item-count {
.rank-badge {
width: 60px;
height: 30px;
display: flex;
align-items: center;
}
.rank {
width: 60px;
justify-content: center;
}
font-size: 16px;
font-weight: bold;
position: relative;
.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;
.rank-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 2px;
object-fit: cover;
}
&.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;
.rank-text {
position: relative; //
z-index: 1;
color: white;
}
&.topItem-other {
.rank-text {
color: #545966;
}
}
}
.item-major {
.item-content {
flex: 1;
font-size: 14px;
display: flex;
align-items: center;
margin-bottom: 10px; //
}
.item-name {
font-size: 16px;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 27px;
}
.item-count {
width: 80px;
justify-content: flex-end;
font-size: 14px;
color: #4B96FF;
font-weight: bold;
font-size: 16px;
color: #2D2E30;
font-weight: 400;
margin-left: auto;
}
</style>
.progress-bar {
height: 6px;
background-color: #f0f2f5;
border-radius: 3px;
overflow: hidden;
.progress-inner {
height: 100%;
background-color: #4b96ff;
border-radius: 3px;
}
}
</style>

View File

@ -159,13 +159,13 @@ onUnmounted(() => {
</div>
<div class="filter-item">
<el-select v-model="selectedTimeFilter" size="small" class="filter-select" @change="handleFilterChange">
<el-option
<el-option
v-for="item in timeFilters"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</div>
</div>
@ -196,13 +196,13 @@ onUnmounted(() => {
.filter-item {
.filter-select {
width: 120px;
:deep(.el-input__wrapper) {
background-color: #f5f7fa;
box-shadow: none;
:deep(.el-input__wrapper) {
background-color: #f5f7fa;
box-shadow: none;
border-radius: 4px;
}
}
:deep(.el-input__inner) {
font-size: 12px;
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -6,17 +6,15 @@ const ContentHeader = defineAsyncComponent(() => import("./content-header.vue"))
//
const LeftTop = defineAsyncComponent(() => import("./LeftTop.vue"));
const LeftCenter = defineAsyncComponent(() => import("./LeftCenter.vue"));
const AverageDuration = defineAsyncComponent(() => import("./average-duration.vue"));
const LeftBottom = defineAsyncComponent(() => import("./components/LeftBottom.vue"));
const LeftBottom = defineAsyncComponent(() => import("./LeftBottom.vue"));
//
const CenterTop = defineAsyncComponent(() => import("./components/CenterTop.vue"));
const CenterBottom = defineAsyncComponent(() => import("./components/CenterBottom.vue"));
//
const RightTop = defineAsyncComponent(() => import("./components/RightTop.vue"));
const RightCenter = defineAsyncComponent(() => import("./components/RightCenter.vue"));
const RightBottom = defineAsyncComponent(() => import("./components/RightBottom.vue"));
const RightCenter = defineAsyncComponent(() => import("./RightCenter.vue"));
</script>
<template>
@ -33,35 +31,24 @@ const RightBottom = defineAsyncComponent(() => import("./components/RightBottom.
<LeftCenter />
</ItemWrap>
<!-- <ItemWrap title="学生用户高考年份统计" class="left-contetn-bottom module-item">
<ItemWrap title="学生用户高考年份统计" class="left-contetn-bottom module-item">
<LeftBottom />
</ItemWrap> -->
</ItemWrap>
</div>
<!-- 中间内容区域 -->
<div class="center-content">
<ItemWrap class="module-item map-container">
<CenterTop />
</ItemWrap>
<ItemWrap title="高频咨询问题" class="module-item">
<CenterTop />
<!-- <ItemWrap title="高频咨询问题" class="module-item">
<CenterBottom />
</ItemWrap>
</ItemWrap> -->
</div>
<!-- 右侧内容区域 -->
<div class="right-content">
<ItemWrap title="平均时长趋势" class="module-item">
<RightTop />
</ItemWrap>
<ItemWrap title="用户量属专业排行" class="module-item">
<ItemWrap class="right-content-top module-item">
<RightCenter />
</ItemWrap>
<ItemWrap title="常见咨询问题" class="module-item">
<RightBottom />
</ItemWrap>
</div>
</div>
</div>
@ -87,7 +74,7 @@ const RightBottom = defineAsyncComponent(() => import("./components/RightBottom.
.left-content, .right-content {
display: flex;
flex-direction: column;
width: 25%;
width: 500px;
gap: 10px;
}
@ -110,6 +97,10 @@ const RightBottom = defineAsyncComponent(() => import("./components/RightBottom.
height: 240px;
}
.right-content-top {
height: 710px;
}
// .module-item {
// height: 100%;
// flex: 1;