feat:第一次提交

This commit is contained in:
JiXinHui 2025-08-12 14:39:25 +08:00
parent 536ee3d7e6
commit 0b1dbf4d39
29 changed files with 3529 additions and 196 deletions

View File

@ -10,6 +10,12 @@ const indexUrl= {
'rightBottom':'/bigscreen/rightBottom',//右下
'rightCenter':'/bigscreen/ranking',// 报警排名
'consultationTrend':'/bigscreen/consultationTrend',// 咨询人数趋势图
'teacherConsultStats':'/bigscreen/teacherConsultStats',// 教师咨询数据统计
'studentDistribution':'/bigscreen/studentDistribution',// 学生用户属考生分布统计
'highSchoolRanking':'/bigscreen/highSchoolRanking',// 用户量属高中排行
'averageDuration':'/bigscreen/averageDuration',// 平均时长趋势
'majorRanking':'/bigscreen/majorRanking',// 用户量属专业排行
'commonQuestions':'/bigscreen/commonQuestions',// 常见咨询问题
}
export default indexUrl
@ -54,6 +60,37 @@ export const rightBottom=(param:any={})=>{
return GET(indexUrl.rightBottom,param)
}
/**咨询人数趋势图 */
export const consultationTrend=(param:any={})=>{
return GET(indexUrl.consultationTrend,param)
}
/**教师咨询数据统计 */
export const teacherConsultStats=(param:any={})=>{
return GET(indexUrl.teacherConsultStats,param)
}
/**学生用户属考生分布统计 */
export const studentDistribution=(param:any={})=>{
return GET(indexUrl.studentDistribution,param)
}
/**用户量属高中排行 */
export const highSchoolRanking=(param:any={})=>{
return GET(indexUrl.highSchoolRanking,param)
}
/**平均时长趋势 */
export const averageDuration=(param:any={})=>{
return GET(indexUrl.averageDuration,param)
}
/**用户量属专业排行 */
export const majorRanking=(param:any={})=>{
return GET(indexUrl.majorRanking,param)
}
/**常见咨询问题 */
export const commonQuestions=(param:any={})=>{
return GET(indexUrl.commonQuestions,param)
}

View File

@ -12,6 +12,14 @@ html,body{
src: url(../font/YouSheBiaoTiHei-2.ttf);
}
@font-face {
font-family: 'DS-Digital';
src: url('../font/DS-DIGI-1.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
html .el-message {
--yh-bg-color-container:#242424;
--yh-shadow-3: 0 16px 24px rgba(0, 0, 0, .14), 0 6px 30px rgba(0, 0, 0, 12%), 0 8px 10px rgba(0, 0, 0, 20%);

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -12,15 +12,17 @@ const props = withDefaults(
<template>
<div class="item_wrap">
<div class="item_title" v-if="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">{{ title }}</div>
<div class="item_title" v-if="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">{{ title }}</div>
</div>
<div
:class="title !== '' ? 'item_title_content' : 'item_title_content_def'"
>
<slot></slot>
</div>
</div>
<div :class="title !== '' ? 'item_title_content' : 'item_title_content_def'">
<slot></slot>
</div>
</div>
</template>
<style scoped lang="scss">
@ -30,7 +32,7 @@ $item_title_content-height: calc(100% - 33px);
.item_wrap {
background: #fff;
border-radius: 20px;
padding: 30px;
padding: 20px 30px 0 30px;
}
.item_title {
@ -49,7 +51,7 @@ $item_title_content-height: calc(100% - 33px);
.title-inner {
font-weight: 600;
color: #393D44;
color: #393d44;
}
}

View File

@ -236,6 +236,132 @@ export default [
}
};
}
},
// 教师咨询数据统计
{
url: "/bigscreen/teacherConsultStats",
type: "get",
response: () => {
return {
success: true,
data: {
consultCount: 12452,
replyCount: 12452,
messageCount: 2231
}
};
}
},
// 学生用户属考生分布统计
{
url: "/bigscreen/studentDistribution",
type: "get",
response: () => {
return {
success: true,
data: {
years: ['2022', '2023', '2024', '2025', '2026', '2027'],
values: [320, 400, 350, 500, 450, 380],
totalCount: 24680,
growthRate: 12.5
}
};
}
},
// 用户量属高中排行
{
url: "/bigscreen/highSchoolRanking",
type: "get",
response: () => {
return {
success: true,
data: [
{ rank: 1, school: '杭州第二中学', count: 3954 },
{ rank: 2, school: '杭州第一中学', count: 2360 },
{ rank: 3, school: '杭州学军中学', count: 1025 },
{ rank: 4, school: '杭州高级中学', count: 998 },
{ rank: 5, school: '杭州师范大学附属中学', count: 874 }
]
};
}
},
// 平均时长趋势
{
url: "/bigscreen/averageDuration",
type: "get",
response: () => {
return {
success: true,
data: {
days: ['7/1', '7/2', '7/3', '7/4', '7/5', '7/6', '7/7'],
durations: [65, 80, 45, 68, 80, 55, 60]
}
};
}
},
// 用户量属专业排行
{
url: "/bigscreen/majorRanking",
type: "get",
response: () => {
return {
success: true,
data: [
{ rank: 1, major: '计算机科学与技术', count: 3954 },
{ rank: 2, major: '软件工程', count: 2360 },
{ rank: 3, major: '人工智能', count: 1025 },
{ rank: 4, major: '数据科学与大数据技术', count: 998 },
{ rank: 5, major: '电子信息工程', count: 874 },
{ rank: 6, major: '通信工程', count: 3954 },
{ rank: 7, major: '网络工程', count: 2360 },
{ rank: 8, major: '信息安全', count: 1025 },
{ rank: 9, major: '物联网工程', count: 998 },
{ rank: 10, major: '自动化', count: 874 }
]
};
}
},
// 常见咨询问题
{
url: "/bigscreen/commonQuestions",
type: "get",
response: () => {
return {
success: true,
data: [
{
id: 1,
question: '学校有哪些奖学金政策和申请条件?',
count: 3948
},
{
id: 2,
question: '我的高考成绩能报考哪些学校和专业?',
count: 2249
},
{
id: 3,
question: '大学生实习和就业有哪些资源?',
count: 900
},
{
id: 4,
question: '如何申请转专业以及需要满足什么条件?',
count: 600
},
{
id: 5,
question: '如何申请休学、复学或者退学?',
count: 380
},
{
id: 6,
question: '如果高考分数不理想,有哪些复读或者其他选择?',
count: 20
}
]
};
}
}
];

View File

@ -0,0 +1,119 @@
<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

@ -0,0 +1,100 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import * as echarts from 'echarts';
import { centerMap } from "@/api";
// import 'echarts/map/js/china';
const chartRef = ref<HTMLElement | null>(null);
let chart: echarts.ECharts | null = null;
//
const mapData = ref({
dataList: [] as {name: string, value: number}[],
regionCode: 'china'
});
//
const getMapData = () => {
centerMap({regionCode: 'china'}).then(res => {
if (res.success) {
mapData.value = res.data;
initChart();
}
});
};
const initChart = () => {
if (!chartRef.value) return;
chart = echarts.init(chartRef.value);
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}'
},
visualMap: {
min: 0,
max: 1000,
left: 'left',
top: 'bottom',
text: ['高', '低'],
calculable: true,
inRange: {
color: ['#e0f3f8', '#4575b4']
}
},
series: [
{
name: '用户分布',
type: 'map',
map: 'china',
roam: false,
label: {
show: false
},
emphasis: {
label: {
show: true
}
},
data: mapData.value.dataList
}
]
};
chart.setOption(option);
};
//
const handleResize = () => {
if (chart) {
chart.resize();
}
};
onMounted(() => {
getMapData();
window.addEventListener('resize', handleResize);
});
//
const onUnmounted = () => {
window.removeEventListener('resize', handleResize);
if (chart) {
chart.dispose();
chart = null;
}
};
</script>
<template>
<div class="map-container" ref="chartRef"></div>
</template>
<style scoped lang="scss">
.map-container {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,155 @@
<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

@ -0,0 +1,237 @@
<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 chartData = ref({
dates: ['7/1', '7/2', '7/3', '7/4', '7/5'],
values: [80, 90, 55, 82, 62]
});
//
const conversationTypes = ref([
{ label: '会话类型', value: 'all' }
]);
const selectedType = ref('all');
const timeFilters = ref([
{ label: '时间筛选', value: 'week' }
]);
const selectedTimeFilter = ref('week');
//
const getChartData = () => {
averageDuration({
type: selectedType.value,
timeFilter: selectedTimeFilter.value
}).then(res => {
if (res.success) {
// API {dates: [], values: []}
// chartData.value = res.data;
initChart();
} else {
// API使
initChart();
}
}).catch(() => {
// 使
initChart();
});
};
//
const handleFilterChange = () => {
getChartData();
};
const initChart = () => {
if (!chartRef.value) return;
chart = echarts.init(chartRef.value);
const option = {
grid: {
top: '10%',
left: '8%',
right: '5%',
bottom: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: chartData.value.dates,
axisLine: {
lineStyle: {
color: '#E0E0E0'
}
},
axisLabel: {
color: '#999999',
fontSize: 12
}
},
yAxis: {
type: 'value',
max: 100,
interval: 30,
axisLabel: {
color: '#99AABF',
fontSize: 12,
formatter: '{value}'
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#E0E0E0'
}
}
},
series: [
{
type: 'bar',
data: chartData.value.values,
barWidth: '30%',
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: '#4B96FF' //
},
{
offset: 1,
color: '#65CBFF' //
}
]
},
borderRadius: [4, 4, 0, 0]
}
}
]
};
chart.setOption(option);
};
//
const handleResize = () => {
if (chart) {
chart.resize();
}
};
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="chart-header">
<div class="filters">
<div class="filter-item">
<el-select v-model="selectedType" size="small" class="filter-select" @change="handleFilterChange">
<el-option
v-for="item in conversationTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="filter-item">
<el-select v-model="selectedTimeFilter" size="small" class="filter-select" @change="handleFilterChange">
<el-option
v-for="item in timeFilters"
: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">
.average-duration {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: #fff;
border-radius: 10px;
padding: 15px;
box-sizing: border-box;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
.title {
display: flex;
align-items: center;
font-size: 16px;
font-weight: bold;
color: #333;
.title-icon {
width: 4px;
height: 16px;
background-color: #4B96FF;
margin-right: 8px;
border-radius: 2px;
}
}
.filters {
display: flex;
gap: 10px;
.filter-item {
.filter-select {
width: 120px;
:deep(.el-input__wrapper) {
background-color: #f5f7fa;
box-shadow: none;
border-radius: 4px;
}
:deep(.el-input__inner) {
font-size: 12px;
}
}
}
}
}
.chart-container {
flex: 1;
width: 100%;
}
</style>

112
src/views/index/LeftTop.vue Normal file
View File

@ -0,0 +1,112 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { teacherConsultStats } from "@/api";
//
const teacherStats = ref({
consultCount: 0,
replyCount: 0,
messageCount: 0
});
//
const getTeacherStats = () => {
teacherConsultStats().then(res => {
if (res.success) {
teacherStats.value = res.data;
}
});
};
onMounted(() => {
getTeacherStats();
});
</script>
<template>
<div class="stats-container">
<div class="stat-item">
<div class="stat-icon blue-bg">
<img src="@/assets/img/zheke/蓝色.png" alt="咨询次数">
</div>
<div class="stat-info">
<div class="stat-value">{{ teacherStats.consultCount }}</div>
<div class="stat-label">咨询次数</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon orange-bg">
<img src="@/assets/img/zheke/橙色.png" alt="回复数">
</div>
<div class="stat-info">
<div class="stat-value">{{ teacherStats.replyCount }}</div>
<div class="stat-label">回复数</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon cyan-bg">
<img src="@/assets/img/zheke/浅蓝色.png" alt="留言数">
</div>
<div class="stat-info">
<div class="stat-value">{{ teacherStats.messageCount }}</div>
<div class="stat-label">留言数</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.stats-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
padding: 15px 0;
}
.stat-item {
display: flex;
align-items: center;
width: 100%;
margin-bottom: 10px;
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
img {
width: 30px;
height: 30px;
}
&.blue-bg {
background-color: rgba(75, 150, 255, 0.1);
}
&.orange-bg {
background-color: rgba(250, 140, 22, 0.1);
}
&.cyan-bg {
background-color: rgba(24, 144, 255, 0.1);
}
}
.stat-info {
.stat-value {
font-size: 24px;
font-weight: bold;
color: #333;
}
.stat-label {
font-size: 14px;
color: #666;
}
}
</style>

View File

@ -0,0 +1,158 @@
<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

@ -0,0 +1,178 @@
<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

@ -0,0 +1,218 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';
import { averageDuration } from "@/api";
const chartRef = ref<HTMLElement | null>(null);
let chart: echarts.ECharts | null = null;
//
const chartData = ref({
dates: ['7/1', '7/2', '7/3', '7/4', '7/5'],
values: [80, 90, 55, 82, 62]
});
//
const conversationTypes = ref([
{ label: '会话类型', value: 'all' }
]);
const selectedType = ref('all');
const timeFilters = ref([
{ label: '时间筛选', value: 'week' }
]);
const selectedTimeFilter = ref('week');
//
const getChartData = () => {
averageDuration({
type: selectedType.value,
timeFilter: selectedTimeFilter.value
}).then(res => {
if (res.success) {
// API {dates: [], values: []}
// chartData.value = res.data;
initChart();
} else {
// API使
initChart();
}
}).catch(() => {
// 使
initChart();
});
};
//
const handleFilterChange = () => {
getChartData();
};
const initChart = () => {
if (!chartRef.value) return;
chart = echarts.init(chartRef.value);
const option = {
grid: {
top: '10%',
left: '8%',
right: '5%',
bottom: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: chartData.value.dates,
axisLine: {
lineStyle: {
color: '#E0E0E0'
}
},
axisLabel: {
color: '#999999',
fontSize: 12
}
},
yAxis: {
type: 'value',
max: 100,
interval: 30,
axisLabel: {
color: '#99AABF',
fontSize: 12,
formatter: '{value}'
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#E0E0E0'
}
}
},
series: [
{
type: 'bar',
data: chartData.value.values,
barWidth: '30%',
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: '#4B96FF' //
},
{
offset: 1,
color: '#65CBFF' //
}
]
},
borderRadius: [4, 4, 0, 0]
}
}
]
};
chart.setOption(option);
};
//
const handleResize = () => {
if (chart) {
chart.resize();
}
};
onMounted(() => {
getChartData();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (chart) {
chart.dispose();
chart = null;
}
});
</script>
<template>
<div class="average-duration">
<div class="chart-header">
<div class="filters">
<div class="filter-item">
<el-select v-model="selectedType" size="small" class="filter-select" @change="handleFilterChange">
<el-option
v-for="item in conversationTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="filter-item">
<el-select v-model="selectedTimeFilter" size="small" class="filter-select" @change="handleFilterChange">
<el-option
v-for="item in timeFilters"
: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">
.average-duration {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 0;
box-sizing: border-box;
}
.chart-header {
display: flex;
justify-content: flex-end;
align-items: center;
margin-bottom: 10px;
.filters {
display: flex;
gap: 10px;
.filter-item {
.filter-select {
width: 120px;
:deep(.el-input__wrapper) {
background-color: #f5f7fa;
box-shadow: none;
border-radius: 4px;
}
:deep(.el-input__inner) {
font-size: 12px;
}
}
}
}
}
.chart-container {
flex: 1;
width: 100%;
}
</style>

View File

@ -0,0 +1,143 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
//
const questionData = ref([
{
id: 1,
question: '学校有哪些奖学金政策和申请条件?',
count: 3948
},
{
id: 2,
question: '我的高考成绩能报考哪些学校和专业?',
count: 2249
},
{
id: 3,
question: '大学生实习和就业有哪些资源?',
count: 900
},
{
id: 4,
question: '如何申请转专业以及需要满足什么条件?',
count: 600
},
{
id: 5,
question: '如何申请休学、复学或者退学?',
count: 380
},
{
id: 6,
question: '如果高考分数不理想,有哪些复读或者其他选择?',
count: 20
}
]);
//
const fetchQuestionData = () => {
// API
// API
setTimeout(() => {
//
}, 500);
};
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

@ -0,0 +1,119 @@
<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

@ -0,0 +1,100 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import * as echarts from 'echarts';
import { centerMap } from "@/api";
// import 'echarts/map/js/china';
const chartRef = ref<HTMLElement | null>(null);
let chart: echarts.ECharts | null = null;
//
const mapData = ref({
dataList: [] as {name: string, value: number}[],
regionCode: 'china'
});
//
const getMapData = () => {
centerMap({regionCode: 'china'}).then(res => {
if (res.success) {
mapData.value = res.data;
initChart();
}
});
};
const initChart = () => {
if (!chartRef.value) return;
chart = echarts.init(chartRef.value);
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}'
},
visualMap: {
min: 0,
max: 1000,
left: 'left',
top: 'bottom',
text: ['高', '低'],
calculable: true,
inRange: {
color: ['#e0f3f8', '#4575b4']
}
},
series: [
{
name: '用户分布',
type: 'map',
map: 'china',
roam: false,
label: {
show: false
},
emphasis: {
label: {
show: true
}
},
data: mapData.value.dataList
}
]
};
chart.setOption(option);
};
//
const handleResize = () => {
if (chart) {
chart.resize();
}
};
onMounted(() => {
getMapData();
window.addEventListener('resize', handleResize);
});
//
const onUnmounted = () => {
window.removeEventListener('resize', handleResize);
if (chart) {
chart.dispose();
chart = null;
}
};
</script>
<template>
<div class="map-container" ref="chartRef"></div>
</template>
<style scoped lang="scss">
.map-container {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,155 @@
<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

@ -0,0 +1,188 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import * as echarts from 'echarts';
import { studentDistribution } from "@/api";
const chartRef = ref<HTMLElement | null>(null);
let chart: echarts.ECharts | null = null;
//
const chartData = ref({
years: [] as string[],
values: [] as number[],
totalCount: 0,
growthRate: 0
});
//
const getChartData = () => {
studentDistribution().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: '15%',
left: '3%',
right: '4%',
bottom: '10%',
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
xAxis: {
type: 'category',
data: chartData.value.years,
axisLine: {
lineStyle: {
color: '#E0E0E0'
}
},
axisLabel: {
color: '#999999'
}
},
yAxis: {
type: 'value',
splitLine: {
lineStyle: {
type: 'dashed',
color: '#E0E0E0'
}
},
axisLabel: {
color: '#999999'
}
},
series: [
{
name: '学生人数',
type: 'line',
data: chartData.value.values,
smooth: true,
showSymbol: true,
symbolSize: 8,
itemStyle: {
color: '#4B96FF'
},
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)'
}
]
}
}
}
]
};
chart.setOption(option);
};
//
const handleResize = () => {
if (chart) {
chart.resize();
}
};
onMounted(() => {
getChartData();
window.addEventListener('resize', handleResize);
});
//
const onUnmounted = () => {
window.removeEventListener('resize', handleResize);
if (chart) {
chart.dispose();
chart = null;
}
};
</script>
<template>
<div class="student-distribution">
<div class="chart-container" ref="chartRef"></div>
<div class="chart-info">
<div class="info-item">
<div class="info-label">总考生数量</div>
<div class="info-value">{{ chartData.totalCount.toLocaleString() }}</div>
</div>
<div class="info-item">
<div class="info-label">同比增长</div>
<div class="info-value increase">+{{ chartData.growthRate }}%</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.student-distribution {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.chart-container {
flex: 1;
width: 100%;
}
.chart-info {
display: flex;
justify-content: space-around;
margin-top: 10px;
padding: 10px 0;
.info-item {
text-align: center;
.info-label {
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.info-value {
font-size: 20px;
font-weight: bold;
color: #333;
&.increase {
color: #52c41a;
}
&.decrease {
color: #f5222d;
}
}
}
}
</style>

View File

@ -0,0 +1,132 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { teacherConsultStats } from "@/api";
interface TeacherStats {
consultCount: number;
replyCount: number;
messageCount: number;
[key: string]: number; //
}
//
const teacherStats = ref<TeacherStats>({
consultCount: 0,
replyCount: 0,
messageCount: 0,
});
//
const getTeacherStats = () => {
teacherConsultStats().then((res) => {
if (res.success) {
teacherStats.value = res.data;
}
});
};
onMounted(() => {
getTeacherStats();
});
</script>
<template>
<div class="stats-container">
<div class="stat-item blue-card">
<div class="stat-icon">
<img src="@/assets/img/zheke/ai-stats1.png" alt="咨询次数" />
</div>
<div class="stat-label">咨询次数</div>
<div class="stat-value">
{{ teacherStats.consultCount }}<span class="unit"></span>
</div>
</div>
<div class="stat-item orange-card">
<div class="stat-icon">
<img src="@/assets/img/zheke/ai-stats2.png" alt="回复数" />
</div>
<div class="stat-info">
<div class="stat-value">
{{ teacherStats.replyCount }}<span class="unit"></span>
</div>
<div class="stat-label">回复数</div>
</div>
</div>
<div class="stat-item cyan-card">
<div class="stat-icon">
<img src="@/assets/img/zheke/ai-stats4.png" alt="留言数" />
</div>
<div class="stat-info">
<div class="stat-value">
{{ teacherStats.messageCount }}<span class="unit"></span>
</div>
<div class="stat-label">留言数</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.stats-container {
display: flex;
gap: 6px;
height: 100%;
box-sizing: border-box;
margin-top: 10px;
}
.stat-item {
display: flex;
flex: 1;
flex-direction: column;
align-items: flex-start;
border-radius: 10px;
padding: 9px 0 0 14px;
background-size: 100% 100%;
background-repeat: no-repeat;
&.blue-card {
background-image: url("@/assets/img/zheke/蓝色.png");
}
&.orange-card {
background-image: url("@/assets/img/zheke/橙色.png");
}
&.cyan-card {
background-image: url("@/assets/img/zheke/浅蓝色.png");
}
}
.stat-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
}
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #fff;
.unit {
font-size: 14px;
margin-left: 2px;
}
}
.stat-label {
font-size: 14px;
color: #fff;
opacity: 0.9;
}
</style>

View File

@ -0,0 +1,119 @@
<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

@ -0,0 +1,158 @@
<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

@ -0,0 +1,178 @@
<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

@ -24,20 +24,20 @@ const formattedConsultCount = computed(() => formatNumber(consultCount.value));
//
const consultTypeOptions = [
{ value: 'all', label: '全部类型' },
{ value: 'academic', label: '学业咨询' },
{ value: 'career', label: '就业咨询' },
{ value: 'personal', label: '个人发展' },
{ value: 'other', label: '其他咨询' }
{ value: "all", label: "全部类型" },
{ value: "academic", label: "学业咨询" },
{ value: "career", label: "就业咨询" },
{ value: "personal", label: "个人发展" },
{ value: "other", label: "其他咨询" },
];
const selectedConsultType = ref(consultTypeOptions[0].value);
const timeRangeOptions = [
{ value: '1week', label: '近一周' },
{ value: '1month', label: '近一月' },
{ value: '3months', label: '近三月' },
{ value: 'halfyear', label: '近半年' },
{ value: '1year', label: '近一年' }
{ value: "1week", label: "近一周" },
{ value: "1month", label: "近一月" },
{ value: "3months", label: "近三月" },
{ value: "halfyear", label: "近半年" },
{ value: "1year", label: "近一年" },
];
const selectedTimeRange = ref(timeRangeOptions[0].value);
@ -53,28 +53,28 @@ const dateRange = ref<[Date, Date]>(getDefaultDateRange() as [Date, Date]);
//
const option = ref({});
const getData = () => {
//
const params = {
consultType: selectedConsultType.value,
timeRange: selectedTimeRange.value,
dateRange: dateRange.value
};
consultationTrend().then((res)=> {
if(res.success) {
setOptions(res.data)
} else {
ElMessage({
message: res.msg,
type: "warning",
});
}
})
}
//
const params = {
consultType: selectedConsultType.value,
timeRange: selectedTimeRange.value,
dateRange: dateRange.value,
};
consultationTrend().then((res) => {
if (res.success) {
setOptions(res.data);
} else {
ElMessage({
message: res.msg,
type: "warning",
});
}
});
};
//
const handleFilterChange = () => {
getData();
getData();
};
//
@ -82,125 +82,124 @@ watch(selectedConsultType, handleFilterChange);
watch(selectedTimeRange, handleFilterChange);
watch(dateRange, handleFilterChange);
const setOptions = async (newData:any) => {
option.value = {
grid: {
top: '15%',
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
const setOptions = async (newData: any) => {
option.value = {
grid: {
top: "15%",
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true,
},
backgroundColor: "#ffffff",
tooltip: {
trigger: "axis",
axisPointer: {
type: "line",
lineStyle: {
color: "#4B96FF",
width: 1,
},
backgroundColor: '#ffffff',
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line',
lineStyle: {
color: '#4B96FF',
width: 1
}
},
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#F1F1F1',
borderWidth: 1,
padding: [8, 10],
textStyle: {
color: '#666'
},
formatter: function(params: any) {
const data = params[0];
return `${data.name}<br/><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background-color:#4B96FF;margin-right:5px;"></span>${data.value}`;
}
},
backgroundColor: "rgba(255, 255, 255, 0.9)",
borderColor: "#F1F1F1",
borderWidth: 1,
padding: [8, 10],
textStyle: {
color: "#666",
},
formatter: function (params: any) {
const data = params[0];
return `${data.name}<br/><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background-color:#4B96FF;margin-right:5px;"></span>${data.value}`;
},
},
xAxis: {
type: "category",
boundaryGap: false,
data: newData.dateList,
axisLine: {
lineStyle: {
color: "#E0E0E0",
},
xAxis: {
type: 'category',
boundaryGap: false,
data: newData.dateList,
axisLine: {
lineStyle: {
color: '#E0E0E0'
}
},
axisLabel: {
color: '#999999',
formatter: function(value: string) {
return value.replace('2023-', '');
}
}
},
axisLabel: {
color: "#999999",
formatter: function (value: string) {
return value.replace("2023-", "");
},
yAxis: {
type: 'value',
max: 900,
min: 0,
interval: 300,
splitNumber: 3,
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#E0E0E0'
}
},
axisLabel: {
color: '#999999',
formatter: '{value}'
}
},
},
yAxis: {
type: "value",
max: 900,
min: 0,
interval: 300,
splitNumber: 3,
axisLine: {
show: false,
},
axisTick: {
show: false,
},
splitLine: {
lineStyle: {
type: "dashed",
color: "#E0E0E0",
},
series: [
{
type: 'line',
data: newData.valueList,
smooth: true,
symbol: 'circle',
symbolSize: 8,
showSymbol: function(idx: number) {
//
return idx === 2;
},
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)'
}
]
}
}
}
]
}
}
},
axisLabel: {
color: "#999999",
formatter: "{value}",
},
},
series: [
{
type: "line",
data: newData.valueList,
smooth: true,
symbol: "circle",
symbolSize: 8,
showSymbol: function (idx: number) {
//
return idx === 2;
},
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)",
},
],
},
},
},
],
};
};
//
onMounted(() => {
getData();
getData();
});
</script>
<template>
@ -208,10 +207,9 @@ onMounted(() => {
<!-- 今日咨询人数 -->
<div class="header-item today-count">
<div class="item-title">今日咨询人数</div>
<!-- 使用格式化后的值 -->
<div class="seven-segment-container">
<div class="seven-segment-display">{{ formattedValue }}</div>
</div>
<div class="seven-segment-display">{{ formattedValue }}</div>
<div class="count-details">
<div class="detail-item">
<div class="icon-wrap student-icon">
@ -240,14 +238,14 @@ onMounted(() => {
<div class="stats-cards">
<div class="stat-card blue-card">
<div class="card-icon">
<img src="@/assets/img/zheke/ai-stats1.png" alt="咨询次数">
<img src="@/assets/img/zheke/ai-stats1.png" alt="咨询次数" />
</div>
<div class="card-label">咨询次数</div>
<div class="card-value">12,452<span class="unit"></span></div>
</div>
<div class="stat-card orange-card">
<div class="card-icon">
<img src="@/assets/img/zheke/ai-stats2.png" alt=" 平均对话次数">
<img src="@/assets/img/zheke/ai-stats2.png" alt=" 平均对话次数" />
</div>
<div class="card-content">
<div class="card-label">平均对话次数</div>
@ -256,7 +254,7 @@ onMounted(() => {
</div>
<div class="stat-card green-card">
<div class="card-icon">
<img src="@/assets/img/zheke/ai-stats3.png" alt="智能回复率">
<img src="@/assets/img/zheke/ai-stats3.png" alt="智能回复率" />
</div>
<div class="card-content">
<div class="card-label">智能回复率</div>
@ -272,7 +270,11 @@ onMounted(() => {
<div class="item-title">咨询人数趋势图</div>
<div class="chart-filters">
<div class="filter-item">
<el-select v-model="selectedConsultType" placeholder="咨询类型" size="small">
<el-select
v-model="selectedConsultType"
placeholder="咨询类型"
size="small"
>
<el-option
v-for="item in consultTypeOptions"
:key="item.value"
@ -292,7 +294,11 @@ onMounted(() => {
/>
</div>
<div class="filter-item">
<el-select v-model="selectedTimeRange" placeholder="时间范围" size="small">
<el-select
v-model="selectedTimeRange"
placeholder="时间范围"
size="small"
>
<el-option
v-for="item in timeRangeOptions"
:key="item.value"
@ -304,7 +310,11 @@ onMounted(() => {
</div>
</div>
<div class="chart-container">
<v-chart class="chart" :option="option" v-if="JSON.stringify(option) != '{}'"></v-chart>
<v-chart
class="chart"
:option="option"
v-if="JSON.stringify(option) != '{}'"
></v-chart>
</div>
</div>
</div>
@ -315,17 +325,13 @@ onMounted(() => {
display: flex;
align-items: center;
box-sizing: border-box;
height: 260px;
height: 240px;
width: 100%;
background-color: #fff;
border-radius: 20px;
// padding: 15px;
// box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
.header-item {
height: 100%;
padding: 20px 0 0 38px;
.item-title {
font-family: DouyinSans;
font-weight: bold;
@ -337,27 +343,22 @@ onMounted(() => {
//
.today-count {
width: 25%;
// border: 1px solid black;
padding: 30px 40px;
.seven-segment-container {
margin: 15px 0;
height: 100px;
display: flex;
align-items: center;
// justify-content: center;
.seven-segment-display {
font-size: 100px;
font-family: DS-Digital;
font-weight: bold;
color: #4d50dd;
}
box-sizing: border-box;
.seven-segment-display {
font-size: 100px;
font-family: DS-Digital;
font-weight: bold;
color: #4d50dd;
height: 74px;
line-height: 74px;
margin-top: 22px;
letter-spacing: 10px;
}
.count-details {
display: flex;
// gap: 40px;
margin-top: 22px;
.detail-item {
flex: 1;
display: flex;
@ -395,8 +396,6 @@ onMounted(() => {
// AI
.ai-stats {
width: 29%;
padding: 30px 35px;
.stats-cards {
margin-top: 24px;
display: flex;
@ -435,7 +434,7 @@ onMounted(() => {
.card-icon {
width: 40px;
height: 40px;
margin-bottom:14px;
margin-bottom: 14px;
}
.card-label {
@ -460,14 +459,10 @@ onMounted(() => {
//
.trend-chart {
flex: 1;
padding: 30px 33px 0 35px;
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
// margin-bottom: 15px;
.chart-filters {
display: flex;
gap: 15px;
@ -478,16 +473,16 @@ onMounted(() => {
font-size: 12px;
color: #666;
min-width: 120px;
:deep(.el-input__wrapper) {
background-color: #f5f7fa;
box-shadow: none;
}
:deep(.el-select .el-input) {
width: 120px;
}
:deep(.el-date-editor--daterange) {
width: 240px;
}

View File

@ -0,0 +1,154 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
//
const rankingData = ref([
{ rank: 1, school: '杭州第二中学', count: 3954 },
{ rank: 2, school: '杭州第一中学', count: 2360 },
{ rank: 3, school: '杭州学军中学', count: 1025 },
{ rank: 4, school: '杭州高级中学', count: 998 },
{ rank: 5, school: '杭州师范大学附属中学', count: 874 }
]);
//
const fetchRankingData = () => {
// API
// API
setTimeout(() => {
//
}, 500);
};
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,11 +1,69 @@
<script setup lang="ts">
import zkItem from "@/components/zk-item";
import ContentHeader from "./content-header.vue";
import { defineAsyncComponent } from 'vue';
import ItemWrap from "@/components/item-wrap";
const ContentHeader = defineAsyncComponent(() => import("./content-header.vue"));
//
const LeftTop = defineAsyncComponent(() => import("./components/LeftTop.vue"));
const LeftCenter = defineAsyncComponent(() => import("./LeftCenter.vue"));
const AverageDuration = defineAsyncComponent(() => import("./average-duration.vue"));
const LeftBottom = defineAsyncComponent(() => import("./components/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"));
</script>
<template>
<div class="index-box">
<ContentHeader />
<div class="content-container">
<!-- 左侧内容区域 -->
<div class="left-content">
<ItemWrap title="教师咨询数据统计" class="left-contetn-top module-item">
<LeftTop />
</ItemWrap>
<ItemWrap title="平均对话时长" class="left-contetn-center module-item">
<LeftCenter />
</ItemWrap>
<!-- <ItemWrap title="学生用户高考年份统计" class="left-contetn-bottom module-item">
<LeftBottom />
</ItemWrap> -->
</div>
<!-- 中间内容区域 -->
<div class="center-content">
<ItemWrap class="module-item map-container">
<CenterTop />
</ItemWrap>
<ItemWrap title="高频咨询问题" class="module-item">
<CenterBottom />
</ItemWrap>
</div>
<!-- 右侧内容区域 -->
<div class="right-content">
<ItemWrap title="平均时长趋势" class="module-item">
<RightTop />
</ItemWrap>
<ItemWrap title="用户量属专业排行" class="module-item">
<RightCenter />
</ItemWrap>
<ItemWrap title="常见咨询问题" class="module-item">
<RightBottom />
</ItemWrap>
</div>
</div>
</div>
</template>
@ -13,8 +71,51 @@ import ContentHeader from "./content-header.vue";
.index-box {
width: 100%;
display: flex;
flex-direction: column;
min-height: calc(100% - 80px);
justify-content: space-between;
padding: 20px;
}
.content-container {
display: flex;
width: 100%;
height: 100%;
gap: 20px;
margin-top: 12px;
}
.left-content, .right-content {
display: flex;
flex-direction: column;
width: 25%;
gap: 20px;
}
.center-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.left-contetn-top {
height: 200px;
}
.left-contetn-center {
height: 250px;
}
.left-contetn-bottom {
height: 240px;
}
// .module-item {
// height: 100%;
// flex: 1;
// }
.map-container {
height: 60%;
}
</style>

View File

@ -0,0 +1,162 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
//
const rankingData = ref([
{ rank: 1, major: '计算机科学与技术', count: 3954 },
{ rank: 2, major: '软件工程', count: 2360 },
{ rank: 3, major: '人工智能', count: 1025 },
{ rank: 4, major: '数据科学与大数据技术', count: 998 },
{ rank: 5, major: '电子信息工程', count: 874 },
{ rank: 6, major: '通信工程', count: 3954 },
{ rank: 7, major: '网络工程', count: 2360 },
{ rank: 8, major: '信息安全', count: 1025 },
{ rank: 9, major: '物联网工程', count: 998 },
{ rank: 10, major: '自动化', count: 874 }
]);
//
const fetchRankingData = () => {
// API
// API
setTimeout(() => {
//
}, 500);
};
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

@ -0,0 +1,178 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import * as echarts from 'echarts';
const chartRef = ref<HTMLElement | null>(null);
let chart: echarts.ECharts | null = null;
//
const years = ['2022', '2023', '2024', '2025', '2026', '2027'];
const data = [320, 400, 350, 500, 450, 380];
const initChart = () => {
if (!chartRef.value) return;
chart = echarts.init(chartRef.value);
const option = {
grid: {
top: '15%',
left: '3%',
right: '4%',
bottom: '10%',
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
xAxis: {
type: 'category',
data: years,
axisLine: {
lineStyle: {
color: '#E0E0E0'
}
},
axisLabel: {
color: '#999999'
}
},
yAxis: {
type: 'value',
splitLine: {
lineStyle: {
type: 'dashed',
color: '#E0E0E0'
}
},
axisLabel: {
color: '#999999'
}
},
series: [
{
name: '学生人数',
type: 'line',
data: data,
smooth: true,
showSymbol: true,
symbolSize: 8,
itemStyle: {
color: '#4B96FF'
},
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)'
}
]
}
}
}
]
};
chart.setOption(option);
};
//
const handleResize = () => {
if (chart) {
chart.resize();
}
};
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
});
//
const onUnmounted = () => {
window.removeEventListener('resize', handleResize);
if (chart) {
chart.dispose();
chart = null;
}
};
//
defineComponent({
name: 'StudentDistribution'
});
</script>
<template>
<div class="student-distribution">
<div class="chart-container" ref="chartRef"></div>
<div class="chart-info">
<div class="info-item">
<div class="info-label">总考生数量</div>
<div class="info-value">24,680</div>
</div>
<div class="info-item">
<div class="info-label">同比增长</div>
<div class="info-value increase">+12.5%</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.student-distribution {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.chart-container {
flex: 1;
width: 100%;
}
.chart-info {
display: flex;
justify-content: space-around;
margin-top: 10px;
padding: 10px 0;
.info-item {
text-align: center;
.info-label {
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.info-value {
font-size: 20px;
font-weight: bold;
color: #333;
&.increase {
color: #52c41a;
}
&.decrease {
color: #f5222d;
}
}
}
}
</style>