feat:添加试卷分析页面;添加消息中心页面
This commit is contained in:
parent
6b685501dd
commit
764064bd80
@ -341,7 +341,7 @@ const handleMoveCourse = (course: any) => {
|
|||||||
.course-card {
|
.course-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
aspect-ratio: 190 / 201;
|
/* aspect-ratio: 190 / 201; */
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
border: 1px solid #D8D8D8;
|
border: 1px solid #D8D8D8;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -431,11 +431,10 @@ const handleMoveCourse = (course: any) => {
|
|||||||
|
|
||||||
.course-name {
|
.course-name {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
height: 20px;
|
|
||||||
font-family: AppleSystemUIFont;
|
font-family: AppleSystemUIFont;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
line-height: 20px;
|
line-height: 1.5;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
@ -566,8 +565,6 @@ const handleMoveCourse = (course: any) => {
|
|||||||
|
|
||||||
.course-name {
|
.course-name {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
height: 24px;
|
|
||||||
line-height: 24px;
|
|
||||||
width: 85%;
|
width: 85%;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
/* 文字往上移动 */
|
/* 文字往上移动 */
|
||||||
@ -606,8 +603,6 @@ const handleMoveCourse = (course: any) => {
|
|||||||
|
|
||||||
.course-name {
|
.course-name {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
height: 22px;
|
|
||||||
line-height: 22px;
|
|
||||||
width: 82%;
|
width: 82%;
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
/* 文字往上移动 */
|
/* 文字往上移动 */
|
||||||
@ -686,8 +681,6 @@ const handleMoveCourse = (course: any) => {
|
|||||||
|
|
||||||
.course-name {
|
.course-name {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
height: 18px;
|
|
||||||
line-height: 18px;
|
|
||||||
width: 78%;
|
width: 78%;
|
||||||
margin-top: -5px;
|
margin-top: -5px;
|
||||||
/* 文字往上移动 */
|
/* 文字往上移动 */
|
||||||
@ -726,8 +719,6 @@ const handleMoveCourse = (course: any) => {
|
|||||||
|
|
||||||
.course-name {
|
.course-name {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
height: 16px;
|
|
||||||
line-height: 16px;
|
|
||||||
width: 75%;
|
width: 75%;
|
||||||
margin-top: -4px;
|
margin-top: -4px;
|
||||||
/* 文字往上移动 */
|
/* 文字往上移动 */
|
||||||
@ -789,8 +780,6 @@ const handleMoveCourse = (course: any) => {
|
|||||||
|
|
||||||
.course-name {
|
.course-name {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
height: 14px;
|
|
||||||
line-height: 14px;
|
|
||||||
width: 70%;
|
width: 70%;
|
||||||
margin-top: -3px;
|
margin-top: -3px;
|
||||||
/* 文字往上移动 */
|
/* 文字往上移动 */
|
||||||
|
@ -36,6 +36,7 @@ import PersonalCenter from '@/components/admin/PersonalCenter.vue'
|
|||||||
import CourseManagement from '@/components/admin/CourseManagement.vue'
|
import CourseManagement from '@/components/admin/CourseManagement.vue'
|
||||||
import MyResources from '@/components/admin/MyResources.vue'
|
import MyResources from '@/components/admin/MyResources.vue'
|
||||||
import StudentManagement from '@/components/admin/StudentManagement.vue'
|
import StudentManagement from '@/components/admin/StudentManagement.vue'
|
||||||
|
import MessageCenter from '@/views/teacher/message/MessageCenter.vue'
|
||||||
|
|
||||||
// 课程管理子组件
|
// 课程管理子组件
|
||||||
import CourseCategory from '@/components/admin/CourseComponents/CourseCategory.vue'
|
import CourseCategory from '@/components/admin/CourseComponents/CourseCategory.vue'
|
||||||
@ -79,6 +80,7 @@ import StudentList from '@/views/teacher/ExamPages/StudentList.vue'
|
|||||||
import GradingPage from '@/views/teacher/ExamPages/GradingPage.vue'
|
import GradingPage from '@/views/teacher/ExamPages/GradingPage.vue'
|
||||||
import ExamTaking from '@/views/teacher/ExamPages/ExamTaking.vue'
|
import ExamTaking from '@/views/teacher/ExamPages/ExamTaking.vue'
|
||||||
import ExamNoticeBeforeStart from '@/views/teacher/ExamPages/ExamNoticeBeforeStart.vue'
|
import ExamNoticeBeforeStart from '@/views/teacher/ExamPages/ExamNoticeBeforeStart.vue'
|
||||||
|
import ExamAnalysis from '@/views/teacher/ExamPages/ExamAnalysis.vue'
|
||||||
|
|
||||||
import ChapterEditor from '@/views/teacher/course/ChapterEditor.vue'
|
import ChapterEditor from '@/views/teacher/course/ChapterEditor.vue'
|
||||||
|
|
||||||
@ -294,6 +296,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: MyResources,
|
component: MyResources,
|
||||||
meta: { title: '我的资源' }
|
meta: { title: '我的资源' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'message-center',
|
||||||
|
name: 'MessageCenter',
|
||||||
|
component: MessageCenter,
|
||||||
|
meta: { title: '消息中心' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'student-management',
|
path: 'student-management',
|
||||||
name: 'StudentManagement',
|
name: 'StudentManagement',
|
||||||
@ -364,6 +372,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: ExamLibrary,
|
component: ExamLibrary,
|
||||||
meta: { title: '试卷管理' }
|
meta: { title: '试卷管理' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'exam-analysis',
|
||||||
|
name: 'ExamAnalysis',
|
||||||
|
component: ExamAnalysis,
|
||||||
|
meta: { title: '试卷分析' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'marking-center',
|
path: 'marking-center',
|
||||||
name: 'MarkingCenter',
|
name: 'MarkingCenter',
|
||||||
|
@ -78,14 +78,13 @@
|
|||||||
<img :src="activeNavItem === 2 ? '/images/teacher/我的资源(选中).png' : '/images/teacher/我的资源.png'" alt="">
|
<img :src="activeNavItem === 2 ? '/images/teacher/我的资源(选中).png' : '/images/teacher/我的资源.png'" alt="">
|
||||||
<span>我的资源</span>
|
<span>我的资源</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<!-- <router-link to="/teacher/my-resources" class="nav-item" :class="{ active: activeNavItem === 2 }"
|
<router-link to="/teacher/message-center" class="nav-item" :class="{ active: activeNavItem === 5 }"
|
||||||
@click="setActiveNavItem(2)">
|
@click="setActiveNavItem(5)">
|
||||||
<img :src="activeNavItem === 2 ? '/images/teacher/我的资源(选中).png' : '/images/teacher/我的资源.png'" alt="">
|
<img :src="activeNavItem === 5 ? '/images/teacher/消息中心(选中).png' : '/images/teacher/消息中心.png'" alt="">
|
||||||
<span>消息中心</span>
|
<span>消息中心</span>
|
||||||
</router-link> -->
|
</router-link>
|
||||||
<router-link to="/teacher/personal-center" class="nav-item" :class="{ active: activeNavItem === 3 }"
|
<router-link to="/teacher/personal-center" class="nav-item" :class="{ active: activeNavItem === 3 }"
|
||||||
@click="setActiveNavItem(3)">
|
@click="setActiveNavItem(3)">
|
||||||
|
|
||||||
<img :src="activeNavItem === 3 ? '/images/teacher/个人中心(选中).png' : '/images/teacher/个人中心.png'" alt="">
|
<img :src="activeNavItem === 3 ? '/images/teacher/个人中心(选中).png' : '/images/teacher/个人中心.png'" alt="">
|
||||||
<span>个人中心</span>
|
<span>个人中心</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
@ -131,7 +130,7 @@ const height = window.innerHeight;
|
|||||||
console.log(`当前屏幕宽度: ${width}px, 高度: ${height}px`);
|
console.log(`当前屏幕宽度: ${width}px, 高度: ${height}px`);
|
||||||
|
|
||||||
// 添加导航项激活状态管理
|
// 添加导航项激活状态管理
|
||||||
const activeNavItem = ref(0); // 0: 课程管理, 1: 学员管理, 2: 我的资源, 3: 个人中心
|
const activeNavItem = ref(0); // 0: 课程管理, 1: 学员管理, 2: 我的资源, 3: 个人中心, 4: 考试管理, 5: 消息中心
|
||||||
const activeSubNavItem = ref(''); // 子菜单激活状态
|
const activeSubNavItem = ref(''); // 子菜单激活状态
|
||||||
const examMenuExpanded = ref(false); // 考试管理菜单展开状态
|
const examMenuExpanded = ref(false); // 考试管理菜单展开状态
|
||||||
const studentMenuExpanded = ref(false); // 学员中心菜单展开状态
|
const studentMenuExpanded = ref(false); // 学员中心菜单展开状态
|
||||||
@ -609,6 +608,8 @@ const updateActiveNavItem = () => {
|
|||||||
const arr = ['question-bank', 'exam-library', 'marking-center'];
|
const arr = ['question-bank', 'exam-library', 'marking-center'];
|
||||||
const found = arr.find(item => path.includes(item));
|
const found = arr.find(item => path.includes(item));
|
||||||
activeSubNavItem.value = found || '';
|
activeSubNavItem.value = found || '';
|
||||||
|
} else if(path.includes('message-center')){
|
||||||
|
activeNavItem.value = 5; // 消息中心
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
883
src/views/teacher/ExamPages/ExamAnalysis.vue
Normal file
883
src/views/teacher/ExamPages/ExamAnalysis.vue
Normal file
@ -0,0 +1,883 @@
|
|||||||
|
<template>
|
||||||
|
<div class="exam-analysis-container">
|
||||||
|
<div class="header-section">
|
||||||
|
<n-button quaternary circle size="large" @click="goBack" class="back-button">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ArrowBackOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
<h1 class="title">试卷分析</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 考试基本信息 -->
|
||||||
|
<div class="exam-info-section">
|
||||||
|
<h2 class="exam-name">{{ examInfo.name }}</h2>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">考试分类:</span>
|
||||||
|
<span class="value">{{ examInfo.category }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">应考人数:</span>
|
||||||
|
<span class="value">{{ examInfo.totalStudents }}人(参考率{{ examInfo.participationRate }}%)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">总分:</span>
|
||||||
|
<span class="value">{{ examInfo.totalScore }}分</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">平均分:</span>
|
||||||
|
<span class="value">{{ examInfo.averageScore }}分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">及格分:</span>
|
||||||
|
<span class="value">{{ examInfo.passScore }}分</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">最高分:</span>
|
||||||
|
<span class="value">{{ examInfo.highestScore }}分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">考题时长:</span>
|
||||||
|
<span class="value">{{ examInfo.duration }}分钟(平均答题时长{{ examInfo.averageDuration }}分钟)</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">最低分:</span>
|
||||||
|
<span class="value">{{ examInfo.lowestScore }}分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">考试时间:</span>
|
||||||
|
<span class="value">{{ examInfo.startTime }} - {{ examInfo.endTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">平均时长:</span>
|
||||||
|
<span class="value">{{ examInfo.averageDuration }}分钟</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab切换区域 -->
|
||||||
|
<div class="tab-section">
|
||||||
|
<n-tabs v-model:value="activeTab" type="line" size="large">
|
||||||
|
<n-tab-pane name="ranking" tab="学生排名">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="table-section">
|
||||||
|
<n-data-table :columns="rankingColumns" :data="rankingData" :pagination="paginationConfig"
|
||||||
|
class="ranking-table" :scroll-x="1200" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="chart-section">
|
||||||
|
<v-chart ref="rankingChartRef" class="chart-container" :option="rankingChartOption"
|
||||||
|
:autoresize="true" />
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<n-tab-pane name="scores" tab="分数统计">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="score-stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>分数分布</h3>
|
||||||
|
<v-chart class="chart-container" :option="scoreDistributionChartOption"
|
||||||
|
:autoresize="true" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>及格率统计</h3>
|
||||||
|
<div class="pass-rate-info">
|
||||||
|
<div class="rate-item">
|
||||||
|
<span class="rate-label">及格人数:</span>
|
||||||
|
<span class="rate-value">{{ scoreStats.passCount }}人</span>
|
||||||
|
</div>
|
||||||
|
<div class="rate-item">
|
||||||
|
<span class="rate-label">不及格人数:</span>
|
||||||
|
<span class="rate-value">{{ scoreStats.failCount }}人</span>
|
||||||
|
</div>
|
||||||
|
<div class="rate-item">
|
||||||
|
<span class="rate-label">及格率:</span>
|
||||||
|
<span class="rate-value">{{ scoreStats.passRate }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<n-tab-pane name="average" tab="成绩分析">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="average-stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>学生成绩对比</h3>
|
||||||
|
<v-chart class="chart-container" :option="studentScoreChartOption" :autoresize="true" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>题型平均分</h3>
|
||||||
|
<div class="question-type-stats">
|
||||||
|
<div v-for="type in questionTypeStats" :key="type.type" class="type-stat">
|
||||||
|
<span class="type-name">{{ type.type }}:</span>
|
||||||
|
<span class="type-score">{{ type.averageScore }}分</span>
|
||||||
|
<span class="type-rate">(正确率{{ type.correctRate }}%)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed, onUnmounted } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { NTabs, NTabPane, NDataTable } from 'naive-ui';
|
||||||
|
import type { DataTableColumns } from 'naive-ui';
|
||||||
|
import { ArrowBackOutline } from '@vicons/ionicons5';
|
||||||
|
import VChart from 'vue-echarts';
|
||||||
|
import { use } from 'echarts/core';
|
||||||
|
import {
|
||||||
|
CanvasRenderer
|
||||||
|
} from 'echarts/renderers';
|
||||||
|
import {
|
||||||
|
BarChart,
|
||||||
|
PieChart,
|
||||||
|
LineChart
|
||||||
|
} from 'echarts/charts';
|
||||||
|
import {
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
GridComponent
|
||||||
|
} from 'echarts/components';
|
||||||
|
|
||||||
|
use([
|
||||||
|
CanvasRenderer,
|
||||||
|
BarChart,
|
||||||
|
PieChart,
|
||||||
|
LineChart,
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
GridComponent
|
||||||
|
]);
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const examId = ref<string | null>(null);
|
||||||
|
|
||||||
|
// 响应式窗口大小
|
||||||
|
const windowWidth = ref(window.innerWidth);
|
||||||
|
|
||||||
|
// Tab相关
|
||||||
|
const activeTab = ref('ranking');
|
||||||
|
|
||||||
|
// 考试基本信息
|
||||||
|
const examInfo = ref({
|
||||||
|
name: '试卷示例',
|
||||||
|
category: '考试分类',
|
||||||
|
totalStudents: 25,
|
||||||
|
participationRate: 92.00,
|
||||||
|
totalScore: 115,
|
||||||
|
averageScore: 76.5,
|
||||||
|
passScore: 69,
|
||||||
|
highestScore: 98,
|
||||||
|
duration: 60,
|
||||||
|
averageDuration: 45.2,
|
||||||
|
startTime: '2025-08-14 20:41:00',
|
||||||
|
endTime: '2025-08-17 20:41:00',
|
||||||
|
lowestScore: 42
|
||||||
|
});
|
||||||
|
|
||||||
|
// 学生成绩表格列定义
|
||||||
|
const rankingColumns: DataTableColumns = [
|
||||||
|
{ title: '排名', key: 'rank', width: 80, align: 'center', fixed: 'left' },
|
||||||
|
{ title: '学生姓名', key: 'studentName', width: 120, fixed: 'left' },
|
||||||
|
{ title: '学号', key: 'studentId', width: 120, align: 'center' },
|
||||||
|
{ title: '班级', key: 'className', width: 120, align: 'center' },
|
||||||
|
{ title: '总分', key: 'totalScore', width: 100, align: 'center' },
|
||||||
|
{ title: '得分', key: 'score', width: 100, align: 'center' },
|
||||||
|
{ title: '得分率', key: 'scoreRate', width: 100, align: 'center' },
|
||||||
|
{ title: '考试时长', key: 'examDuration', width: 120, align: 'center' },
|
||||||
|
{ title: '提交时间', key: 'submitTime', width: 150, align: 'center' },
|
||||||
|
{ title: '状态', key: 'status', width: 100, align: 'center' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 学生成绩表格数据
|
||||||
|
const rankingData = ref([
|
||||||
|
{
|
||||||
|
rank: 1,
|
||||||
|
studentName: '张三',
|
||||||
|
studentId: '2021001001',
|
||||||
|
className: '计算机1班',
|
||||||
|
totalScore: 115,
|
||||||
|
score: 98,
|
||||||
|
scoreRate: '85.22%',
|
||||||
|
examDuration: '45分钟',
|
||||||
|
submitTime: '2025-08-14 21:15:32',
|
||||||
|
status: '已完成'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rank: 2,
|
||||||
|
studentName: '李四',
|
||||||
|
studentId: '2021001002',
|
||||||
|
className: '计算机1班',
|
||||||
|
totalScore: 111,
|
||||||
|
score: 96,
|
||||||
|
scoreRate: '83.48%',
|
||||||
|
examDuration: '42分钟',
|
||||||
|
submitTime: '2025-08-14 21:12:18',
|
||||||
|
status: '已完成'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rank: 3,
|
||||||
|
studentName: '王五',
|
||||||
|
studentId: '2021002001',
|
||||||
|
className: '计算机2班',
|
||||||
|
totalScore: 110,
|
||||||
|
score: 94,
|
||||||
|
scoreRate: '81.74%',
|
||||||
|
examDuration: '38分钟',
|
||||||
|
submitTime: '2025-08-14 21:08:45',
|
||||||
|
status: '已完成'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rank: 4,
|
||||||
|
studentName: '赵六',
|
||||||
|
studentId: '2021003001',
|
||||||
|
className: '软件工程1班',
|
||||||
|
totalScore: 100,
|
||||||
|
score: 92,
|
||||||
|
scoreRate: '80.00%',
|
||||||
|
examDuration: '50分钟',
|
||||||
|
submitTime: '2025-08-14 21:25:12',
|
||||||
|
status: '已完成'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rank: 5,
|
||||||
|
studentName: '钱七',
|
||||||
|
studentId: '2021003002',
|
||||||
|
className: '软件工程1班',
|
||||||
|
totalScore: 99,
|
||||||
|
score: 89,
|
||||||
|
scoreRate: '77.39%',
|
||||||
|
examDuration: '47分钟',
|
||||||
|
submitTime: '2025-08-14 21:22:33',
|
||||||
|
status: '已完成'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rank: 6,
|
||||||
|
studentName: '孙八',
|
||||||
|
studentId: '2021004001',
|
||||||
|
className: '软件工程2班',
|
||||||
|
totalScore: 98,
|
||||||
|
score: 87,
|
||||||
|
scoreRate: '75.65%',
|
||||||
|
examDuration: '44分钟',
|
||||||
|
submitTime: '2025-08-14 21:19:28',
|
||||||
|
status: '已完成'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rank: 7,
|
||||||
|
studentName: '周九',
|
||||||
|
studentId: '2021005001',
|
||||||
|
className: '网络工程班',
|
||||||
|
totalScore: 95,
|
||||||
|
score: 85,
|
||||||
|
scoreRate: '73.91%',
|
||||||
|
examDuration: '52分钟',
|
||||||
|
submitTime: '2025-08-14 21:27:46',
|
||||||
|
status: '已完成'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rank: 8,
|
||||||
|
studentName: '吴十',
|
||||||
|
studentId: '2021002002',
|
||||||
|
className: '计算机2班',
|
||||||
|
totalScore: 88,
|
||||||
|
score: 82,
|
||||||
|
scoreRate: '71.30%',
|
||||||
|
examDuration: '41分钟',
|
||||||
|
submitTime: '2025-08-14 21:16:55',
|
||||||
|
status: '已完成'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rank: 9,
|
||||||
|
studentName: '郑一',
|
||||||
|
studentId: '2021001003',
|
||||||
|
className: '计算机1班',
|
||||||
|
totalScore: 80,
|
||||||
|
score: 78,
|
||||||
|
scoreRate: '67.83%',
|
||||||
|
examDuration: '55分钟',
|
||||||
|
submitTime: '2025-08-14 21:30:21',
|
||||||
|
status: '已完成'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rank: 10,
|
||||||
|
studentName: '王二',
|
||||||
|
studentId: '2021004002',
|
||||||
|
className: '软件工程2班',
|
||||||
|
totalScore: 79,
|
||||||
|
score: 75,
|
||||||
|
scoreRate: '65.22%',
|
||||||
|
examDuration: '48分钟',
|
||||||
|
submitTime: '2025-08-14 21:23:17',
|
||||||
|
status: '已完成'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
const paginationConfig = ref({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
showSizePicker: true,
|
||||||
|
pageSizes: [10, 20, 50],
|
||||||
|
showQuickJumper: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分数统计
|
||||||
|
const scoreStats = ref({
|
||||||
|
passCount: 115,
|
||||||
|
failCount: 28,
|
||||||
|
passRate: 80.42
|
||||||
|
});
|
||||||
|
|
||||||
|
// 题型统计
|
||||||
|
const questionTypeStats = ref([
|
||||||
|
{ type: '单选题', averageScore: 18.5, correctRate: 74 },
|
||||||
|
{ type: '多选题', averageScore: 12.8, correctRate: 64 },
|
||||||
|
{ type: '判断题', averageScore: 8.9, correctRate: 89 },
|
||||||
|
{ type: '填空题', averageScore: 15.2, correctRate: 76 },
|
||||||
|
{ type: '简答题', averageScore: 21.1, correctRate: 70 }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 学生成绩分布图表配置
|
||||||
|
// const rankingChartOption = computed(() => ({
|
||||||
|
// title: {
|
||||||
|
// text: '学生成绩分布统计',
|
||||||
|
// left: 'center',
|
||||||
|
// textStyle: {
|
||||||
|
// fontSize: windowWidth.value < 768 ? 14 : 16
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// tooltip: {
|
||||||
|
// trigger: 'axis',
|
||||||
|
// axisPointer: {
|
||||||
|
// type: 'shadow'
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// grid: {
|
||||||
|
// left: windowWidth.value < 768 ? '10%' : '3%',
|
||||||
|
// right: windowWidth.value < 768 ? '10%' : '4%',
|
||||||
|
// bottom: windowWidth.value < 768 ? '15%' : '3%',
|
||||||
|
// containLabel: true
|
||||||
|
// },
|
||||||
|
// xAxis: {
|
||||||
|
// type: 'category',
|
||||||
|
// data: ['0-20分', '21-40分', '41-60分', '61-80分', '81-100分'],
|
||||||
|
// axisLabel: {
|
||||||
|
// fontSize: windowWidth.value < 768 ? 10 : 12,
|
||||||
|
// rotate: windowWidth.value < 480 ? 45 : 0
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// yAxis: {
|
||||||
|
// type: 'value',
|
||||||
|
// name: '人数',
|
||||||
|
// nameTextStyle: {
|
||||||
|
// fontSize: windowWidth.value < 768 ? 10 : 12
|
||||||
|
// },
|
||||||
|
// axisLabel: {
|
||||||
|
// fontSize: windowWidth.value < 768 ? 10 : 12
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// series: [
|
||||||
|
// {
|
||||||
|
// name: '人数',
|
||||||
|
// type: 'bar',
|
||||||
|
// data: [0, 1, 2, 4, 3], // 根据学生成绩数据统计
|
||||||
|
// itemStyle: {
|
||||||
|
// color: '#5470c6'
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }));
|
||||||
|
|
||||||
|
// 学生分数柱状图配置
|
||||||
|
const studentScoreChartOption = computed(() => ({
|
||||||
|
title: {
|
||||||
|
text: 'Top 10 学生成绩排行',
|
||||||
|
left: 'center',
|
||||||
|
textStyle: {
|
||||||
|
fontSize: windowWidth.value < 768 ? 14 : 16
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow'
|
||||||
|
},
|
||||||
|
formatter: '{b}<br/>成绩: {c}分'
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: windowWidth.value < 768 ? '10%' : '3%',
|
||||||
|
right: windowWidth.value < 768 ? '10%' : '4%',
|
||||||
|
bottom: windowWidth.value < 768 ? '15%' : '3%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: rankingData.value.slice(0, 10).map(item => item.studentName),
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: windowWidth.value < 768 ? 10 : 12,
|
||||||
|
rotate: windowWidth.value < 480 ? 45 : 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '分数',
|
||||||
|
nameTextStyle: {
|
||||||
|
fontSize: windowWidth.value < 768 ? 10 : 12
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: windowWidth.value < 768 ? 10 : 12
|
||||||
|
},
|
||||||
|
max: Math.max(...rankingData.value.slice(0, 10).map(item => item.totalScore)) + 10,
|
||||||
|
min: 0
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '总分',
|
||||||
|
type: 'bar',
|
||||||
|
data: rankingData.value.slice(0, 10).map(item => item.totalScore),
|
||||||
|
itemStyle: {
|
||||||
|
color: '#91cc75'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 分数分布图表配置
|
||||||
|
const scoreDistributionChartOption = computed(() => ({
|
||||||
|
title: {
|
||||||
|
text: '分数分布',
|
||||||
|
left: 'center',
|
||||||
|
textStyle: {
|
||||||
|
fontSize: windowWidth.value < 768 ? 14 : 16
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item'
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orient: windowWidth.value < 768 ? 'horizontal' : 'vertical',
|
||||||
|
left: windowWidth.value < 768 ? 'center' : 'left',
|
||||||
|
top: windowWidth.value < 768 ? 'bottom' : 'middle',
|
||||||
|
itemWidth: windowWidth.value < 768 ? 15 : 25,
|
||||||
|
itemHeight: windowWidth.value < 768 ? 10 : 14,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: windowWidth.value < 768 ? 10 : 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '分数分布',
|
||||||
|
type: 'pie',
|
||||||
|
radius: windowWidth.value < 768 ? '40%' : '50%',
|
||||||
|
center: windowWidth.value < 768 ? ['50%', '40%'] : ['50%', '50%'],
|
||||||
|
data: [
|
||||||
|
{ value: 40, name: '优秀(90-100分)' },
|
||||||
|
{ value: 68, name: '良好(80-89分)' },
|
||||||
|
{ value: 35, name: '中等(70-79分)' },
|
||||||
|
{ value: 22, name: '及格(60-69分)' },
|
||||||
|
{ value: 28, name: '不及格(0-59分)' }
|
||||||
|
],
|
||||||
|
label: {
|
||||||
|
fontSize: windowWidth.value < 768 ? 10 : 12
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
examId.value = route.query.examId as string || null;
|
||||||
|
|
||||||
|
// 添加窗口大小变化监听器
|
||||||
|
const handleResize = () => {
|
||||||
|
windowWidth.value = window.innerWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
// 组件卸载时移除监听器
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.exam-analysis-container {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #E6E6E6;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
margin: 0 8px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 考试信息区域 */
|
||||||
|
.exam-info-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #1890ff;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #666;
|
||||||
|
min-width: 100px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab区域 */
|
||||||
|
.tab-section {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 表格区域 */
|
||||||
|
.table-section {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-table {
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图表区域 */
|
||||||
|
.chart-section {
|
||||||
|
margin-top: 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分数统计页面 */
|
||||||
|
.score-stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
padding: 24px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #1890ff;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pass-rate-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-item:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-label {
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-value {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平均分页面 */
|
||||||
|
.average-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-type-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-stat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-stat:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-name {
|
||||||
|
color: #666;
|
||||||
|
min-width: 80px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-score {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-rate {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
|
||||||
|
.score-stats-grid,
|
||||||
|
.average-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
height: 350px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.exam-analysis-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
min-width: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-item,
|
||||||
|
.type-stat {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-rate {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.exam-analysis-container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-name {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-table {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 深色模式支持 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.exam-analysis-container {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-info-section,
|
||||||
|
.stat-card,
|
||||||
|
.chart-section {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
border-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-item,
|
||||||
|
.type-stat {
|
||||||
|
background-color: #333;
|
||||||
|
border-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label,
|
||||||
|
.type-name {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value,
|
||||||
|
.rate-value,
|
||||||
|
.type-score {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -108,6 +108,9 @@ const createColumns = ({
|
|||||||
render(row) {
|
render(row) {
|
||||||
const buttons: VNode[] = [];
|
const buttons: VNode[] = [];
|
||||||
if (row.status === '发布中') {
|
if (row.status === '发布中') {
|
||||||
|
buttons.push(
|
||||||
|
h(NButton, { size: 'small', type: 'primary', ghost: true, style: 'margin: 0 3px;', onClick: () => handleAction('试卷分析', row) }, { default: () => '试卷分析' })
|
||||||
|
);
|
||||||
buttons.push(
|
buttons.push(
|
||||||
h(NButton, { size: 'small', type: 'primary', ghost: true, style: 'margin: 0 3px;', onClick: () => handleAction('批阅', row) }, { default: () => '批阅' })
|
h(NButton, { size: 'small', type: 'primary', ghost: true, style: 'margin: 0 3px;', onClick: () => handleAction('批阅', row) }, { default: () => '批阅' })
|
||||||
);
|
);
|
||||||
@ -146,6 +149,10 @@ const examData = ref<Exam[]>([
|
|||||||
|
|
||||||
const columns = createColumns({
|
const columns = createColumns({
|
||||||
handleAction: (action, row) => {
|
handleAction: (action, row) => {
|
||||||
|
if(action === '试卷分析'){
|
||||||
|
router.push({ name: 'ExamAnalysis', query: { examId: row.id } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
message.info(`执行操作: ${action} on row ${row.id}`);
|
message.info(`执行操作: ${action} on row ${row.id}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
147
src/views/teacher/message/MessageCenter.vue
Normal file
147
src/views/teacher/message/MessageCenter.vue
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<div class="message-center">
|
||||||
|
<!-- 顶部Tab导航 -->
|
||||||
|
<div class="tab-container">
|
||||||
|
<n-tabs v-model:value="activeTab" type="line" size="large" class="message-tabs">
|
||||||
|
<n-tab-pane name="notification" tab="即时消息">
|
||||||
|
<template #tab>
|
||||||
|
<div class="tab-item">
|
||||||
|
<span>即时消息</span>
|
||||||
|
<n-badge :value="notificationCount" :show="notificationCount > 0" class="tab-badge">
|
||||||
|
</n-badge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<n-tab-pane name="comment" tab="评论和@">
|
||||||
|
<template #tab>
|
||||||
|
<div class="tab-item">
|
||||||
|
<span>评论和@</span>
|
||||||
|
<n-badge :value="commentCount" :show="commentCount > 0" class="tab-badge">
|
||||||
|
</n-badge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<n-tab-pane name="favorite" tab="赞和收藏">
|
||||||
|
<template #tab>
|
||||||
|
<div class="tab-item">
|
||||||
|
<span>赞和收藏</span>
|
||||||
|
<n-badge :value="favoriteCount" :show="favoriteCount > 0" class="tab-badge">
|
||||||
|
</n-badge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<n-tab-pane name="system" tab="系统消息">
|
||||||
|
<template #tab>
|
||||||
|
<div class="tab-item">
|
||||||
|
<span>系统消息</span>
|
||||||
|
<n-badge :value="systemCount" :show="systemCount > 0" class="tab-badge">
|
||||||
|
</n-badge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab内容区域 -->
|
||||||
|
<div class="tab-content">
|
||||||
|
<div v-show="activeTab === 'notification'">
|
||||||
|
<NotificationMessages />
|
||||||
|
</div>
|
||||||
|
<div v-show="activeTab === 'comment'">
|
||||||
|
<CommentLikes />
|
||||||
|
</div>
|
||||||
|
<div v-show="activeTab === 'favorite'">
|
||||||
|
<FavoriteMessages />
|
||||||
|
</div>
|
||||||
|
<div v-show="activeTab === 'system'">
|
||||||
|
<SystemMessages />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { NBadge, NTabs, NTabPane } from 'naive-ui'
|
||||||
|
|
||||||
|
// 导入子组件
|
||||||
|
import NotificationMessages from './components/NotificationMessages.vue'
|
||||||
|
import CommentLikes from './components/CommentLikes.vue'
|
||||||
|
import FavoriteMessages from './components/FavoriteMessages.vue'
|
||||||
|
import SystemMessages from './components/SystemMessages.vue'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const activeTab = ref('notification') // 当前激活的tab
|
||||||
|
|
||||||
|
// 各类消息数量(角标显示)
|
||||||
|
const notificationCount = ref(5) // 即时消息数量
|
||||||
|
const commentCount = ref(3) // 评论和@数量
|
||||||
|
const favoriteCount = ref(0) // 赞和收藏数量
|
||||||
|
const systemCount = ref(0) // 系统消息数量
|
||||||
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始化逻辑
|
||||||
|
loadMessageCounts()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载各类消息数量
|
||||||
|
const loadMessageCounts = () => {
|
||||||
|
// TODO: 这里后续可以调用API获取实际的消息数量
|
||||||
|
// 暂时使用模拟数据
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.message-center {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-tabs {
|
||||||
|
--n-tab-text-color: #666;
|
||||||
|
--n-tab-text-color-active: #1890ff;
|
||||||
|
--n-bar-color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-badge {
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整badge样式 */
|
||||||
|
:deep(.n-badge) {
|
||||||
|
--n-color: #ff4d4f;
|
||||||
|
--n-text-color: #fff;
|
||||||
|
--n-font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整tabs整体样式 */
|
||||||
|
:deep(.n-tabs .n-tabs-nav .n-tabs-nav-scroll-content) {
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tabs .n-tabs-tab) {
|
||||||
|
padding: 12px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
550
src/views/teacher/message/components/CommentLikes.vue
Normal file
550
src/views/teacher/message/components/CommentLikes.vue
Normal file
@ -0,0 +1,550 @@
|
|||||||
|
<template>
|
||||||
|
<div class="message-center">
|
||||||
|
<!-- 消息列表 -->
|
||||||
|
<div class="message-list">
|
||||||
|
<div v-for="message in messages" :key="message.id" class="message-item">
|
||||||
|
<!-- 用户头像 -->
|
||||||
|
<div class="avatar-container">
|
||||||
|
<img :src="message.avatar" :alt="message.username" class="avatar" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息内容 -->
|
||||||
|
<div class="message-content">
|
||||||
|
<!-- 评论 -->
|
||||||
|
<div class="message-header" v-if="message.type === 0">
|
||||||
|
<div>
|
||||||
|
<span class="username">
|
||||||
|
{{ message.username }}评论了我:
|
||||||
|
</span>
|
||||||
|
<span class="content">{{ message.content }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="timestamp">{{ message.timestamp }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- @ -->
|
||||||
|
<div class="message-header" v-if="message.type === 1">
|
||||||
|
<div>
|
||||||
|
<span class="username">
|
||||||
|
{{ message.username }}@了我:
|
||||||
|
</span>
|
||||||
|
<span class="content">{{ message.content }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="timestamp">{{ message.timestamp }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message-text">回复我的课程: <span class="course-info">{{ message.courseInfo }}</span></div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="message-btns">
|
||||||
|
<div class="message-actions">
|
||||||
|
<button class="action-btn" @click="toggleReply(message.id)">
|
||||||
|
<n-icon size="16">
|
||||||
|
<ChatbubbleEllipsesOutline />
|
||||||
|
</n-icon>
|
||||||
|
回复
|
||||||
|
</button>
|
||||||
|
<button class="action-btn" @click="toggleFavorite(message.id)">
|
||||||
|
<!-- <i class="icon-favorite" :class="{ active: message.isFavorited }"></i> -->
|
||||||
|
<n-icon size="16">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 32 32">
|
||||||
|
<path
|
||||||
|
d="M26 12h-6V6a3.003 3.003 0 0 0-3-3h-2.133a2.01 2.01 0 0 0-1.98 1.717l-.845 5.917L8.465 16H2v14h21a7.008 7.008 0 0 0 7-7v-7a4.005 4.005 0 0 0-4-4zM8 28H4V18h4zm20-5a5.006 5.006 0 0 1-5 5H10V17.303l3.958-5.937l.91-6.366H17a1 1 0 0 1 1 1v8h8a2.002 2.002 0 0 1 2 2z"
|
||||||
|
fill="currentColor"></path>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
点赞
|
||||||
|
</button>
|
||||||
|
<button class="action-btn delete-btn">
|
||||||
|
<n-icon size="16">
|
||||||
|
<TrashOutline />
|
||||||
|
</n-icon>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button class="action-btn delete-btn">
|
||||||
|
<n-icon size="16">
|
||||||
|
<WarningOutline />
|
||||||
|
</n-icon>
|
||||||
|
举报
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 回复输入框 -->
|
||||||
|
<div v-if="message.showReplyBox" class="reply-box">
|
||||||
|
<textarea v-model="message.replyContent" placeholder="回复内容:" class="reply-textarea" rows="3"></textarea>
|
||||||
|
<div class="reply-actions">
|
||||||
|
<button class="cancel-btn" @click="cancelReply(message.id)">取消</button>
|
||||||
|
<button class="send-btn" @click="sendReply(message.id)">发送</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination">
|
||||||
|
<button class="page-btn" :disabled="currentPage === 1" @click="goToPage(1)">首页</button>
|
||||||
|
<button class="page-btn" :disabled="currentPage === 1" @click="goToPage(currentPage - 1)">上一页</button>
|
||||||
|
|
||||||
|
<button v-for="page in visiblePages" :key="page" class="page-btn" :class="{ active: page === currentPage }"
|
||||||
|
@click="goToPage(page)">
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span v-if="showEllipsis" class="ellipsis">...</span>
|
||||||
|
<button class="page-btn" @click="goToPage(totalPages)">{{ totalPages }}</button>
|
||||||
|
|
||||||
|
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">下一页</button>
|
||||||
|
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(totalPages)">尾页</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { ChatbubbleEllipsesOutline, TrashOutline, WarningOutline } from '@vicons/ionicons5'
|
||||||
|
|
||||||
|
// 消息类型定义
|
||||||
|
interface Message {
|
||||||
|
id: number
|
||||||
|
type: number
|
||||||
|
username: string
|
||||||
|
avatar: string
|
||||||
|
courseInfo: string
|
||||||
|
content: string
|
||||||
|
timestamp: string
|
||||||
|
isLiked: boolean
|
||||||
|
isFavorited: boolean
|
||||||
|
showReplyBox: boolean
|
||||||
|
replyContent: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const messages = ref<Message[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 1,
|
||||||
|
username: '王建华化学老师',
|
||||||
|
avatar: 'https://picsum.photos/200/200',
|
||||||
|
courseInfo: '《教师小学期制实验》',
|
||||||
|
content: '这里是老师留言的内容了',
|
||||||
|
timestamp: '7月20日',
|
||||||
|
isLiked: false,
|
||||||
|
isFavorited: false,
|
||||||
|
showReplyBox: false,
|
||||||
|
replyContent: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: 0,
|
||||||
|
username: '叶仲学习分子',
|
||||||
|
avatar: 'https://picsum.photos/200/200',
|
||||||
|
courseInfo: '《教师小学期制实验》',
|
||||||
|
content: '还好期末考成分学的记忆',
|
||||||
|
timestamp: '7月20日 12:41',
|
||||||
|
isLiked: false,
|
||||||
|
isFavorited: false,
|
||||||
|
showReplyBox: false,
|
||||||
|
replyContent: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
type: 1,
|
||||||
|
username: '课程学习端课学',
|
||||||
|
avatar: 'https://picsum.photos/200/200',
|
||||||
|
courseInfo: '《教师小学期制实验》',
|
||||||
|
content: '没事多看看课程你就懂了',
|
||||||
|
timestamp: '7月20日',
|
||||||
|
isLiked: false,
|
||||||
|
isFavorited: false,
|
||||||
|
showReplyBox: false,
|
||||||
|
replyContent: ''
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 分页相关
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const totalPages = ref(29)
|
||||||
|
|
||||||
|
// 计算显示的页码
|
||||||
|
const visiblePages = computed(() => {
|
||||||
|
const pages = []
|
||||||
|
const start = Math.max(1, currentPage.value - 2)
|
||||||
|
const end = Math.min(totalPages.value, currentPage.value + 2)
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否显示省略号
|
||||||
|
const showEllipsis = computed(() => {
|
||||||
|
return currentPage.value + 2 < totalPages.value - 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onMounted(() => {
|
||||||
|
loadMessages()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法定义
|
||||||
|
const loadMessages = () => {
|
||||||
|
// TODO: 调用API加载消息数据
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFavorite = (messageId: number) => {
|
||||||
|
const message = messages.value.find(m => m.id === messageId)
|
||||||
|
if (message) {
|
||||||
|
message.isFavorited = !message.isFavorited
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleReply = (messageId: number) => {
|
||||||
|
const message = messages.value.find(m => m.id === messageId)
|
||||||
|
if (message) {
|
||||||
|
// 先隐藏所有其他消息的回复框
|
||||||
|
messages.value.forEach(m => {
|
||||||
|
if (m.id !== messageId) {
|
||||||
|
m.showReplyBox = false
|
||||||
|
m.replyContent = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 切换当前消息的回复框状态
|
||||||
|
message.showReplyBox = !message.showReplyBox
|
||||||
|
if (message.showReplyBox) {
|
||||||
|
message.replyContent = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelReply = (messageId: number) => {
|
||||||
|
const message = messages.value.find(m => m.id === messageId)
|
||||||
|
if (message) {
|
||||||
|
message.showReplyBox = false
|
||||||
|
message.replyContent = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendReply = (messageId: number) => {
|
||||||
|
const message = messages.value.find(m => m.id === messageId)
|
||||||
|
if (message && message.replyContent.trim()) {
|
||||||
|
// TODO: 调用API发送回复
|
||||||
|
console.log('发送回复:', message.replyContent)
|
||||||
|
message.showReplyBox = false
|
||||||
|
message.replyContent = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToPage = (page: number) => {
|
||||||
|
if (page >= 1 && page <= totalPages.value) {
|
||||||
|
currentPage.value = page
|
||||||
|
loadMessages()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.message-center {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-list {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item:hover {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item.unread {
|
||||||
|
background-color: #f6f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread-dot {
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
top: 20px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container {
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
color: #000;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-info {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
color: #999999;
|
||||||
|
background-color: #F5F8FB;
|
||||||
|
padding: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-btns{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 0;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.delete-btn:hover {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn i {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn i.active {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图标样式 - 使用伪元素模拟图标 */
|
||||||
|
.icon-like::before {
|
||||||
|
content: "👍";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-favorite::before {
|
||||||
|
content: "⭐";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-reply::before {
|
||||||
|
content: "💬";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-delete::before {
|
||||||
|
content: "🗑️";
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-box {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 80px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-textarea:focus {
|
||||||
|
border-color: #1890ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-textarea::placeholder {
|
||||||
|
color: #bfbfbf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn,
|
||||||
|
.send-btn {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-btn {
|
||||||
|
background: #1890ff;
|
||||||
|
border: 1px solid #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-btn:hover {
|
||||||
|
background: #40a9ff;
|
||||||
|
border-color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 32px 20px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
background: #fff;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn:hover:not(:disabled) {
|
||||||
|
border-color: #1890ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn.active {
|
||||||
|
background: #1890ff;
|
||||||
|
border-color: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn:disabled {
|
||||||
|
color: #bfbfbf;
|
||||||
|
border-color: #f0f0f0;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis {
|
||||||
|
color: #bfbfbf;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.message-item {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn {
|
||||||
|
min-width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
513
src/views/teacher/message/components/FavoriteMessages.vue
Normal file
513
src/views/teacher/message/components/FavoriteMessages.vue
Normal file
@ -0,0 +1,513 @@
|
|||||||
|
<template>
|
||||||
|
<div class="message-center">
|
||||||
|
<!-- 消息列表 -->
|
||||||
|
<div class="message-list">
|
||||||
|
<div v-for="message in messages" :key="message.id" class="message-item">
|
||||||
|
<!-- 用户头像 -->
|
||||||
|
<div class="avatar-container">
|
||||||
|
<img :src="message.avatar" :alt="message.username" class="avatar" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息内容 -->
|
||||||
|
<div class="message-content">
|
||||||
|
<!-- 点赞消息 -->
|
||||||
|
<div class="message-header" v-if="message.type === 0">
|
||||||
|
<div>
|
||||||
|
<span class="username">
|
||||||
|
{{ message.username }}赞了我的评论:
|
||||||
|
</span>
|
||||||
|
<span class="content">{{ message.content }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="timestamp">{{ message.timestamp }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- 收藏消息 -->
|
||||||
|
<div class="message-header" v-if="message.type === 1">
|
||||||
|
<div class="header-left">
|
||||||
|
<div>
|
||||||
|
<span class="username">
|
||||||
|
{{ message.username }}收藏了我的课程:
|
||||||
|
</span>
|
||||||
|
<span class="course-info">{{ message.courseInfo }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="timestamp">{{ message.timestamp }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- 课程封面图片 - 只有收藏消息才显示 -->
|
||||||
|
<div v-if="message.type === 1 && message.courseImage" class="course-image-container">
|
||||||
|
<img :src="message.courseImage" :alt="message.courseInfo" class="course-image" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message-text" v-if="message.type === 0">课程:
|
||||||
|
<span class="course-info">{{ message.courseInfo }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination">
|
||||||
|
<button class="page-btn" :disabled="currentPage === 1" @click="goToPage(1)">首页</button>
|
||||||
|
<button class="page-btn" :disabled="currentPage === 1" @click="goToPage(currentPage - 1)">上一页</button>
|
||||||
|
|
||||||
|
<button v-for="page in visiblePages" :key="page" class="page-btn" :class="{ active: page === currentPage }"
|
||||||
|
@click="goToPage(page)">
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span v-if="showEllipsis" class="ellipsis">...</span>
|
||||||
|
<button class="page-btn" @click="goToPage(totalPages)">{{ totalPages }}</button>
|
||||||
|
|
||||||
|
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">下一页</button>
|
||||||
|
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(totalPages)">尾页</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
|
||||||
|
// 消息类型定义
|
||||||
|
interface Message {
|
||||||
|
id: number
|
||||||
|
type: number // 0-点赞, 1-收藏
|
||||||
|
username: string
|
||||||
|
avatar: string
|
||||||
|
courseInfo: string
|
||||||
|
content: string
|
||||||
|
timestamp: string
|
||||||
|
courseImage?: string
|
||||||
|
isLiked: boolean
|
||||||
|
isFavorited: boolean
|
||||||
|
showReplyBox: boolean
|
||||||
|
replyContent: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const messages = ref<Message[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 0, // 点赞评论 - 有评论内容,无图片
|
||||||
|
username: '王建华化学老师',
|
||||||
|
avatar: 'https://picsum.photos/200/200',
|
||||||
|
courseInfo: '《教师小学期制实验》',
|
||||||
|
content: '这里老师营养饮用的说明了',
|
||||||
|
timestamp: '7月20日\n12:41',
|
||||||
|
isLiked: false,
|
||||||
|
isFavorited: false,
|
||||||
|
showReplyBox: false,
|
||||||
|
replyContent: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: 1, // 收藏课程 - 无评论内容,有图片
|
||||||
|
username: '王建华化学老师',
|
||||||
|
avatar: 'https://picsum.photos/200/200',
|
||||||
|
courseInfo: '《教师小学期制实验》',
|
||||||
|
content: '',
|
||||||
|
timestamp: '7月20日\n12:41',
|
||||||
|
courseImage: 'https://picsum.photos/300/200',
|
||||||
|
isLiked: false,
|
||||||
|
isFavorited: false,
|
||||||
|
showReplyBox: false,
|
||||||
|
replyContent: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
type: 0, // 点赞评论 - 有评论内容,无图片
|
||||||
|
username: '王建华化学老师',
|
||||||
|
avatar: 'https://picsum.photos/200/200',
|
||||||
|
courseInfo: '《教师小学期制实验》',
|
||||||
|
content: '这里老师营养饮用的说明了',
|
||||||
|
timestamp: '7月20日\n12:41',
|
||||||
|
isLiked: false,
|
||||||
|
isFavorited: false,
|
||||||
|
showReplyBox: false,
|
||||||
|
replyContent: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
type: 1, // 收藏课程 - 无评论内容,有图片
|
||||||
|
username: '王建华化学老师',
|
||||||
|
avatar: 'https://picsum.photos/200/200',
|
||||||
|
courseInfo: '《教师小学期制实验》',
|
||||||
|
content: '',
|
||||||
|
timestamp: '7月20日\n12:41',
|
||||||
|
courseImage: 'https://picsum.photos/300/200',
|
||||||
|
isLiked: false,
|
||||||
|
isFavorited: false,
|
||||||
|
showReplyBox: false,
|
||||||
|
replyContent: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
type: 1, // 收藏课程 - 无评论内容,有图片
|
||||||
|
username: '王建华化学老师',
|
||||||
|
avatar: 'https://picsum.photos/200/200',
|
||||||
|
courseInfo: '《教师小学期制实验》',
|
||||||
|
content: '',
|
||||||
|
timestamp: '7月20日\n12:41',
|
||||||
|
courseImage: 'https://picsum.photos/300/200',
|
||||||
|
isLiked: false,
|
||||||
|
isFavorited: false,
|
||||||
|
showReplyBox: false,
|
||||||
|
replyContent: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
type: 0, // 点赞评论 - 有评论内容,无图片
|
||||||
|
username: '王建华化学老师',
|
||||||
|
avatar: 'https://picsum.photos/200/200',
|
||||||
|
courseInfo: '《教师小学期制实验》',
|
||||||
|
content: '这里老师营养饮用的说明了',
|
||||||
|
timestamp: '7月20日\n12:41',
|
||||||
|
isLiked: false,
|
||||||
|
isFavorited: false,
|
||||||
|
showReplyBox: false,
|
||||||
|
replyContent: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
type: 0, // 点赞评论 - 有评论内容,无图片
|
||||||
|
username: '王建华化学老师',
|
||||||
|
avatar: 'https://picsum.photos/200/200',
|
||||||
|
courseInfo: '《教师小学期制实验》',
|
||||||
|
content: '这里老师营养饮用的说明了',
|
||||||
|
timestamp: '7月20日\n12:41',
|
||||||
|
isLiked: false,
|
||||||
|
isFavorited: false,
|
||||||
|
showReplyBox: false,
|
||||||
|
replyContent: ''
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 分页相关
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const totalPages = ref(29)
|
||||||
|
|
||||||
|
// 计算显示的页码
|
||||||
|
const visiblePages = computed(() => {
|
||||||
|
const pages = []
|
||||||
|
const start = Math.max(1, currentPage.value - 2)
|
||||||
|
const end = Math.min(totalPages.value, currentPage.value + 2)
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否显示省略号
|
||||||
|
const showEllipsis = computed(() => {
|
||||||
|
return currentPage.value + 2 < totalPages.value - 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onMounted(() => {
|
||||||
|
loadMessages()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法定义
|
||||||
|
const loadMessages = () => {
|
||||||
|
// TODO: 调用API加载消息数据
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToPage = (page: number) => {
|
||||||
|
if (page >= 1 && page <= totalPages.value) {
|
||||||
|
currentPage.value = page
|
||||||
|
loadMessages()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.message-center {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-list {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item:hover {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container {
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left{
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
color: #000;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-info{
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-info {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
color: #999999;
|
||||||
|
background-color: #F5F8FB;
|
||||||
|
padding: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-image-container {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-image {
|
||||||
|
max-width: 150px;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-btns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 0;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.delete-btn:hover {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-box {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 80px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-textarea:focus {
|
||||||
|
border-color: #1890ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-textarea::placeholder {
|
||||||
|
color: #bfbfbf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn,
|
||||||
|
.send-btn {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-btn {
|
||||||
|
background: #1890ff;
|
||||||
|
border: 1px solid #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-btn:hover {
|
||||||
|
background: #40a9ff;
|
||||||
|
border-color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 32px 20px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
background: #fff;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn:hover:not(:disabled) {
|
||||||
|
border-color: #1890ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn.active {
|
||||||
|
background: #1890ff;
|
||||||
|
border-color: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn:disabled {
|
||||||
|
color: #bfbfbf;
|
||||||
|
border-color: #f0f0f0;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis {
|
||||||
|
color: #bfbfbf;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.message-item {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-image {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn {
|
||||||
|
min-width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
208
src/views/teacher/message/components/MessageInput.vue
Normal file
208
src/views/teacher/message/components/MessageInput.vue
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<div class="message-input-container">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<n-input
|
||||||
|
v-model:value="messageText"
|
||||||
|
type="textarea"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:autosize="{ minRows: 1, maxRows: 4 }"
|
||||||
|
:bordered="false"
|
||||||
|
class="message-input"
|
||||||
|
@keydown="handleKeyDown"
|
||||||
|
@input="handleInput"
|
||||||
|
/>
|
||||||
|
<div class="input-actions">
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
size="medium"
|
||||||
|
:disabled="!canSend"
|
||||||
|
class="send-button"
|
||||||
|
@click="handleSend"
|
||||||
|
>
|
||||||
|
发送
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { NInput, NButton } from 'naive-ui'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
interface Props {
|
||||||
|
placeholder?: string
|
||||||
|
maxLength?: number
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
placeholder: '请输入消息内容...',
|
||||||
|
maxLength: 500,
|
||||||
|
disabled: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits<{
|
||||||
|
send: [message: string]
|
||||||
|
input: [value: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const messageText = ref('')
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const canSend = computed(() => {
|
||||||
|
return messageText.value.trim().length > 0 && !props.disabled
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
const handleSend = () => {
|
||||||
|
if (canSend.value) {
|
||||||
|
emit('send', messageText.value.trim())
|
||||||
|
messageText.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInput = (value: string) => {
|
||||||
|
emit('input', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
// Ctrl/Cmd + Enter 发送消息
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
||||||
|
event.preventDefault()
|
||||||
|
handleSend()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法供父组件调用
|
||||||
|
defineExpose({
|
||||||
|
focus: () => {
|
||||||
|
// 可以在这里添加聚焦逻辑
|
||||||
|
},
|
||||||
|
clear: () => {
|
||||||
|
messageText.value = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.message-input-container {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
border-top: 1px solid #e8e8e8;
|
||||||
|
padding: 16px 20px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input {
|
||||||
|
flex: 1;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
min-height: 44px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input:hover {
|
||||||
|
background: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input:focus-within {
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button {
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button:not(:disabled):hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义Naive UI样式 */
|
||||||
|
:deep(.n-input .n-input__textarea) {
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-input .n-input__textarea::placeholder) {
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-input.n-input--focus .n-input__textarea) {
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-button.n-button--primary-type) {
|
||||||
|
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-button.n-button--primary-type:not(.n-button--disabled):hover) {
|
||||||
|
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-button.n-button--primary-type.n-button--disabled) {
|
||||||
|
background: #d9d9d9;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.message-input-container {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button {
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input {
|
||||||
|
padding: 10px 14px;
|
||||||
|
min-height: 40px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
780
src/views/teacher/message/components/NotificationMessages.vue
Normal file
780
src/views/teacher/message/components/NotificationMessages.vue
Normal file
@ -0,0 +1,780 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chat-container">
|
||||||
|
<!-- 左侧联系人/群组列表 -->
|
||||||
|
<div class="contacts-panel">
|
||||||
|
<!-- 联系人列表头部 -->
|
||||||
|
<div class="contacts-header">
|
||||||
|
<h3 class="contacts-title">全部信息</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 联系人列表 -->
|
||||||
|
<div class="contacts-list">
|
||||||
|
<div v-for="contact in contacts" :key="contact.id" class="contact-item"
|
||||||
|
:class="{ active: contact.id === activeContactId, unread: contact.unreadCount > 0 }"
|
||||||
|
@click="selectContact(contact.id)">
|
||||||
|
<div class="contact-avatar">
|
||||||
|
<img :src="contact.avatar" :alt="contact.name" />
|
||||||
|
<div v-if="contact.type === 'group'" class="group-indicator">
|
||||||
|
<n-icon size="12" color="#fff">
|
||||||
|
<PeopleOutline />
|
||||||
|
</n-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="contact-info">
|
||||||
|
<div class="contact-header">
|
||||||
|
<span class="contact-name">{{ contact.name }}</span>
|
||||||
|
<span class="contact-time">{{ contact.lastMessageTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="contact-preview">
|
||||||
|
<span class="last-message">{{ contact.lastMessage }}</span>
|
||||||
|
<n-badge v-if="contact.unreadCount > 0" :value="contact.unreadCount" :max="99" class="unread-badge" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧聊天区域 -->
|
||||||
|
<div class="chat-panel">
|
||||||
|
<div v-if="!activeContactId" class="chat-empty">
|
||||||
|
<div class="empty-content">
|
||||||
|
<n-icon size="64" color="#d9d9d9">
|
||||||
|
<ChatbubbleEllipsesOutline />
|
||||||
|
</n-icon>
|
||||||
|
<p class="empty-text">选择一个对话开始聊天</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="chat-content">
|
||||||
|
<!-- 聊天头部 -->
|
||||||
|
<div class="chat-header">
|
||||||
|
<div></div>
|
||||||
|
<div class="chat-user-info">
|
||||||
|
<div class="chat-user-details">
|
||||||
|
<h4 class="chat-user-name">{{ activeContact?.name }}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chat-actions">
|
||||||
|
<n-button text>
|
||||||
|
<n-icon size="18">
|
||||||
|
<EllipsisVertical />
|
||||||
|
</n-icon>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 聊天消息区域 -->
|
||||||
|
<div class="chat-messages" ref="messagesContainer">
|
||||||
|
<div class="messages-content">
|
||||||
|
<div v-for="message in currentMessages" :key="message.id" class="message-wrapper">
|
||||||
|
<!-- 日期分隔符 -->
|
||||||
|
<div v-if="message.showDateDivider" class="date-divider">
|
||||||
|
<span class="date-text">{{ message.dateText }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息内容 -->
|
||||||
|
<div class="message-item" :class="{ 'message-own': message.isOwn }">
|
||||||
|
<div v-if="!message.isOwn" class="message-avatar">
|
||||||
|
<img :src="message.avatar" :alt="message.senderName" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message-content">
|
||||||
|
<div v-if="!message.isOwn" class="message-sender">{{ message.senderName }}</div>
|
||||||
|
|
||||||
|
<!-- 文本消息 -->
|
||||||
|
<div v-if="message.type === 'text'" class="message-bubble">
|
||||||
|
<p class="message-text">{{ message.content }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片消息 -->
|
||||||
|
<div v-else-if="message.type === 'image'" class="message-bubble image-bubble">
|
||||||
|
<img :src="message.content" class="message-image" @click="previewImage(message.content)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件消息 -->
|
||||||
|
<div v-else-if="message.type === 'file'" class="message-bubble file-bubble">
|
||||||
|
<div class="file-info">
|
||||||
|
<n-icon size="20" color="#1890ff">
|
||||||
|
<DocumentOutline />
|
||||||
|
</n-icon>
|
||||||
|
<div class="file-details">
|
||||||
|
<span class="file-name">{{ message.fileName }}</span>
|
||||||
|
<span class="file-size">{{ message.fileSize }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="message-time">{{ message.time }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息输入区域 -->
|
||||||
|
<div class="chat-input-area">
|
||||||
|
<MessageInput @send="handleSendMessage" placeholder="请输入消息内容..." ref="messageInputRef" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed, nextTick } from 'vue'
|
||||||
|
import { NIcon, NBadge } from 'naive-ui'
|
||||||
|
import {
|
||||||
|
EllipsisVertical,
|
||||||
|
PeopleOutline,
|
||||||
|
ChatbubbleEllipsesOutline,
|
||||||
|
DocumentOutline
|
||||||
|
} from '@vicons/ionicons5'
|
||||||
|
import MessageInput from './MessageInput.vue'
|
||||||
|
|
||||||
|
// 联系人类型定义
|
||||||
|
interface Contact {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
avatar: string
|
||||||
|
type: 'user' | 'group'
|
||||||
|
lastMessage: string
|
||||||
|
lastMessageTime: string
|
||||||
|
unreadCount: number
|
||||||
|
isOnline?: boolean
|
||||||
|
memberCount?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息类型定义
|
||||||
|
interface Message {
|
||||||
|
id: number
|
||||||
|
contactId: number
|
||||||
|
type: 'text' | 'image' | 'file'
|
||||||
|
content: string
|
||||||
|
senderName: string
|
||||||
|
avatar: string
|
||||||
|
time: string
|
||||||
|
isOwn: boolean
|
||||||
|
showDateDivider?: boolean
|
||||||
|
dateText?: string
|
||||||
|
fileName?: string
|
||||||
|
fileSize?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const activeContactId = ref<number | null>(null)
|
||||||
|
const messagesContainer = ref<HTMLElement>()
|
||||||
|
const messageInputRef = ref()
|
||||||
|
|
||||||
|
// 模拟联系人数据
|
||||||
|
const contacts = ref<Contact[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '李小多',
|
||||||
|
avatar: 'https://picsum.photos/40/40?random=1',
|
||||||
|
type: 'user',
|
||||||
|
lastMessage: '这里是智慧你人的语法数字和信息',
|
||||||
|
lastMessageTime: '10:22',
|
||||||
|
unreadCount: 0,
|
||||||
|
isOnline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '直播学习小组群 (9)',
|
||||||
|
avatar: 'https://picsum.photos/40/40?random=2',
|
||||||
|
type: 'group',
|
||||||
|
lastMessage: '这里新是智慧你人的语法数字和信息',
|
||||||
|
lastMessageTime: '2024年7月23日',
|
||||||
|
unreadCount: 0,
|
||||||
|
memberCount: 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: '王明',
|
||||||
|
avatar: 'https://picsum.photos/40/40?random=3',
|
||||||
|
type: 'user',
|
||||||
|
lastMessage: '好的,我知道了',
|
||||||
|
lastMessageTime: '昨天',
|
||||||
|
unreadCount: 2,
|
||||||
|
isOnline: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: '张老师',
|
||||||
|
avatar: 'https://picsum.photos/40/40?random=4',
|
||||||
|
type: 'user',
|
||||||
|
lastMessage: '明天的课程安排已经发布',
|
||||||
|
lastMessageTime: '昨天',
|
||||||
|
unreadCount: 1,
|
||||||
|
isOnline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: '陆娜娜',
|
||||||
|
avatar: 'https://picsum.photos/40/40?random=5',
|
||||||
|
type: 'user',
|
||||||
|
lastMessage: '课程资料我已经整理好了',
|
||||||
|
lastMessageTime: '昨天',
|
||||||
|
unreadCount: 0,
|
||||||
|
isOnline: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: '李科度',
|
||||||
|
avatar: 'https://picsum.photos/40/40?random=6',
|
||||||
|
type: 'user',
|
||||||
|
lastMessage: '下次见面详谈',
|
||||||
|
lastMessageTime: '昨天',
|
||||||
|
unreadCount: 0,
|
||||||
|
isOnline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
name: '王小滑',
|
||||||
|
avatar: 'https://picsum.photos/40/40?random=7',
|
||||||
|
type: 'user',
|
||||||
|
lastMessage: '收到,谢谢!',
|
||||||
|
lastMessageTime: '昨天',
|
||||||
|
unreadCount: 0,
|
||||||
|
isOnline: false
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 模拟消息数据
|
||||||
|
const messages = ref<Message[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
contactId: 1,
|
||||||
|
type: 'text',
|
||||||
|
content: '这里新是智慧你人的语法数字和信息章,多归程回目记录',
|
||||||
|
senderName: '李小多',
|
||||||
|
avatar: 'https://picsum.photos/40/40?random=1',
|
||||||
|
time: '10:22',
|
||||||
|
isOwn: false,
|
||||||
|
showDateDivider: true,
|
||||||
|
dateText: '2024年7月23日'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
contactId: 1,
|
||||||
|
type: 'text',
|
||||||
|
content: '收到',
|
||||||
|
senderName: '我',
|
||||||
|
avatar: 'https://picsum.photos/40/40?random=me',
|
||||||
|
time: '10:23',
|
||||||
|
isOwn: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
contactId: 2,
|
||||||
|
type: 'image',
|
||||||
|
content: 'https://picsum.photos/300/200?random=1',
|
||||||
|
senderName: '张三',
|
||||||
|
avatar: 'https://picsum.photos/40/40?random=8',
|
||||||
|
time: '10:25',
|
||||||
|
isOwn: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
contactId: 2,
|
||||||
|
type: 'file',
|
||||||
|
content: '',
|
||||||
|
senderName: '李四',
|
||||||
|
avatar: 'https://picsum.photos/40/40?random=9',
|
||||||
|
time: '10:30',
|
||||||
|
isOwn: false,
|
||||||
|
fileName: '2025年全家爱词学习人工智能老师考级试卷-点击下载.pptx',
|
||||||
|
fileSize: '2.5MB'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const activeContact = computed(() => {
|
||||||
|
return contacts.value.find(contact => contact.id === activeContactId.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentMessages = computed(() => {
|
||||||
|
return messages.value.filter(message => message.contactId === activeContactId.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法定义
|
||||||
|
const selectContact = (contactId: number) => {
|
||||||
|
activeContactId.value = contactId
|
||||||
|
// 清除未读数量
|
||||||
|
const contact = contacts.value.find(c => c.id === contactId)
|
||||||
|
if (contact) {
|
||||||
|
contact.unreadCount = 0
|
||||||
|
}
|
||||||
|
nextTick(() => {
|
||||||
|
scrollToBottom()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSendMessage = (content: string) => {
|
||||||
|
if (!activeContactId.value) return
|
||||||
|
|
||||||
|
const newMessage: Message = {
|
||||||
|
id: Date.now(),
|
||||||
|
contactId: activeContactId.value,
|
||||||
|
type: 'text',
|
||||||
|
content,
|
||||||
|
senderName: '我',
|
||||||
|
avatar: 'https://picsum.photos/40/40?random=me',
|
||||||
|
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
||||||
|
isOwn: true
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.value.push(newMessage)
|
||||||
|
|
||||||
|
// 更新联系人最后消息
|
||||||
|
const contact = contacts.value.find(c => c.id === activeContactId.value)
|
||||||
|
if (contact) {
|
||||||
|
contact.lastMessage = content
|
||||||
|
contact.lastMessageTime = newMessage.time
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
scrollToBottom()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (messagesContainer.value) {
|
||||||
|
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewImage = (src: string) => {
|
||||||
|
// TODO: 实现图片预览功能
|
||||||
|
console.log('预览图片:', src)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onMounted(() => {
|
||||||
|
// 默认选择第一个联系人
|
||||||
|
if (contacts.value.length > 0) {
|
||||||
|
selectContact(contacts.value[0].id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chat-container {
|
||||||
|
display: flex;
|
||||||
|
height: 1000px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧联系人面板 */
|
||||||
|
.contacts-panel {
|
||||||
|
width: 300px;
|
||||||
|
border-right: 1px solid #e8e8e8;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-header {
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item {
|
||||||
|
display: flex;
|
||||||
|
padding: 12px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item.active {
|
||||||
|
background: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item.unread {
|
||||||
|
background: #fff7e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-avatar {
|
||||||
|
position: relative;
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-avatar img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
right: -2px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #1890ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-preview {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-message {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧聊天面板 */
|
||||||
|
.chat-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
margin-top: 16px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-user-details h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-user-status {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-action-btn:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-content {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-wrapper {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-divider {
|
||||||
|
text-align: center;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-text {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item.message-own {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-avatar {
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-avatar img {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-own .message-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-sender {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble {
|
||||||
|
background: #f0f0f0;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-own .message-bubble {
|
||||||
|
background: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-bubble {
|
||||||
|
padding: 4px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-image {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 150px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-image:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-bubble {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 4px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-own .message-time {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义滚动条 */
|
||||||
|
.contacts-list::-webkit-scrollbar,
|
||||||
|
.chat-messages::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-list::-webkit-scrollbar-track,
|
||||||
|
.chat-messages::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-list::-webkit-scrollbar-thumb,
|
||||||
|
.chat-messages::-webkit-scrollbar-thumb {
|
||||||
|
background: #ccc;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-list::-webkit-scrollbar-thumb:hover,
|
||||||
|
.chat-messages::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chat-container {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-panel {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item {
|
||||||
|
margin: 0 8px 1px 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-content {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
max-width: 75%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
407
src/views/teacher/message/components/SystemMessages.vue
Normal file
407
src/views/teacher/message/components/SystemMessages.vue
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
<template>
|
||||||
|
<div class="system-messages">
|
||||||
|
<!-- 消息列表 -->
|
||||||
|
<div class="message-list">
|
||||||
|
<div v-for="message in messages" :key="message.id" class="message-item" :class="{ unread: !message.isRead }">
|
||||||
|
<!-- 未读消息圆点 -->
|
||||||
|
<!-- <div v-if="!message.isRead" class="unread-dot"></div> -->
|
||||||
|
|
||||||
|
<!-- 系统图标 -->
|
||||||
|
<div class="avatar-container">
|
||||||
|
<div class="system-avatar">
|
||||||
|
<n-icon size="24" color="#1890ff">
|
||||||
|
<NotificationsOutline />
|
||||||
|
</n-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息内容 -->
|
||||||
|
<div class="message-content">
|
||||||
|
<!-- 消息头部 -->
|
||||||
|
<div class="message-header">
|
||||||
|
<div class="message-title">
|
||||||
|
<span class="title">{{ message.title }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="timestamp">{{ message.timestamp }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息内容 -->
|
||||||
|
<div class="message-text">
|
||||||
|
{{ message.content }}
|
||||||
|
<n-button type="info" text icon-placement="right" style="margin-left: 6px;">
|
||||||
|
查看详情
|
||||||
|
<template #icon>
|
||||||
|
<NIcon>
|
||||||
|
<ChevronForward />
|
||||||
|
</NIcon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-if="messages.length === 0" class="empty-state">
|
||||||
|
<n-icon size="48" color="#d9d9d9">
|
||||||
|
<NotificationsOffOutline />
|
||||||
|
</n-icon>
|
||||||
|
<p class="empty-text">暂无系统消息</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination" v-if="messages.length > 0">
|
||||||
|
<button class="page-btn" :disabled="currentPage === 1" @click="goToPage(1)">首页</button>
|
||||||
|
<button class="page-btn" :disabled="currentPage === 1" @click="goToPage(currentPage - 1)">上一页</button>
|
||||||
|
|
||||||
|
<button v-for="page in visiblePages" :key="page" class="page-btn" :class="{ active: page === currentPage }"
|
||||||
|
@click="goToPage(page)">
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span v-if="showEllipsis" class="ellipsis">...</span>
|
||||||
|
<button class="page-btn" @click="goToPage(totalPages)">{{ totalPages }}</button>
|
||||||
|
|
||||||
|
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">下一页</button>
|
||||||
|
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(totalPages)">尾页</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { NIcon } from 'naive-ui'
|
||||||
|
import { NotificationsOutline, NotificationsOffOutline, ChevronForward } from '@vicons/ionicons5'
|
||||||
|
|
||||||
|
// 系统消息类型定义
|
||||||
|
interface SystemMessage {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
timestamp: string
|
||||||
|
isRead: boolean
|
||||||
|
type: 'info' | 'warning' | 'success' | 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const messages = ref<SystemMessage[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '你的会员已专属权利已更新,保来享取今日内障,仅此一天!',
|
||||||
|
content: '好消息好消息!BiliBiliWorld2024年在线大学6月29日(周六)正式开课!!!',
|
||||||
|
timestamp: '7月20日 12:41',
|
||||||
|
isRead: false,
|
||||||
|
type: 'info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '你的会员已专属权利已更新,保来享取今日内障,仅此一天!',
|
||||||
|
content: '好消息好消息!BiliBiliWorld2024年在线大学6月29日(周六)正式开课!!!',
|
||||||
|
timestamp: '7月20日 12:41',
|
||||||
|
isRead: false,
|
||||||
|
type: 'info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '你的会员已专属权利已更新,保来享取今日内障,仅此一天!',
|
||||||
|
content: '好消息好消息!BiliBiliWorld2024年在线大学6月29日(周六)正式开课!!!',
|
||||||
|
timestamp: '7月20日 12:41',
|
||||||
|
isRead: false,
|
||||||
|
type: 'info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: '你的会员已专属权利已更新,保来享取今日内障,仅此一天!',
|
||||||
|
content: '好消息好消息!BiliBiliWorld2024年在线大学6月29日(周六)正式开课!!!',
|
||||||
|
timestamp: '7月20日 12:41',
|
||||||
|
isRead: false,
|
||||||
|
type: 'info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: '你的会员已专属权利已更新,保来享取今日内障,仅此一天!',
|
||||||
|
content: '好消息好消息!BiliBiliWorld2024年在线大学6月29日(周六)正式开课!!!',
|
||||||
|
timestamp: '7月20日 12:41',
|
||||||
|
isRead: false,
|
||||||
|
type: 'info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title: '你的会员已专属权利已更新,保来享取今日内障,仅此一天!',
|
||||||
|
content: '好消息好消息!BiliBiliWorld2024年在线大学6月29日(周六)正式开课!!!',
|
||||||
|
timestamp: '7月20日 12:41',
|
||||||
|
isRead: false,
|
||||||
|
type: 'info'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 分页相关
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const totalPages = ref(29)
|
||||||
|
|
||||||
|
// 计算显示的页码
|
||||||
|
const visiblePages = computed(() => {
|
||||||
|
const pages = []
|
||||||
|
const start = Math.max(1, currentPage.value - 2)
|
||||||
|
const end = Math.min(totalPages.value, currentPage.value + 2)
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否显示省略号
|
||||||
|
const showEllipsis = computed(() => {
|
||||||
|
return currentPage.value + 2 < totalPages.value - 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onMounted(() => {
|
||||||
|
loadMessages()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法定义
|
||||||
|
const loadMessages = () => {
|
||||||
|
// TODO: 调用API加载系统消息数据
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToPage = (page: number) => {
|
||||||
|
if (page >= 1 && page <= totalPages.value) {
|
||||||
|
currentPage.value = page
|
||||||
|
loadMessages()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.system-messages {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-list {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.unread-dot {
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
top: 20px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container {
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid #91d5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-title {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background-color: #1890ff;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #000;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 0;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.delete-btn:hover {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 80px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
margin-top: 16px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 32px 20px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
background: #fff;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn:hover:not(:disabled) {
|
||||||
|
border-color: #1890ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn.active {
|
||||||
|
background: #1890ff;
|
||||||
|
border-color: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn:disabled {
|
||||||
|
color: #bfbfbf;
|
||||||
|
border-color: #f0f0f0;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis {
|
||||||
|
color: #bfbfbf;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.message-item {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-title {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn {
|
||||||
|
min-width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
x
Reference in New Issue
Block a user