feat:第一次提交
This commit is contained in:
parent
536ee3d7e6
commit
0b1dbf4d39
|
@ -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)
|
||||
}
|
|
@ -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.
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in New Issue