course_system/src/views/CourseDetailModal.vue
2025-04-19 13:30:41 +08:00

1235 lines
34 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="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>