1235 lines
34 KiB
Vue
1235 lines
34 KiB
Vue
<template>
|
||
<div class="modal-overlay" v-if="isVisible">
|
||
<div class="modal-content">
|
||
<span class="close" @click="closeModal">×</span>
|
||
|
||
<!-- 显示加载中状态 -->
|
||
<!-- <div v-if="loading" class="loading">
|
||
<el-icon><i class="el-icon-loading"></i></el-icon>
|
||
<p>加载中...</p>
|
||
</div> -->
|
||
|
||
<!-- 左边课程详情区 -->
|
||
<div class="course-detail">
|
||
<h3>
|
||
{{ course.course_name }}
|
||
<!-- 检查 titles 是否存在且不为空,并在外层元素使用 v-if -->
|
||
<template v-if="course.titles && course.titles.length > 0">
|
||
<span v-for="title in course.titles"
|
||
:key="title.title"
|
||
class="course-title"
|
||
:style="{ borderColor: title.color, color: title.color }">
|
||
{{ title.title }}
|
||
</span>
|
||
</template>
|
||
<button class="share-btn" @click="shareCourse">分享给好友</button>
|
||
</h3>
|
||
<p><strong>分类:</strong> {{ getCategoryName(course.category_id) }}</p>
|
||
<p><strong>教师:</strong> {{ course.teachers }}</p>
|
||
<p><strong>开课院系:</strong> {{ getCollegeName(course.college) }}</p>
|
||
|
||
<!-- 复制成功的模态框 -->
|
||
<div class="share-modal" v-if="showShareModal">
|
||
链接复制成功,快去分享给其他小伙伴吧!
|
||
</div>
|
||
|
||
<!-- 错误提示模态框 -->
|
||
<div class="error-modal" v-if="showErrorModal">
|
||
获取评论数据失败,正在刷新页面...
|
||
</div>
|
||
|
||
<!-- 评分区 -->
|
||
<div class="rating-section">
|
||
<h4>课程实时平均分 ({{ course.rating_count }}人评分)</h4>
|
||
<div class="total-rating">{{ totalRating }}</div>
|
||
<div class="progress-container">
|
||
<el-progress class="progress" :percentage="fiveStarPercent" status="success" text-inside>5星: {{ fiveStarPercent.toFixed(2) }}%</el-progress>
|
||
<el-progress class="progress" :percentage="fourStarPercent" status="success" text-inside>4星: {{ fourStarPercent.toFixed(2) }}%</el-progress>
|
||
<el-progress class="progress" :percentage="threeStarPercent" status="warning" text-inside>3星: {{ threeStarPercent.toFixed(2) }}%</el-progress>
|
||
<el-progress class="progress" :percentage="twoStarPercent" status="warning" text-inside>2星: {{ twoStarPercent.toFixed(2) }}%</el-progress>
|
||
<el-progress class="progress" :percentage="oneStarPercent" status="exception" text-inside>1星: {{ oneStarPercent.toFixed(2) }}%</el-progress>
|
||
</div>
|
||
|
||
|
||
|
||
|
||
<div class="stars">
|
||
<span
|
||
v-for="n in 5"
|
||
:key="n"
|
||
:class="{ 'highlighted': n <= userRating }"
|
||
@mouseover="highlightStars(n)"
|
||
@mouseout="resetStars"
|
||
@click="confirmRating(n)"
|
||
>
|
||
★
|
||
</span>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- 在课程详情区域添加 -->
|
||
<p class="ai-summary">
|
||
<span style="display: block; margin-bottom: -15px;">
|
||
<strong>AI课程总结</strong>
|
||
<span style="color: white; font-size: 8px; margin-left: 5px; border-radius: 4px; padding: 1px 4px; background-color: red; vertical-align: super;">Beta</span>
|
||
<span style="font-size: 12px; margin-left: 5px;">
|
||
内容由 <img src="https://download.东北大学.com/course_system/deepseek.svg" style="height: 16px; vertical-align: middle;" /> DeepSeek V3 生成,仅供参考
|
||
</span>
|
||
</span>
|
||
<br>
|
||
<template v-if="displayedSummary">
|
||
{{ displayedSummary }}<span v-if="isTyping" class="typing-cursor">|</span>
|
||
</template>
|
||
<template v-else>
|
||
当前课程的评分和评论数据过少,暂不能生成AI总结。
|
||
</template>
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 右边评论区 -->
|
||
<div class="comment-section">
|
||
<div class="sort-options">
|
||
<label><input type="radio" value="like_count" v-model="sortBy" @change="fetchComments"> 赞数最多优先</label>
|
||
<label><input type="radio" value="comment_time" v-model="sortBy" @change="fetchComments"> 最新评论优先</label>
|
||
</div>
|
||
|
||
<div v-if="filteredComments" class="comments-list">
|
||
<div v-for="comment in comments" :key="comment.comment_id" class="comment">
|
||
<div class="comment-meta">
|
||
<strong>{{ comment.nickname }}</strong>
|
||
<span class="comment-time" style="margin-left: 10px">发表于 {{ formatTime(comment.comment_time) }}</span>
|
||
<span
|
||
class="like-section"
|
||
@click="toggleLike(comment)"
|
||
:class="{ liked: comment.is_liked }"
|
||
>
|
||
❤ {{ comment.like_count }}
|
||
</span>
|
||
</div>
|
||
<div style="margin-top: 8px">{{ comment.comment_content }}</div>
|
||
<div class="comment-actions">
|
||
<span>评分: <strong style="color: red;">{{ comment.rating }}</strong></span>
|
||
<button
|
||
v-if="comment.deleteable"
|
||
@click="deleteComment(comment.comment_id)"
|
||
class="delete-btn"
|
||
>
|
||
删除
|
||
</button>
|
||
<button
|
||
v-else
|
||
class="chat-start-btn"
|
||
@click="openChatModal(comment.comment_id)"
|
||
>
|
||
发起聊天
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p v-else class="no-comments">本课程暂无评论。</p>
|
||
|
||
<!-- 发起聊天模态框 -->
|
||
<div v-if="isChatModalVisible" class="chat-modal-overlay">
|
||
<div class="chat-modal-content">
|
||
<span class="chat-close-btn" @click="closeChatModal">×</span>
|
||
<h3>发起聊天</h3>
|
||
<textarea
|
||
v-model="chatMessage"
|
||
placeholder="请输入聊天内容(至少6个字符)"
|
||
class="chat-textarea"
|
||
></textarea>
|
||
<div class="chat-modal-actions">
|
||
<button @click="submitChat" :disabled="chatMessage.length < 6">发起</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- 会话已存在模态框 -->
|
||
<div v-if="isConversationExistsModalVisible" class="chat-modal-overlay">
|
||
<div class="chat-modal-content">
|
||
<span class="chat-close-btn" @click="closeConversationExistsModal">×</span>
|
||
<h3>在此课程下,您与此用户已有会话!</h3>
|
||
<div class="chat-modal-actions">
|
||
<button @click="navigateToConversation(existingConversationId)">进入会话</button>
|
||
<button @click="closeConversationExistsModal">取消</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
<!-- 发表评论 -->
|
||
<div class="new-comment">
|
||
<input v-model="nickname" placeholder="一句话概括您的评论,如:被x老师伤透了心">
|
||
<textarea v-model="commentContent" placeholder="我们推荐您分享本课程的课程内容、学习难度、考核方式和给分好坏,以帮助更多同学做出判断。支持删除自己的评论。" style="resize: none; height: 50px;"></textarea>
|
||
<button @click="submitComment">提交评论</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
props: {
|
||
isVisible: {
|
||
type: Boolean,
|
||
required: true
|
||
},
|
||
course: {
|
||
type: Object,
|
||
required: true
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
// loading: true, // 添加loading状态
|
||
token: null, // 存储token
|
||
userRating: 0, // 当前用户对该课程的评分
|
||
totalRating: 0, // 课程总评分
|
||
fiveStarPercent: 0,
|
||
fourStarPercent: 0,
|
||
threeStarPercent: 0,
|
||
twoStarPercent: 0,
|
||
oneStarPercent: 0,
|
||
comments: [], // 评论列表
|
||
nickname: '',
|
||
commentContent: '',
|
||
sortBy: 'like_count', // 默认按点赞数排序
|
||
showShareModal: false, // 控制分享模态框的显示
|
||
cachedEmail: null, // 缓存的邮箱信息,初始为 null
|
||
isChatModalVisible: false, // 控制发起聊天模态框的显示
|
||
isConversationExistsModalVisible: false, // 控制会话已存在模态框的显示
|
||
chatMessage: '', // 存储用户输入的聊天内容
|
||
currentCommentId: null, // 当前发起聊天的评论ID
|
||
existingConversationId: null, // 已存在的会话ID
|
||
showErrorModal: false, // 新增错误模态框的显示状态
|
||
aiSummary: null, // 存储AI课程总结
|
||
displayedSummary: '', // 用于显示动画效果的文本
|
||
isTyping: false, // 控制动画状态
|
||
shouldStopTyping: false, // 添加控制动画中断的标志
|
||
};
|
||
},
|
||
async mounted() {
|
||
// this.loading = true; // 确保加载状态被激活
|
||
try {
|
||
// 获取token
|
||
this.getToken();
|
||
|
||
// 使用 Promise.all 调用 fetchAndCacheEmail 和其他异步操作
|
||
await Promise.all([
|
||
this.fetchAndCacheEmail(), // 先缓存邮箱
|
||
this.fetchRatingDistribution(),
|
||
this.fetchUserRating(),
|
||
this.fetchComments(),
|
||
this.fetchAISummary()
|
||
]);
|
||
} catch (error) {
|
||
console.error('Error loading data:', error);
|
||
// 可以设置一个错误消息的状态并在UI上显示
|
||
}
|
||
// this.loading = false; // 加载完成后,无论成功或失败,都应关闭加载状态
|
||
},
|
||
|
||
// 暂时不需要,因为每次selectedCourse改变时,都会挂载一个新的CourseDetailModal组件
|
||
watch: {
|
||
async course(newCourse) {
|
||
if (newCourse && newCourse.course_id) {
|
||
// 重置 AI 相关状态
|
||
this.aiSummary = null;
|
||
this.displayedSummary = '';
|
||
this.isTyping = false;
|
||
|
||
// 确保token是最新的
|
||
this.getToken();
|
||
|
||
// this.loading = true; // 开始加载时设置加载状态
|
||
// console.log('Course changed:', newCourse.course_id);
|
||
|
||
// 使用 await 确保所有数据加载完成
|
||
try {
|
||
await Promise.all([
|
||
this.fetchRatingDistribution(),
|
||
this.fetchUserRating(),
|
||
this.fetchComments(),
|
||
this.fetchAISummary()
|
||
]);
|
||
} catch (error) {
|
||
console.error('Error fetching course details:', error);
|
||
// 处理错误,例如展示错误信息
|
||
}
|
||
|
||
// this.loading = false; // 所有请求完成后取消加载状态
|
||
}
|
||
},
|
||
displayedSummary() {
|
||
if (this.isTyping) {
|
||
this.$nextTick(() => {
|
||
const courseDetail = document.querySelector('.course-detail');
|
||
if (courseDetail) {
|
||
courseDetail.scrollTop = courseDetail.scrollHeight;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
filteredComments() {
|
||
return this.comments.length > 0 ? this.comments : null;
|
||
}
|
||
},
|
||
|
||
|
||
methods: {
|
||
// 获取token方法
|
||
getToken() {
|
||
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
|
||
if (tokenCookie) {
|
||
this.token = tokenCookie.split('=')[1];
|
||
} else {
|
||
console.error('Token not found in cookies');
|
||
this.token = null;
|
||
}
|
||
},
|
||
closeModal() {
|
||
this.shouldStopTyping = true; // 设置中断标志
|
||
this.isTyping = false;
|
||
this.displayedSummary = ''; // 清空显示的文本
|
||
this.aiSummary = null; // 清空 AI 总结
|
||
this.$emit('close');
|
||
},
|
||
getCategoryName(categoryId) {
|
||
const categories = {
|
||
1: '选修课-通识选修类',
|
||
2: '选修课-人文选修类',
|
||
3: '选修课-专业方向类',
|
||
4: '选修课-体育类',
|
||
5: '选修课-学科基础类',
|
||
6: '选修课-暑期国际课',
|
||
7: '选修课-数学与自然科学类',
|
||
8: '选修课-重修专栏',
|
||
9: '必修课-数学与自然科学类',
|
||
10: '必修课-人文社会科学类',
|
||
11: '必修课-学科基础类',
|
||
12: '必修课-专业方向类',
|
||
13: '必修课-实践类'
|
||
};
|
||
return categories[categoryId] || '未知分类';
|
||
},
|
||
async fetchRatingDistribution() {
|
||
const response = await fetch(`https://coursesystem.xn--xhq44jb2fzpc.com/get-course-ratings-distribution?course_id=${this.course.course_id}`);
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
this.totalRating = result.total_rating || 0;
|
||
this.fiveStarPercent = result.five_stars * 100;
|
||
this.fourStarPercent = result.four_stars * 100;
|
||
this.threeStarPercent = result.three_stars * 100;
|
||
this.twoStarPercent = result.two_stars * 100;
|
||
this.oneStarPercent = result.one_star * 100;
|
||
} else if (response.status === 404) {
|
||
// 如果返回404, 设置所有相关数据为0
|
||
this.totalRating = 0;
|
||
this.fiveStarPercent = 0;
|
||
this.fourStarPercent = 0;
|
||
this.threeStarPercent = 0;
|
||
this.twoStarPercent = 0;
|
||
this.oneStarPercent = 0;
|
||
}
|
||
},
|
||
|
||
async fetchUserRating() {
|
||
const response = await fetch(`https://coursesystem.xn--xhq44jb2fzpc.com/get-user-rating?course_id=${this.course.course_id}`, {
|
||
headers: {
|
||
'Authorization': this.token
|
||
}
|
||
});
|
||
if (response.status === 200) {
|
||
const result = await response.json();
|
||
this.userRating = result.rating;
|
||
} else {
|
||
this.userRating = 0; // 用户未评分
|
||
}
|
||
},
|
||
highlightStars(star) {
|
||
this.userRating = star;
|
||
},
|
||
resetStars() {
|
||
this.fetchUserRating(); // 重置为用户实际评分
|
||
},
|
||
confirmRating(star) {
|
||
if (confirm(`您确定要给课程 "${this.course.course_name}" 评分:${star}.0 吗?如您之前有评分记录,此评分将覆盖之前的评分。`)) {
|
||
this.submitRating(star);
|
||
}
|
||
},
|
||
async submitRating(rating) {
|
||
const response = await fetch('https://coursesystem.xn--xhq44jb2fzpc.com/submit-rating', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': this.token
|
||
},
|
||
body: JSON.stringify({
|
||
course_id: this.course.course_id,
|
||
rating: rating
|
||
})
|
||
});
|
||
if (response.ok) {
|
||
this.fetchRatingDistribution();
|
||
this.fetchUserRating();
|
||
}
|
||
},
|
||
// 添加打字机效果方法
|
||
async typewriterEffect(text, speed = 10) {
|
||
this.isTyping = true;
|
||
this.displayedSummary = '';
|
||
this.shouldStopTyping = false; // 重置中断标志
|
||
for (let i = 0; i < text.length; i++) {
|
||
if (this.shouldStopTyping) break; // 检查是否需要中断
|
||
this.displayedSummary += text[i];
|
||
await new Promise(resolve => setTimeout(resolve, speed));
|
||
}
|
||
this.isTyping = false;
|
||
},
|
||
|
||
// 新增获取AI总结的方法
|
||
async fetchAISummary() {
|
||
try {
|
||
const response = await fetch(
|
||
`https://coursesystem.xn--xhq44jb2fzpc.com/ai_summary/summary?course_id=${this.course.course_id}`
|
||
);
|
||
|
||
if (!response.ok) {
|
||
if (response.status === 404) {
|
||
this.aiSummary = null;
|
||
this.displayedSummary = '当前课程的评分和评论数据过少,暂不能生成AI总结。';
|
||
return;
|
||
}
|
||
throw new Error('获取AI总结失败');
|
||
}
|
||
|
||
const result = await response.json();
|
||
if (result.summary) {
|
||
this.aiSummary = result.summary;
|
||
await this.typewriterEffect(this.aiSummary);
|
||
} else {
|
||
this.displayedSummary = '当前课程的评分和评论数据过少,暂不能生成AI总结。';
|
||
}
|
||
} catch (error) {
|
||
console.error('生成AI总结失败:', error);
|
||
this.displayedSummary = '生成AI总结时出错,请稍后再试。';
|
||
}
|
||
},
|
||
|
||
// 从 fetchComments 方法中移除 AI 总结相关的代码
|
||
async fetchComments() {
|
||
try {
|
||
if (!this.token) {
|
||
this.showErrorModal = true;
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 2000);
|
||
return;
|
||
}
|
||
|
||
const response = await fetch(
|
||
`https://coursesystem.xn--xhq44jb2fzpc.com/get-comments?course_id=${this.course.course_id}&sort_by=${this.sortBy}`,
|
||
{
|
||
headers: {
|
||
'Authorization': this.token
|
||
}
|
||
}
|
||
);
|
||
|
||
if (response.status === 403) {
|
||
this.showErrorModal = true;
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 2000);
|
||
return;
|
||
}
|
||
|
||
if (response.status === 404) {
|
||
this.comments = [];
|
||
return;
|
||
}
|
||
|
||
if (!response.ok) {
|
||
throw new Error('获取评论失败');
|
||
}
|
||
|
||
const result = await response.json();
|
||
this.comments = result.comments;
|
||
} catch (error) {
|
||
console.error('获取评论失败:', error);
|
||
this.showErrorModal = true;
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 2000);
|
||
}
|
||
},
|
||
|
||
async toggleLike(comment) {
|
||
if (comment.is_liked) {
|
||
await fetch('https://coursesystem.xn--xhq44jb2fzpc.com/unlike-comment', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': this.token
|
||
},
|
||
body: JSON.stringify({
|
||
comment_id: comment.comment_id
|
||
})
|
||
});
|
||
} else {
|
||
await fetch('https://coursesystem.xn--xhq44jb2fzpc.com/like-comment', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': this.token
|
||
},
|
||
body: JSON.stringify({
|
||
comment_id: comment.comment_id
|
||
})
|
||
});
|
||
}
|
||
this.fetchComments(); // 重新获取评论
|
||
},
|
||
async submitComment() {
|
||
if (this.nickname.length < 3 || this.commentContent.length < 6) {
|
||
alert('您的昵称请至少包含3个字符,评论请至少包含6个字符。');
|
||
return;
|
||
}
|
||
|
||
await fetch('https://coursesystem.xn--xhq44jb2fzpc.com/submit-comment', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': this.token
|
||
},
|
||
body: JSON.stringify({
|
||
course_id: this.course.course_id,
|
||
nickname: this.nickname,
|
||
comment_content: this.commentContent
|
||
})
|
||
});
|
||
|
||
this.fetchComments(); // 重新获取评论
|
||
|
||
// 清空昵称和评论内容输入框
|
||
this.nickname = '';
|
||
this.commentContent = '';
|
||
},
|
||
|
||
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}`;
|
||
},
|
||
|
||
// 新增函数:请求服务器解析JWT并缓存邮箱
|
||
async fetchAndCacheEmail() {
|
||
if (this.cachedEmail) {
|
||
return; // 如果已经缓存了邮箱,则不需要再次请求
|
||
}
|
||
|
||
if (!this.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': this.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);
|
||
}
|
||
},
|
||
|
||
// 修改 getUserEmailFromCookie,直接返回缓存的邮箱
|
||
getUserEmailFromCookie() {
|
||
if (this.cachedEmail) {
|
||
return this.cachedEmail; // 直接返回缓存的邮箱
|
||
} else {
|
||
console.error('邮箱尚未缓存');
|
||
return ''; // 返回空字符串以保持调用的兼容性
|
||
}
|
||
},
|
||
|
||
|
||
|
||
// 生成 Base64 URL 编码
|
||
base64Encode(value) {
|
||
return btoa(value);
|
||
},
|
||
// 处理分享按钮点击事件
|
||
shareCourse() {
|
||
const courseName = this.course.course_name;
|
||
const courseId = this.base64Encode(this.course.course_id.toString());
|
||
|
||
let shareText = '';
|
||
|
||
// 根据 totalRating 生成不同的分享文本
|
||
if (this.totalRating !== 0) {
|
||
// totalRating 不为 0,生成包含评分的分享文本
|
||
shareText = `我在NEU小站为课程【${courseName}】评分,当前评分【${this.totalRating}】,点击【https://course.xn--xhq44jb2fzpc.com/courses?c=${courseId}】加入评分吧!`;
|
||
} else {
|
||
// totalRating 为 0,生成不包含评分的分享文本
|
||
shareText = `我在NEU小站为课程【${courseName}】评分,点击【https://course.xn--xhq44jb2fzpc.com/courses?c=${courseId}】加入评分吧!`;
|
||
}
|
||
|
||
// 将分享文本复制到剪贴板
|
||
this.copyToClipboard(shareText);
|
||
|
||
// 显示分享成功的模态框
|
||
this.showShareModal = true;
|
||
|
||
// 3秒后自动关闭模态框
|
||
setTimeout(() => {
|
||
this.showShareModal = false;
|
||
}, 3000);
|
||
},
|
||
|
||
// 复制文本到剪贴板
|
||
copyToClipboard(text) {
|
||
const textarea = document.createElement('textarea');
|
||
textarea.value = text;
|
||
document.body.appendChild(textarea);
|
||
textarea.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(textarea);
|
||
},
|
||
|
||
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] || '未知院系';
|
||
},
|
||
|
||
// 打开发起聊天模态框
|
||
openChatModal(commentId) {
|
||
this.currentCommentId = commentId;
|
||
this.chatMessage = '';
|
||
this.isChatModalVisible = true;
|
||
},
|
||
|
||
// 关闭发起聊天模态框
|
||
closeChatModal() {
|
||
this.isChatModalVisible = false;
|
||
},
|
||
|
||
// 关闭会话已存在模态框
|
||
closeConversationExistsModal() {
|
||
this.isConversationExistsModalVisible = false;
|
||
},
|
||
|
||
// 提交聊天内容并调用接口
|
||
async submitChat() {
|
||
if (this.chatMessage.length < 6) {
|
||
alert('聊天内容需至少6个字符');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('https://coursesystem.xn--xhq44jb2fzpc.com/chat/start-conversation', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
Authorization: this.token
|
||
},
|
||
body: JSON.stringify({
|
||
comment_id: this.currentCommentId,
|
||
content: this.chatMessage
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok && result.message === 'Conversation started successfully') {
|
||
// 成功创建会话,导航到会话页面
|
||
this.$router.push(`/chat?mid=${result.conversation_id}`);
|
||
} else if (response.ok && result.message === 'Conversation already exists') {
|
||
// 会话已存在,显示已存在模态框
|
||
this.existingConversationId = result.conversation_id;
|
||
this.isConversationExistsModalVisible = true;
|
||
} else if (result.error === 'Recipient account status is abnormal, cannot create a conversation') {
|
||
// 接收方账号异常
|
||
alert('此账号状态异常,暂时无法发起聊天!');
|
||
} else if (result.error === 'Cannot start a conversation with yourself!') {
|
||
// 不能向自己发起聊天
|
||
alert('不能向自己发起聊天!');
|
||
} else {
|
||
// 其他失败情况
|
||
alert('发起会话失败,请稍后再试');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error starting conversation:', error);
|
||
alert('发起会话失败,请稍后再试');
|
||
} finally {
|
||
this.closeChatModal();
|
||
}
|
||
},
|
||
|
||
|
||
|
||
// 跳转到已存在的会话页面
|
||
navigateToConversation(conversationId) {
|
||
this.$router.push(`/chat?mid=${conversationId}`);
|
||
this.closeConversationExistsModal();
|
||
},
|
||
async deleteComment(commentId) {
|
||
const confirmDelete = confirm("确定删除评论吗?");
|
||
if (!confirmDelete) return; // 如果用户点击取消,直接返回
|
||
|
||
try {
|
||
const response = await fetch('https://coursesystem.xn--xhq44jb2fzpc.com/delete-comment', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': this.token
|
||
},
|
||
body: JSON.stringify({
|
||
comment_id: commentId
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
// 删除成功后重新获取评论列表
|
||
this.fetchComments();
|
||
} else {
|
||
const error = await response.json();
|
||
alert(error.error || '删除评论失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('删除评论出错:', error);
|
||
alert('删除评论失败');
|
||
}
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 999;
|
||
}
|
||
|
||
.loading {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-direction: column;
|
||
text-align: center;
|
||
height: 200px; /* 控制加载动画的高度 */
|
||
}
|
||
|
||
.el-icon {
|
||
font-size: 40px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.modal-content {
|
||
background-color: white;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
width: 80%;
|
||
height: 80%;
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
position: relative;
|
||
}
|
||
|
||
.close {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
cursor: pointer;
|
||
font-size: 24px;
|
||
}
|
||
|
||
.course-detail {
|
||
/* width: 40%;
|
||
padding-right: 20px;
|
||
border-right: 1px solid #ccc; */
|
||
max-height: calc(90vh - 120px); /* 设置最大高度,减去头部和其他元素的高度 */
|
||
overflow-y: auto; /* 垂直滚动 */
|
||
overflow-x: auto; /* 水平滚动 */
|
||
padding-right: 15px; /* 为滚动条预留空间 */
|
||
width: 40%;
|
||
/* border-right: 1px solid #ccc; 添加右侧边框作为分隔线 */
|
||
}
|
||
|
||
/* 美化滚动条样式 */
|
||
.course-detail::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px; /* 水平滚动条的高度 */
|
||
}
|
||
|
||
.course-detail::-webkit-scrollbar-track {
|
||
background: #f1f1f1;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.course-detail::-webkit-scrollbar-thumb {
|
||
background: #888;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.course-detail::-webkit-scrollbar-thumb:hover {
|
||
background: #555;
|
||
}
|
||
|
||
/* 确保内容不会被压缩 */
|
||
.course-detail > * {
|
||
min-width: fit-content;
|
||
}
|
||
|
||
.rating-section {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.total-rating {
|
||
font-size: 40px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.stars span {
|
||
font-size: 30px;
|
||
cursor: pointer;
|
||
transition: color 0.3s; /* 平滑过渡 */
|
||
/* padding: 5px; 增大点击区域 */
|
||
|
||
}
|
||
|
||
.stars .highlighted {
|
||
color: gold;
|
||
}
|
||
|
||
.stars span:not(.highlighted) {
|
||
color: #ccc; /* 未点亮星星的颜色 */
|
||
}
|
||
|
||
.comment-section {
|
||
width: 55%;
|
||
padding-left: 20px;
|
||
}
|
||
|
||
.comments-list {
|
||
max-height: 350px;
|
||
overflow-y: auto;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.comment {
|
||
padding: 10px 0;
|
||
border-bottom: 1px solid #ddd;
|
||
}
|
||
|
||
.like-section {
|
||
cursor: pointer;
|
||
color: gray;
|
||
}
|
||
|
||
.like-section.liked {
|
||
color: red;
|
||
}
|
||
|
||
.new-comment {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.new-comment input,
|
||
.new-comment textarea {
|
||
margin-bottom: 10px;
|
||
padding: 5px;
|
||
border-radius: 5px;
|
||
border: 1px solid #ccc;
|
||
}
|
||
|
||
.new-comment button {
|
||
padding: 10px;
|
||
background-color: #007bff;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.comment-meta{
|
||
font-size: 13px;
|
||
}
|
||
|
||
.comment-time{
|
||
font-size: 13px;
|
||
}
|
||
|
||
|
||
|
||
.progress{
|
||
margin-bottom: 5px;
|
||
width: 250px;
|
||
}
|
||
|
||
::v-deep .progress .el-progress-bar__outer {
|
||
height: 20px !important; /* 设置真正的高度 */
|
||
line-height: 40px; /* 调整文字显示 */
|
||
background-color: #a19999; /* 设置进度条未显示部分的背景颜色 */
|
||
|
||
}
|
||
|
||
.course-title {
|
||
display: inline-flex;
|
||
align-items: center; /* 垂直居中 */
|
||
justify-content: center; /* 水平居中 */
|
||
padding: 2px 6px; /* 根据需要调整 */
|
||
border: 1px solid;
|
||
border-radius: 12px; /* 圆角大小 */
|
||
margin-left: 4px;
|
||
font-size: 0.85em;
|
||
min-height: 20px; /* 根据内容调整高度 */
|
||
}
|
||
|
||
/* 分享按钮样式 */
|
||
.share-btn {
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
border: none;
|
||
padding: 6px 12px;
|
||
margin-left: 10px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.share-btn:hover {
|
||
background-color: #45a049;
|
||
}
|
||
|
||
/* 分享成功模态框样式 */
|
||
.share-modal, .error-modal {
|
||
position: fixed;
|
||
top: 20%;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
/* background-color: #4CAF50; */
|
||
color: white;
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
text-align: center;
|
||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||
font-size: 16px;
|
||
z-index: 9999;
|
||
|
||
/* 渐显动画 */
|
||
animation: fadeInDown 0.5s ease;
|
||
}
|
||
|
||
.share-modal {
|
||
background-color: #4CAF50;
|
||
}
|
||
|
||
.error-modal {
|
||
background-color: #ff4444;
|
||
}
|
||
|
||
/* 渐显+从上往下动画 */
|
||
@keyframes fadeInDown {
|
||
0% {
|
||
opacity: 0;
|
||
transform: translate(-50%, -30px); /* 初始位置稍微在上方 */
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: translate(-50%, 0); /* 最终位置 */
|
||
}
|
||
}
|
||
|
||
/* 发起聊天按钮 */
|
||
.chat-start-btn {
|
||
margin-left: 10px;
|
||
padding: 5px 10px;
|
||
background-color: #409eff; /* 按钮背景色 */
|
||
color: #fff; /* 按钮文字颜色 */
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||
}
|
||
|
||
.chat-start-btn:hover {
|
||
background-color: #66b1ff; /* 悬浮时背景色 */
|
||
transform: scale(1.05); /* 悬浮时放大 */
|
||
}
|
||
|
||
.chat-start-btn:active {
|
||
background-color: #3a8ee6; /* 点击时背景色 */
|
||
transform: scale(0.95); /* 点击时缩小 */
|
||
}
|
||
|
||
/* 模态框背景 */
|
||
.chat-modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
/* background: rgba(0, 0, 0, 0.5); 半透明背景 */
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 1000;
|
||
animation: chat-fadeIn 0.3s ease; /* 模态框背景淡入动画 */
|
||
}
|
||
|
||
/* 模态框内容 */
|
||
.chat-modal-content {
|
||
background: #fff; /* 白色背景 */
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
width: 400px;
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 模态框阴影 */
|
||
text-align: center;
|
||
animation: chat-slideIn 0.3s ease; /* 模态框滑入动画 */
|
||
position: relative;
|
||
}
|
||
|
||
/* 模态框关闭按钮 */
|
||
.chat-modal-content .chat-close-btn {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 15px;
|
||
font-size: 20px;
|
||
color: #999;
|
||
cursor: pointer;
|
||
transition: color 0.3s ease;
|
||
}
|
||
|
||
.chat-modal-content .chat-close-btn:hover {
|
||
color: #333;
|
||
}
|
||
|
||
/* 模态框标题 */
|
||
.chat-modal-content h3 {
|
||
margin-top: 0;
|
||
color: #333;
|
||
font-size: 18px;
|
||
}
|
||
|
||
/* 模态框动作按钮容器 */
|
||
.chat-modal-actions {
|
||
margin-top: 20px;
|
||
display: flex;
|
||
justify-content: space-around;
|
||
}
|
||
|
||
/* 模态框按钮样式 */
|
||
.chat-modal-actions button {
|
||
padding: 10px 20px;
|
||
font-size: 14px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||
}
|
||
|
||
/* 发起按钮 */
|
||
.chat-modal-actions button:first-child {
|
||
background-color: #409eff;
|
||
color: white;
|
||
}
|
||
|
||
.chat-modal-actions button:first-child:hover {
|
||
background-color: #66b1ff;
|
||
}
|
||
|
||
.chat-modal-actions button:first-child:active {
|
||
background-color: #3a8ee6;
|
||
}
|
||
|
||
/* 取消按钮 */
|
||
.chat-modal-actions button:last-child {
|
||
background-color: #f5f5f5;
|
||
color: #666;
|
||
}
|
||
|
||
.chat-modal-actions button:last-child:hover {
|
||
background-color: #e0e0e0;
|
||
}
|
||
|
||
.chat-modal-actions button:last-child:active {
|
||
background-color: #d6d6d6;
|
||
}
|
||
|
||
/* 文本区域样式 */
|
||
.chat-textarea {
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 4px;
|
||
padding: 10px;
|
||
font-size: 14px;
|
||
width: 100%;
|
||
height: 100px;
|
||
box-sizing: border-box;
|
||
resize: none;
|
||
outline: none;
|
||
transition: border-color 0.3s ease;
|
||
}
|
||
|
||
.chat-textarea:focus {
|
||
border-color: #409eff;
|
||
}
|
||
|
||
/* 动画效果 */
|
||
@keyframes chat-fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@keyframes chat-slideIn {
|
||
from {
|
||
transform: translateY(-20px);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateY(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.delete-btn {
|
||
color: red;
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
margin-left: 10px;
|
||
font-size: 14px;
|
||
padding: 2px 8px;
|
||
}
|
||
|
||
.delete-btn:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.comment-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 10px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.ai-summary {
|
||
margin-top: 15px;
|
||
padding: 10px;
|
||
background-color: #f5f5f5;
|
||
border-radius: 4px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.typing-cursor {
|
||
display: inline-block;
|
||
animation: blink 0.7s infinite;
|
||
font-weight: bold;
|
||
}
|
||
|
||
@keyframes blink {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0; }
|
||
}
|
||
|
||
</style>
|