course_system/src/views/CourseList.vue
2025-02-06 15:52:19 +08:00

783 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<h2>我添加的课程</h2>
<!-- 加载状态 -->
<div v-if="!isLoaded" class="loading-message">
加载中,请稍后...
</div>
<!-- 无课程记录时的提示 -->
<div v-else-if="submissions.length === 0" class="empty-message">
您还没有添加任何课程。
</div>
<!-- 课程投稿记录表格 -->
<div v-else class="course-table">
<table>
<thead>
<tr>
<th>课程名</th>
<th>分类</th>
<th>教师</th>
<th>开课院系</th> <!-- 新增列 -->
<th>提交时间</th>
<th>状态更新时间</th>
<th>审核状态</th>
</tr>
</thead>
<tbody>
<tr v-for="submission in paginatedSubmissions" :key="submission.submit_id">
<td>
<span v-if="submission.course_id === 0">{{ submission.course_name }}</span>
<span v-else class="clickable-text" @click="loadCourseDetail(submission.course_id)">
{{ submission.course_name }}
</span>
</td>
<td>{{ getCategoryName(submission.category_id) }}</td>
<td>{{ submission.teachers }}</td>
<td>{{ getCollegeName(submission.college) }}</td> <!-- 显示院系名称 -->
<td>{{ formatTime(submission.create_time) }}</td>
<td>{{ formatTime(submission.status_update_time) }}</td>
<td>{{ submission.status }}</td>
</tr>
</tbody>
</table>
<!-- 页码栏 -->
<div class="pagination-bar">
<button v-for="page in totalPages" :key="page" @click="goToPage(page)" :class="{ active: currentPage === page }">
{{ page }}
</button>
</div>
</div>
<!-- 添加课程区域 -->
<div class="add-course">
<div class="section-header">
<h2>我要添加课程</h2>
<button class="help-btn" @click="showHelp = true">
<span class="help-icon">?</span>
帮助
</button>
</div>
<div class="input-section">
<input v-model="newCourseName" type="text" placeholder="请输入课程名称" />
<select v-model="newCategoryId">
<option value="" disabled>请选择课程类别</option>
<option value="1">选修课-通识选修类</option>
<option value="2">选修课-人文选修类</option>
<option value="3">选修课-专业方向类</option>
<option value="4">选修课-体育类</option>
<option value="5">选修课-学科基础类</option>
<option value="6">选修课-暑期国际课</option>
<option value="7">选修课-数学与自然科学类</option>
<option value="8">选修课-重修专栏</option>
<option value="9">必修课-数学与自然科学类</option>
<option value="10">必修课-人文社会科学类</option>
<option value="11">必修课-学科基础类</option>
<option value="12">必修课-专业方向类</option>
<option value="13">必修课-实践类</option>
</select>
<!-- 新增选择开课院系下拉框 -->
<select v-model="newCollege">
<option value="" disabled>请选择开课院系</option>
<option v-for="(name, id) in filteredColleges" :key="id" :value="id">
{{ name }}
</option>
</select>
<input v-model="newTeachers" type="text" placeholder="请输入任课教师(多个教师之间用英文逗号,隔开)" />
<button @click="submitCourse">提交</button>
</div>
</div>
<!-- 调用 CourseDetailModal 组件传入选中的课程 -->
<CourseDetailModal
:isVisible="showDetail"
:course="selectedCourse"
@close="closeCourseDetail"
/>
<!-- 添加帮助模态框 -->
<div v-if="showHelp" class="help-modal">
<div class="help-modal-content">
<div class="help-modal-header">
<h3>关于本系统课程信息的说明</h3>
<button class="close-btn" @click="showHelp = false">×</button>
</div>
<div class="help-modal-body">
<h3>一些说明</h3>
<div class="help-section">
<p>
<strong>本系统中课程分为两大类选修课和必修课</strong>选修课有8个小类通识选修人文选修专业方向体育学科基础暑期国际课数学与自然科学重修专栏<strong>人文选修类包括采用抢选方式进行选课的所有选修课以及改革开放史社会主义发展史两门人文社会科学类的选修课</strong>体育课单独作为一类不属于人文选修类<strong>重修课程的选课归为选修课的重修专栏类别不属于必修课范畴</strong>重修专栏可以分享<strong>任何类别课程</strong><strong>仅限分享重修体验</strong>
</p>
<p>
必修课有5个小类数学与自然科学人文社会科学学科基础专业方向实践<strong>如上所述本系统中重修的必修课程也属于选修课范畴</strong>特别说明<strong>军训课程属于实践类系辅导员给分在创建课程时课程教师请填写辅导员</strong>
</p>
<p>
在创建课程时请先了解课程的类别以及课程的<strong>开课院系详见下文</strong><strong>如果同一课程的任课教师不同用户可以创建多个同名课程例如A老师-篮球B老师-篮球将被视为两个不同的课程</strong>创建的课程卡片<strong>需经过管理员审核</strong>审核进度可在我的课程中实时查看
</p>
</div>
<h3>如何查看课程类别和开课院系</h3>
<div class="help-section">
<h4>1. 本人课程</h4>
<p>在教务系统选择我的考试选择对应的学期后即可查看当前学期的课程类别和开课院系</p>
<img src="https://download.东北大学.com/course_system/coursehelp.png" width="98%">
</div>
<div class="help-section">
<h4>2. 非本人课程</h4>
<p>在教务系统选择公共课表查询课表类型选择班级课表再选好要查询的学期并填写要查询的班级名称点击查询按钮</p>
<img src="https://download.东北大学.com/course_system/other1.png" width="98%">
<p>进入班级课表后即可对应课程的开课院系然后记下老师的姓名</p>
<img src="https://download.东北大学.com/course_system/other2.png" width="98%">
<p>然后回到公共课表查询页面课表类型选择教师课表同样选好要查询的学期并填写要查询的教师姓名点击查询按钮</p>
<img src="https://download.东北大学.com/course_system/other3.png" width="98%">
<p>进入教师课表后找到此课程即可查看课程类别</p>
<img src="https://download.东北大学.com/course_system/other4.png" width="98%">
</div>
</div>
</div>
</div>
</div>
</template>
<script>
// 引入 CourseDetailModal 组件
import CourseDetailModal from './CourseDetailModal.vue';
export default {
components: {
CourseDetailModal
},
data() {
return {
submissions: [], // 存储用户提交的课程数据
newCourseName: '', // 新提交的课程名
newCategoryId: '', // 新提交的课程分类
newTeachers: '', // 新提交的任课教师
newCollege: '', // 新提交的课程院系,默认空字符串
currentPage: 1, // 当前页
itemsPerPage: 10, // 每页显示的条目数
totalPages: 1, // 总页数
isLoaded: false, // 控制页面加载状态
selectedCourse: null, // 存储选中的课程详情
showDetail: false, // 控制模态框显示
cachedEmail: null, // 缓存的邮箱信息
collegeMap: {
59: '未填写',
1: '材料科学与工程学院',
2: '创新创业学院',
3: '档案馆',
4: '党委宣传部',
5: '党委组织部',
6: '发展规划与学科建设处',
7: '佛山研究生创新学院',
8: '工程训练中心',
9: '工会',
10: '工商管理学院',
11: '国防教育学院',
12: '国际教育学院',
13: '后勤服务中心',
14: '后勤管理处',
15: '浑南管委会',
16: '江河建筑学院',
17: '尖子班',
18: '教务处',
19: '基础学院',
20: '计划财经处',
21: '机器人科学与工程学院',
22: '计算机科学与工程学院',
23: '计算中心',
24: '纪委',
25: '机械工程与自动化学院',
26: '继续教育学院',
27: '科技产业集团',
28: '科学技术研究院',
29: '理学院',
30: '马克思主义学院',
31: '民族教育学院',
33: '人事处',
34: '人文选修课群',
35: '软件学院',
36: '生命科学与健康学院',
37: '体育部',
38: '体育场馆管理中心',
39: '团委',
40: '图书馆',
41: '外国语学院',
42: '外联处',
43: '网络教育学院',
44: '未来技术学院',
45: '文法学院',
46: '校长办公室',
47: '信息科学与工程学院',
48: '新知识课群',
49: '学生处',
50: '学生创新中心',
51: '学生指导服务中心',
52: '研究生院',
53: '冶金学院',
54: '艺术学院',
55: '医学与生物信息工程学院',
56: '医院',
57: '资产与实验室管理处',
58: '资源与土木工程学院'
},
showHelp: false // 添加这一行来控制帮助模态框的显示
};
},
computed: {
// 计算当前页展示的投稿数据
paginatedSubmissions() {
const start = (this.currentPage - 1) * this.itemsPerPage;
const end = start + this.itemsPerPage;
return this.submissions.slice(start, end);
},
// 过滤掉 "不限院系" 的选项
filteredColleges() {
return Object.fromEntries(Object.entries(this.collegeMap).filter(([id]) => id !== '59'));
}
},
mounted() {
// 页面加载时延迟0.3秒加载数据
setTimeout(async () => {
this.isLoaded = true;
// 首先获取并缓存邮箱
await this.fetchAndCacheEmail();
// 然后获取投稿记录
this.fetchSubmissions();
}, 300);
// 检查 URL 参数,如果 m=true 则打开帮助模态框
if (this.$route.query.m === 'true') {
this.showHelp = true;
}
// 在组件挂载后获取查询参数并处理
const encodedParam = this.$route.query.c;
if (encodedParam) {
// 尝试解码并验证
try {
// Base64 URL 解码
const decodedString = atob(encodedParam);
// 检查是否为正整数
if (this.isPositiveInteger(decodedString)) {
this.decodedParam = decodedString;
this.isValid = true;
// console.log('Valid Param:', this.decodedParam);
// 打开课程模态框
this.loadCourseDetail(this.decodedParam);
} else {
// console.warn('Decoded param is not a valid positive integer.');
}
} catch (error) {
// console.error('Error decoding Base64 URL param:', error);
}
} else {
// console.warn('No parameter provided.');
}
},
methods: {
// 检查是否为正整数
isPositiveInteger(value) {
// 检查value是否是正整数
const num = Number(value);
return Number.isInteger(num) && num > 0;
},
// 统一的函数用于从JWT获取并缓存邮箱
async fetchAndCacheEmail() {
if (this.cachedEmail) {
return; // 如果已经缓存了邮箱,则不需要再次请求
}
const cookies = document.cookie.split('; ');
const tokenCookie = cookies.find(cookie => cookie.startsWith('token='));
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
if (!token) {
console.error('未找到JWT信息');
return;
}
try {
// 发送请求到后端验证JWT并获取邮箱
const response = await fetch('https://userlogin.xn--xhq44jb2fzpc.com/verifyToken', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token
}
});
const result = await response.json();
if (response.ok && result.email) {
this.cachedEmail = result.email; // 缓存邮箱信息
} else {
console.error('JWT验证失败或未找到邮箱');
}
} catch (error) {
console.error('JWT验证时出错:', error);
}
},
// 获取用户投稿
// 获取用户投稿
async fetchSubmissions() {
// 从 Cookie 中获取 JWT token
const token = this.getCookie('token');
if (!token) {
alert('无法获取JWT请重新登录。');
return;
}
// 调用查询投稿记录的接口,并添加 Authorization 头
const response = await fetch(`https://coursesystem.xn--xhq44jb2fzpc.com/user/user-submissions`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': token // 添加 JWT 到 Authorization 头
}
});
const result = await response.json();
if (response.status !== 200) {
alert(result.error || '查询投稿记录失败');
return;
}
// 存储查询结果
this.submissions = result;
this.totalPages = Math.ceil(this.submissions.length / this.itemsPerPage);
},
// 读取 cookie 的辅助函数
getCookie(name) {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [key, value] = cookie.trim().split('=');
if (key === name) {
return value;
}
}
return null;
},
// 分页功能
goToPage(page) {
this.currentPage = page;
},
// 提交新课程
async submitCourse() {
if (!this.newCourseName || !this.newCategoryId || !this.newTeachers || !this.newCollege) {
alert('请填写所有字段后提交!');
return;
}
if (confirm('确认提交审核吗?')) {
// 从 Cookie 中获取 JWT token
const token = this.getCookie('token');
if (!token) {
alert('无法获取JWT请重新登录。');
return;
}
const newSubmission = {
course_name: this.newCourseName,
category_id: this.newCategoryId,
teachers: this.newTeachers,
college: this.newCollege
};
await fetch('https://coursesystem.xn--xhq44jb2fzpc.com/user/submit-course', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token // 添加 JWT 到 Authorization 头
},
body: JSON.stringify(newSubmission)
});
// 清空输入框并重新加载投稿记录
this.newCourseName = '';
this.newCategoryId = '';
this.newTeachers = '';
this.newCollege = '';
this.fetchSubmissions();
}
},
// 获取分类名称
getCategoryName(categoryId) {
const categoryMap = {
1: '选修课-通识选修类',
2: '选修课-人文选修类',
3: '选修课-专业方向类',
4: '选修课-体育类',
5: '选修课-学科基础类',
6: '选修课-暑期国际课',
7: '选修课-数学与自然科学类',
8: '选修课-重修专栏',
9: '必修课-数学与自然科学类',
10: '必修课-人文社会科学类',
11: '必修课-学科基础类',
12: '必修课-专业方向类',
13: '必修课-实践类'
};
return categoryMap[categoryId] || '未知分类';
},
// 格式化时间
formatTime(time) {
const date = new Date(time);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
},
// 打开课程详情模态框
async loadCourseDetail(courseId) {
try {
const response = await fetch(`https://coursesystem.xn--xhq44jb2fzpc.com/course-detail?course_id=${courseId}`);
const course = await response.json();
this.selectedCourse = course;
this.showDetail = true;
} catch (error) {
console.error('Error loading course details:', error);
}
},
// 关闭课程详情模态框
closeCourseDetail() {
this.showDetail = false;
this.selectedCourse = null;
},
getCollegeName(collegeId) {
const collegeMap = {
59: '未填写',
1: '材料科学与工程学院',
2: '创新创业学院',
3: '档案馆',
4: '党委宣传部',
5: '党委组织部',
6: '发展规划与学科建设处',
7: '佛山研究生创新学院',
8: '工程训练中心',
9: '工会',
10: '工商管理学院',
11: '国防教育学院',
12: '国际教育学院',
13: '后勤服务中心',
14: '后勤管理处',
15: '浑南管委会',
16: '江河建筑学院',
17: '尖子班',
18: '教务处',
19: '基础学院',
20: '计划财经处',
21: '机器人科学与工程学院',
22: '计算机科学与工程学院',
23: '计算中心',
24: '纪委',
25: '机械工程与自动化学院',
26: '继续教育学院',
27: '科技产业集团',
28: '科学技术研究院',
29: '理学院',
30: '马克思主义学院',
31: '民族教育学院',
33: '人事处',
34: '人文选修课群',
35: '软件学院',
36: '生命科学与健康学院',
37: '体育部',
38: '体育场馆管理中心',
39: '团委',
40: '图书馆',
41: '外国语学院',
42: '外联处',
43: '网络教育学院',
44: '未来技术学院',
45: '文法学院',
46: '校长办公室',
47: '信息科学与工程学院',
48: '新知识课群',
49: '学生处',
50: '学生创新中心',
51: '学生指导服务中心',
52: '研究生院',
53: '冶金学院',
54: '艺术学院',
55: '医学与生物信息工程学院',
56: '医院',
57: '资产与实验室管理处',
58: '资源与土木工程学院'
};
return collegeMap[collegeId] || '未知院系';
}
}
};
</script>
<style scoped>
.loading-message {
font-size: 16px;
color: #666;
text-align: center;
margin-top: 20px;
}
/* 美化表格,线表样式 */
.course-table {
width: 100%;
border-collapse: collapse;
max-height: 270px; /* 设置最大高度,你可以根据需要调整 */
overflow-y: auto; /* 当内容超过最大高度时允许垂直滚动 */
}
.course-table thead {
position: sticky;
top: 0;
z-index: 1;
}
.course-table th, .course-table td {
padding: 10px;
border-bottom: 1px solid #ddd; /* 只保留水平线 */
text-align: left;
}
.course-table th {
background-color: #f1f1f1;
font-weight: bold;
padding: 10px;
border-bottom: 1px solid #ddd;
text-align: left;
/* 确保表头背景色完全不透明 */
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1);
}
.course-table tr:hover {
background-color: #f9f9f9; /* 鼠标悬停时的背景颜色 */
}
/* 表单部分的输入框 */
.input-section input, .input-section select {
display: block;
margin: 10px 0;
padding: 10px;
width: 100%;
max-width: 400px;
border: 1px solid #ccc;
border-radius: 4px; /* 圆角 */
font-size: 16px;
transition: all 0.3s ease; /* 添加过渡效果 */
}
.input-section input:focus, .input-section select:focus {
border-color: #007bff;
outline: none;
box-shadow: 0 0 8px rgba(0, 123, 255, 0.5); /* 焦点效果 */
}
/* 美化按钮,添加圆角和缩放动画 */
.input-section button {
padding: 10px 20px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 8px; /* 圆角 */
cursor: pointer;
transition: transform 0.2s ease, background-color 0.3s ease; /* 添加缩放动画和过渡效果 */
}
.input-section button:hover {
background-color: #0056b3;
transform: scale(1.05); /* 放大效果 */
}
.input-section button:active {
transform: scale(0.95); /* 点击时缩小效果 */
}
/* 分页栏样式美化 */
.pagination-bar {
margin-top: 20px;
text-align: center;
}
.pagination-bar button {
margin: 0 5px;
padding: 5px 10px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #ffffff;
transition: all 0.3s ease; /* 添加过渡效果 */
}
.pagination-bar button:hover {
background-color: #0400f9;
border-color: #0400f9;
color: white;
}
.pagination-bar .active {
background-color: #007bff;
color: white;
border-color: #007bff;
}
.clickable-text {
color: #007bff;
cursor: pointer;
text-decoration: none;
transition: color 0.2s ease; /* 鼠标悬浮时的过渡效果 */
}
.clickable-text:hover {
color: darkblue; /* 鼠标悬浮时颜色变化 */
}
.clickable-text:active {
color: navy; /* 点击时颜色变化 */
}
.empty-message {
text-align: center;
padding: 20px;
color: #666;
font-size: 16px;
}
.section-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 0;
}
.help-btn {
display: flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
color: #495057;
transition: all 0.2s ease;
}
.help-btn:hover {
background: #e9ecef;
}
.help-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 50%;
background: #6c757d;
color: white;
font-size: 12px;
font-weight: bold;
}
.help-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.help-modal-content {
background: white;
border-radius: 12px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.help-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #dee2e6;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #6c757d;
}
.help-section {
margin-bottom: 20px;
}
.help-modal-header h3 {
font-size: 24px;
font-weight: bold;
margin-bottom: 4px;
}
.help-section h4 {
color: #0056b3;
margin-bottom: 10px;
}
.help-section p {
color: #495057;
line-height: 1.6;
margin-bottom: 8px;
}
</style>