course_system/src/views/CourseList.vue

880 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 class="course-list-page">
<h2 class="page-title">我添加的课程</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-cards">
<div v-for="submission in paginatedSubmissions" :key="submission.submit_id" class="course-card" @click="handleCardClick(submission)">
<div class="card-header">
<h3 class="course-name-title">{{ submission.course_name }}</h3>
<div class="status-tag" :class="getStatusClass(submission.status)">
{{ submission.status }}
</div>
</div>
<div class="card-body">
<div class="info-row">
<span class="info-label">分类</span>
<span class="info-val">{{ getCategoryName(submission.category_id) }}</span>
</div>
<div class="info-row">
<span class="info-label">教师</span>
<span class="info-val truncate">{{ submission.teachers }}</span>
</div>
<div class="info-row">
<span class="info-label">院系</span>
<span class="info-val truncate">{{ getCollegeName(submission.college) }}</span>
</div>
</div>
<div class="card-footer">
<div class="time-info">
<div class="time-row">
<span class="time-label">提交:</span>
<span class="time-val">{{ formatTimeShort(submission.create_time) }}</span>
</div>
<div class="time-row">
<span class="time-label">更新:</span>
<span class="time-val">{{ formatTimeShort(submission.status_update_time) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 页码栏 -->
<div class="pagination-bar" v-if="totalPages > 1">
<button @click="goToPage(1)" :class="{ active: currentPage === 1 }" :disabled="currentPage === 1">首页</button>
<button
v-for="page in getPaginationPages()"
:key="page"
@click="goToPage(page)"
:class="{ active: currentPage === page }"
>
{{ page }}
</button>
<button
@click="goToPage(totalPages)"
:class="{ active: currentPage === totalPages }"
:disabled="currentPage === totalPages"
>
尾页
</button>
</div>
<!-- 添加课程区域 -->
<div class="add-course-section card-style">
<div class="section-header">
<h2>我要添加课程</h2>
<button class="help-btn" @click="showHelp = true">
<span class="help-icon">?</span>
帮助
</button>
</div>
<div class="input-grid">
<div class="input-group">
<label>课程名称</label>
<input v-model="newCourseName" type="text" placeholder="请输入课程名称" class="modern-input" />
</div>
<div class="input-group">
<label>课程类别</label>
<select v-model="newCategoryId" class="modern-select">
<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>
</div>
<div class="input-group">
<label>开课院系</label>
<select v-model="newCollege" class="modern-select">
<option value="" disabled>请选择开课院系</option>
<option v-for="(name, id) in filteredColleges" :key="id" :value="id">
{{ name }}
</option>
</select>
</div>
<div class="input-group">
<label>任课教师</label>
<input v-model="newTeachers" type="text" placeholder="多个教师用英文逗号隔开如A,B" class="modern-input" />
</div>
<div class="button-group">
<button @click="submitCourse" class="submit-btn">提交课程</button>
</div>
</div>
</div>
<!-- 添加帮助模态框 -->
<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>
export default {
components: {
},
data() {
return {
submissions: [], // 存储用户提交的课程数据
newCourseName: '', // 新提交的课程名
newCategoryId: '', // 新提交的课程分类
newTeachers: '', // 新提交的任课教师
newCollege: '', // 新提交的课程院系,默认空字符串
currentPage: 1, // 当前页
itemsPerPage: 10, // 每页显示的条目数
totalPages: 1, // 总页数
isLoaded: false, // 控制页面加载状态
cachedEmail: null, // 缓存的邮箱信息
showHelp: false, // 添加这一行来控制帮助模态框的显示
maxVisiblePages: 10, // 最大显示页码数
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: '资源与土木工程学院'
}
};
},
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() {
this.checkMobile();
window.addEventListener('resize', this.checkMobile);
// 页面加载时延迟0.3秒加载数据
setTimeout(async () => {
this.isLoaded = true;
// 获取投稿记录
this.fetchSubmissions();
}, 300);
// 检查 URL 参数,如果 m=true 则打开帮助模态框
if (this.$route.query.m === 'true') {
this.showHelp = true;
}
// 处理 query.c 参数跳转 (保留逻辑但改为直接跳转)
const encodedParam = this.$route.query.c;
if (encodedParam) {
try {
const decodedString = atob(encodedParam);
if (this.isPositiveInteger(decodedString)) {
this.$router.push(`/detail/${decodedString}`);
}
} catch (error) {
// ignore error
}
}
},
beforeUnmount() {
window.removeEventListener('resize', this.checkMobile);
},
methods: {
checkMobile() {
this.maxVisiblePages = window.innerWidth <= 768 ? 5 : 10;
},
// 检查是否为正整数
isPositiveInteger(value) {
const num = Number(value);
return Number.isInteger(num) && num > 0;
},
// 获取用户投稿
async fetchSubmissions() {
const token = this.getCookie('token');
if (!token) {
// alert('无法获取JWT请重新登录。');
// 用户体验优化如果只是查看可能不需要alert但这里是"我添加的课程",必须登录
return;
}
try {
const response = await fetch(`https://coursesystem.xn--xhq44jb2fzpc.com/user/user-submissions`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': token
}
});
if (response.status !== 200) {
const result = await response.json();
alert(result.error || '查询投稿记录失败');
return;
}
const result = await response.json();
this.submissions = result;
this.totalPages = Math.ceil(this.submissions.length / this.itemsPerPage);
} catch (e) {
console.error("Fetch submissions error", e);
}
},
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) {
if (page < 1 || page > this.totalPages) return;
this.currentPage = page;
},
getPaginationPages() {
const pages = [];
const total = this.totalPages;
const current = this.currentPage;
const maxVisible = this.maxVisiblePages;
if (total <= maxVisible) {
for (let i = 1; i <= total; i++) {
pages.push(i);
}
} else {
let startPage = current - Math.floor(maxVisible / 2);
startPage = Math.max(startPage, 1);
let endPage = startPage + maxVisible - 1;
if (endPage > total) {
endPage = total;
startPage = Math.max(endPage - maxVisible + 1, 1);
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
}
return pages;
},
// 提交新课程
async submitCourse() {
if (!this.newCourseName || !this.newCategoryId || !this.newTeachers || !this.newCollege) {
alert('请填写所有字段后提交!');
return;
}
if (confirm('确认提交审核吗?')) {
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
},
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}`;
},
formatTimeShort(time) {
if (!time) return '-';
const date = new Date(time);
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${month}-${day}`;
},
handleCardClick(submission) {
// 如果 course_id 为 0说明还在审核中或者没有关联到正式课程暂时不可点击或点击无反应
if (submission.course_id === 0) {
return;
}
this.$router.push(`/detail/${submission.course_id}`);
},
getStatusClass(status) {
if (status === '已通过') return 'status-approved';
if (status === '待审核') return 'status-pending';
if (status === '已拒绝') return 'status-rejected';
return '';
},
getCollegeName(collegeId) {
return this.collegeMap[collegeId] || '未知院系';
}
}
};
</script>
<style scoped>
.course-list-page {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
min-height: 100vh;
background-color: #f8f9fa;
}
.page-title {
margin-bottom: 20px;
font-size: 24px;
color: #333;
font-weight: bold;
}
.loading-message, .empty-message {
font-size: 16px;
color: #666;
text-align: center;
margin-top: 40px;
}
/* Course Cards Grid */
.course-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 40px;
}
.course-card {
background: white;
border-radius: 12px;
border: 1px solid #f0f0f0;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
}
.course-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
border-color: #e0e0e0;
}
.card-header {
padding: 16px;
border-bottom: 1px solid #f9f9f9;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 10px;
}
.course-name-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
flex: 1;
}
.status-tag {
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
white-space: nowrap;
background: #eee;
color: #666;
}
.status-approved {
background: #e6f7ff;
color: #1890ff;
border: 1px solid #91d5ff;
}
.status-pending {
background: #fffbe6;
color: #faad14;
border: 1px solid #ffe58f;
}
.status-rejected {
background: #fff1f0;
color: #f5222d;
border: 1px solid #ffa39e;
}
.card-body {
padding: 16px;
flex: 1;
}
.info-row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 13px;
color: #666;
}
.info-label {
color: #999;
margin-right: 8px;
flex-shrink: 0;
}
.info-val {
text-align: right;
color: #333;
font-weight: 500;
}
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 70%;
}
.card-footer {
padding: 12px 16px;
background: #fafafa;
border-top: 1px solid #f0f0f0;
}
.time-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.time-row {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #999;
}
/* Pagination */
.pagination-bar {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 40px;
flex-wrap: wrap;
}
.pagination-bar button {
min-width: 36px;
height: 36px;
padding: 0 10px;
border: 1px solid #e0e0e0;
background: white;
color: #606266;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
}
.pagination-bar button:hover:not(:disabled) {
color: #409eff;
border-color: #409eff;
}
.pagination-bar button.active {
background-color: #409eff;
border-color: #409eff;
color: white;
}
.pagination-bar button:disabled {
background: #f5f7fa;
color: #c0c4cc;
cursor: not-allowed;
}
/* Add Course Section */
.add-course-section {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
margin-top: 40px;
}
.section-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
}
.section-header h2 {
margin: 0;
font-size: 20px;
color: #333;
}
.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;
}
.input-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 30px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.input-group label {
font-size: 14px;
color: #606266;
font-weight: 500;
}
.modern-input, .modern-select {
width: 100%;
box-sizing: border-box;
padding: 10px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s;
background: white;
}
.modern-input:focus, .modern-select:focus {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
outline: none;
}
.button-group {
grid-column: 1 / -1;
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.submit-btn {
padding: 10px 30px;
background-color: #409eff;
color: white;
border: none;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.submit-btn:hover {
background-color: #66b1ff;
transform: translateY(-1px);
}
/* Help Modal */
.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: 20px;
font-weight: bold;
margin: 0;
}
.help-section h4 {
color: #0056b3;
margin-bottom: 10px;
}
.help-section p {
color: #495057;
line-height: 1.6;
margin-bottom: 8px;
}
/* Responsive */
@media (max-width: 768px) {
.course-cards {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.input-grid {
grid-template-columns: 1fr;
}
.course-name-title {
font-size: 14px;
}
.status-tag {
font-size: 10px;
padding: 1px 5px;
}
}
@media (max-width: 480px) {
.course-cards {
grid-template-columns: 1fr;
}
}
</style>