界面:
后端代码格式:
前端代码
<template>
<div>
<div v-loading="loading" >
<div class="single-course-intro d-flex align-items-center justify-content-center"
:style="'background-image: url(' + course.imageUrl + ');'">
<div class="single-course-intro-content text-center">
<h3>{{ course.courseName | simpleStrFilter }}</h3>
<div class="meta d-flex align-items-center justify-content-center"
v-if="course.college">
<a href="#">{{ course.teacher }}</a>
<span><i class="fa fa-circle" aria-hidden="true"></i></span>
<a href="#">{{ course.college }} & {{ course.major }}</a>
</div>
<div class="price">{{ course.chargeType === 1 ? $t('exam.course.charge') : $t('exam.course.free') }}
<h6 v-if="course.chargePrice > 0">{{ course.chargePrice }}</h6>
</div>
<div class="favorite-btn" v-show="favoriteBtnText !== undefined" @click="handleFavorite" >
<i :class="detail.favorite ? 'favorite-icon el-icon-star-on' : 'cancel-favorite-icon el-icon-star-off'"></i>
<span>{{ favoriteBtnText }}</span>
</div>
</div>
</div>
<div class="single-course-content padding-50">
<el-row class="my-content-container ml-100 mr-100">
<el-col :span="18" style="padding-right: 40px;">
<el-tabs v-model="activeName">
<el-tab-pane name="desc">
<span slot="label">
<span class="exam-content-btn">{{$t('exam.course.courseIntroduction')}}</span>
</span>
<div class="clever-description">
<div class="about-course mb-30">
<p v-html="course.courseDescription"></p>
</div>
</div>
</el-tab-pane>
<el-tab-pane name="chapter">
<span slot="label">
<span class="exam-content-btn">{{$t('exam.course.chapter')}}</span>
</span>
<div class="about-curriculum mb-30">
<transition name="fade-transform" mode="out-in"
v-for="chapter in detail.chapters" :key="chapter.chapter.id">
<div class="chapter-container">
<p class="chapter-title">{{ chapter.chapter.title }}</p>
<div class="section-container"
v-for="section in chapter.sections" :key="section.section.id">
<p class="section-title" @click="handleClickSection(section.section)">
{{ section.section.title }}
<span class="section-learn-hour" v-if="section.section && section.section.learnHour !== undefined">
<i class="el-icon-caret-right"></i> {{
section.section.learnHour
}}{{$t('exam.course.hour')}}
</span>
</p>
<div class="point-container" v-for="point in section.points"
:key="point.id">
<p class="point-title" @click="handleClickPoint(section.section, point)">
{{ point.title }}
<span class="section-learn-hour">
<i class="el-icon-caret-right"></i> {{
point.learnHour
}}{{$t('exam.course.hour')}}
</span>
</p>
</div>
</div>
</div>
</transition>
</div>
</el-tab-pane>
<el-tab-pane name="evaluate">
<span slot="label">
<span class="exam-content-btn">{{$t('exam.course.courseEvaluation')}}</span>
</span>
<div class="about-review mb-30">
<div>
<el-form :model="evaluate">
<el-form-item label="">
<el-input type="textarea" :rows="3" :placeholder="$t('exam.course.inputEvaluation')"
v-model="evaluate.evaluateContent"></el-input>
</el-form-item>
<el-form-item label="">
<el-rate v-model="evaluate.evaluateLevel"></el-rate>
</el-form-item>
<el-form-item>
<el-button type="primary" class="clever-btn"
@click="handleSubmitEvaluate">{{$t('submit')}}
</el-button>
</el-form-item>
</el-form>
</div>
<div>
<div v-for="item in evaluates" :key="item.id">
<evaluate-item :item="item"></evaluate-item>
</div>
<el-row class="list-pagination" style="margin-top: 16px;" v-show="evaluates && evaluates.length > 10">
<el-pagination
@size-change="handleEvaluateSizeChange"
@current-change="handleEvaluateCurrentChange"
:current-page="evaluateQuery.page"
:page-sizes="[10, 20, 50]"
:page-size="10"
layout="total, sizes, prev, pager, next, jumper"
:total="evaluateTotal">
</el-pagination>
</el-row>
</div>
</div>
</el-tab-pane>
<el-tab-pane name="members">
<span slot="label">
<span class="exam-content-btn">{{$t('exam.course.examinations')}}</span>
</span>
<div class="about-members mb-30">
<p v-if="detail.examinations && detail.examinations.length > 0">
<a v-for="(e, index) in detail.examinations" :key="index" :href="'#/exam-details?examId=' + e.id" target="_self">《{{e.examinationName}}》</a>
</p>
</div>
</el-tab-pane>
<el-tab-pane name="learn">
<span slot="label">
<span class="exam-content-btn">{{$t('exam.course.studyExchange')}}</span>
</span>
<div class="about-review mb-30">
<!-- <p>-->
<!-- {{$t('exam.course.courseAttach')}}:<a :href="courseAttachUrl" target="_blank">{{courseAttachName}}</a>-->
<!-- </p>-->
<div style="display: flex;justify-content: end"><a style="cursor: pointer;" @click="toggleExchangeInput">讨论</a></div>
<div class="user-evaluate-item-reply" v-show="showExchangeInput" >
<el-row :gutter="10" >
<el-col :span="10">
<input @blur="handleExchangeBlur" ref="exchangeInputRef" v-model="ExchangeInputValue" style="width: 100%;height: 20px;margin: 10px 0 10px 0" placeholder="说些什么吧" />
</el-col>
<el-col :span="4">
<div style="width: 100%;height: 20px;margin: 10px 0 10px 0;cursor: pointer" @click="sumbitExchange" >发送</div>
</el-col>
</el-row>
</div>
<div class="exchange-contain" v-for="(exchange,index) in exchangeList" :key = "exchange.id">
<el-row class="user-evaluate-item-bg">
<el-col :span="2">
<!-- <img width="40" height="40" class="user-evaluate-item-avatar" src="../../../../互联网+.jpg">-->
<i class="iconfont icon-user" style="font-size: 40px; color: #5a5a5a;"></i>
</el-col>
<el-col :span="22">
<div class="user-evaluate-item-top">
<span style="color: #333; margin-right: 15px;">{{ exchange.userName }}</span>
</div>
<div class="user-evaluate-item-content" style="color:#666;">
{{exchange.content}}
</div>
<div class="user-evaluate-item-time">
{{ exchange.time }}
<a style="cursor: pointer" @click="toggleReply(exchange.id,index)"> 回复</a>
</div>
<div class="user-evaluate-item-reply" v-show="exchange.showReplyInput" >
<el-row :gutter="10" >
<el-col :span="10">
<input style="width: 100%;height: 20px;margin: 10px 0 10px 0" v-model="ReplyInputValue" @blur="handleReplyBlur(exchange.id)" ref="toggleReplyRef" placeholder="说些什么吧" />
</el-col>
<el-col :span="4">
<div @click="sumbitReply(exchange.id,index)" style="width: 100%;height: 20px;margin: 10px 0 10px 0;cursor: pointer">发送</div>
</el-col>
</el-row>
</div>
</el-col>
</el-row>
<el-row v-for="(reply, index) in exchange.replyList" :key="reply.id" class="user-evaluate-item-bg0" style="margin-top: 10px">
<el-col :span="2" :offset="2">
<!-- <img width="40" height="40" class="user-evaluate-item-avatar" src="../../../../互联网+.jpg">-->
<i class="iconfont icon-user" style="font-size: 40px; color: #5a5a5a;"></i>
</el-col>
<el-col :span="20">
<div class="user-evaluate-item-top">
<span style="color: #333; margin-right: 15px;">{{ reply.userName }}</span>
</div>
<div class="user-evaluate-item-content" style="color:#666;">
{{ reply.reply }}
</div>
<div class="user-evaluate-item-time" style="">
{{reply.time}}
<a style="cursor: pointer" @click="toggleReply2(exchange.id,reply.id,index)"> 回复</a>
</div>
<div class="user-evaluate-item-reply" v-show="reply.showReply2Input">
<el-row :gutter="10" >
<el-col :span="10">
<input v-model="ReplyInputValue2" :ref="`toggleReplyRef2_${reply.id}`" @blur="handleReply2Blur(exchange.id,reply.id)" style="width: 100%;height: 20px;margin: 10px 0 10px 0" placeholder="说些什么吧" />
</el-col>
<el-col :span="4">
<div @click="sumbitReply2(reply.id,exchange.id,index)" style="width: 100%;height: 20px;margin: 10px 0 10px 0;cursor: pointer">发送</div>
</el-col>
</el-row>
</div>
</el-col>
</el-row>
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-col>
<el-col :span="6">
<div class="course-sidebar">
<el-button type="primary" class="clever-btn mb-30 w-100" style="margin-left: 0;" @click="handleStartLearn">
{{ $t('exam.course.startLearn') }}
</el-button>
<div class="sidebar-widget">
<h4>{{$t('exam.course.courseFeatures')}}</h4>
<ul class="features-list">
<li>
<h6><i class="el-icon-alarm-clock"></i>{{$t('exam.course.learnHour')}}</h6>
<h6>{{ detail.learnHour }}</h6>
</li>
<li>
<h6><i class="el-icon-bell"></i>{{$t('exam.course.chapter1')}}</h6>
<h6>{{ detail.chapterSize }}</h6>
</li>
<li>
<h6><i class="el-icon-files"></i>{{$t('exam.course.memberCount')}}</h6>
<h6>{{ detail.memberCount }}</h6>
</li>
<li>
<h6><i class="el-icon-files"></i>{{$t('exam.course.evaluatesCount')}}</h6>
<h6>{{ evaluates.length }}</h6>
</li>
</ul>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import {
getCourseExchange,
getCourseDetail,
joinCourse,
getCourseAttach,
favoriteCourse,
sumbitExchange,
sumbitReply
} from '@/api/exam/course'
import {addObj, getEvaluateList} from '@/api/exam/courseEvaluate'
import {messageSuccess, messageWarn} from '@/utils/util'
import EvaluateItem from '@/components/EvaluateItem'
import ElMessageBox from "@/locales/lang/en/en";
export default {
components: {
EvaluateItem
},
data() {
return {
showExchangeInput: false,
ExchangeInputValue: '',
ReplyInputValue: '',
ReplyInputValue2: '',
exchangeList: [],
loading: true,
courseId: '',
course: {},
detail: {},
value: 3.7,
activeName: 'desc',
evaluate: {
evaluateContent: '',
evaluateLevel: 5
},
evaluates: [],
hasEvaluate: false,
courseAttachName: '',
courseAttachUrl: '',
favoriteBtnText: undefined,
favoriteBtnLoading: false,
evaluateTotal: 0,
evaluateQuery: {
page: 1,
courseId: undefined
}
}
},
created() {
this.courseId = this.$route.query.courseId
this.evaluateQuery.courseId = this.$route.query.courseId
this.getCourseInfo()
this.getEvaluateList()
this.getAttach()
this.getCourseExchangeInfo();
},
computed: {
...mapState({
userInfo: state => state.user.userInfo
})
},
methods: {
sumbitReply2(id, exchangeId, index) {
if (this.ReplyInputValue2 === '') {
this.$refs[`toggleReplyRef2_${id}`][index].focus();
clearTimeout(this.replyTimer2);
return;
}
clearTimeout(this.replyTimer2);
var formData = new FormData();
formData.append("userId", this.userInfo.id)
formData.append("userName", this.userInfo.identifier)
formData.append("exchangeId", exchangeId)
formData.append("reply", this.ReplyInputValue2)
sumbitReply(formData).then(res => {
const newReply = {
id: Math.floor(Math.random() * 1000), // 新元素的 id
exchangeId: exchangeId,
newRecord: false,
reply: this.ReplyInputValue2,
time: this.getCurrentTime(),
userId: this.userInfo.id,
userName: this.userInfo.identifier
};
const targetExchange = this.exchangeList.find(exchange => exchange.id === exchangeId);
if (targetExchange) {
const targetReply = targetExchange.replyList.find(reply => reply.id === id);
targetReply.showReply2Input = !targetReply.showReply2Input;
this.ReplyInputValue2 = ''
}
targetExchange.replyList.push(newReply);
}).catch(error => {
console.error(error)
})
},
handleReply2Blur(exchangeId, id) {
const targetExchange = this.exchangeList.find(exchange => exchange.id === exchangeId);
// 清除之前可能存在的定时器
clearTimeout(this.replyTimer2);
// 设置一个新的定时器
this.replyTimer2 = setTimeout(() => {
// 在这里写定时器要执行的代码
if (targetExchange) {
const targetReply = targetExchange.replyList.find(reply => reply.id === id);
targetReply.showReply2Input = !targetReply.showReply2Input;
}
}, 300);
},
toggleReply2(exchangeId, id, index) {
const targetExchange = this.exchangeList.find(exchange => exchange.id === exchangeId);
if (targetExchange) {
const targetReply = targetExchange.replyList.find(reply => reply.id === id);
targetReply.showReply2Input = !targetReply.showReply2Input;
this.ReplyInputValue2 = "@" + targetReply.userName + ": ";
}
this.$nextTick(() => {
this.$refs[`toggleReplyRef2_${id}`][0].focus();
});
},
sumbitReply(id, index) {
if (this.ReplyInputValue === '') {
this.$refs.toggleReplyRef[index].focus();
clearTimeout(this.replyTimer);
return;
}
clearTimeout(this.replyTimer);
var formData = new FormData();
formData.append("userId", this.userInfo.id)
formData.append("userName", this.userInfo.identifier)
formData.append("exchangeId", id)
formData.append("reply", this.ReplyInputValue)
sumbitReply(formData).then(res => {
const newReply = {
id: Math.floor(Math.random() * 1000), // 新元素的 id
exchangeId: id,
newRecord: false,
reply: this.ReplyInputValue,
time: this.getCurrentTime(),
userId: this.userInfo.id,
userName: this.userInfo.identifier
};
const targetExchange = this.exchangeList.find(exchange => exchange.id === id);
targetExchange.showReplyInput = false;
this.ReplyInputValue = ''
targetExchange.replyList.push(newReply);
}).catch(error => {
console.error(error)
})
},
handleReplyBlur(id) {
const targetExchange = this.exchangeList.find(exchange => exchange.id === id);
// 清除之前可能存在的定时器
clearTimeout(this.replyTimer);
// 设置一个新的定时器
this.replyTimer = setTimeout(() => {
// 在这里写定时器要执行的代码
if (targetExchange) {
targetExchange.showReplyInput = !targetExchange.showReplyInput;
}
}, 300);
},
toggleReply(id, index) {
const targetExchange = this.exchangeList.find(exchange => exchange.id === id);
if (targetExchange) {
targetExchange.showReplyInput = !targetExchange.showReplyInput;
}
this.$nextTick(() => {
this.$refs.toggleReplyRef[index].focus();
this.ReplyInputValue = "@" + targetExchange.userName + ": ";
});
},
handleExchangeBlur() {
// 清除之前可能存在的定时器
clearTimeout(this.exchangeTimer);
// 设置一个新的定时器
this.exchangeTimer = setTimeout(() => {
// 在这里写定时器要执行的代码
this.showExchangeInput = !this.showExchangeInput;
}, 300);
},
sumbitExchange() {
if (this.ExchangeInputValue === '') {
this.$refs.exchangeInputRef.focus();
clearTimeout(this.exchangeTimer);
return;
}
clearTimeout(this.exchangeTimer);
var formData = new FormData();
formData.append("courseId", this.courseId)
formData.append("courseTitle", this.course.courseName)
formData.append("userId", this.userInfo.id)
formData.append("userName", this.userInfo.identifier)
formData.append("content", this.ExchangeInputValue)
sumbitExchange(formData).then(res => {
const newExchange = {
id: Math.floor(Math.random() * 1000), // 新元素的 id
content: this.ExchangeInputValue,
courseId: this.courseId,
courseTitle: this.course.courseName,
replyList: [],
time: this.getCurrentTime(),
userId: this.userInfo.id,
userName: this.userInfo.identifier
};
this.showExchangeInput = !this.showExchangeInput;
this.ExchangeInputValue = '';
this.exchangeList.unshift(newExchange);
}).catch(error => {
console.error(error)
})
},
getCurrentTime() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0'); // 月份从 0 开始,所以要加 1
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const formattedTime = `${year}-${month}-${day} ${hours}:${minutes}`;
return formattedTime;
},
toggleExchangeInput() {
this.showExchangeInput = !this.showExchangeInput;
this.$nextTick(() => {
this.$refs.exchangeInputRef.focus();
});
},
getCourseExchangeInfo() {
getCourseExchange(this.courseId).then(res => {
console.log(res)
this.exchangeList = res.data.result.map(exchange => ({
...exchange,
showReplyInput: false, // 添加showReplyInput属性,并设置为false
replyList: exchange.replyList.map(reply => ({
...reply,
showReply2Input: false // 添加showReply2Input属性,并设置为false
}))
}));
console.log(this.exchangeList)
}).catch(error => {
console.error(error)
this.loading = false
})
},
getCourseInfo() {
this.loading = true
getCourseDetail(this.courseId).then(res => {
this.detail = res.data.result
this.course = res.data.result.course
this.updateFavoriteBtnText()
this.loading = false
}).catch(error => {
console.error(error)
this.loading = false
})
},
getEvaluateList() {
getEvaluateList({...this.evaluateQuery}).then(res => {
const {code} = res.data
if (code === 0) {
this.evaluates = res.data.result.list
this.evaluateTotal = res.data.result.total
}
}).catch(error => {
console.error(error)
})
},
getAttach() {
getCourseAttach(this.courseId).then(res => {
const {code, result} = res.data
if (code === 0 && result) {
this.courseAttachName = result.attachName
this.courseAttachUrl = result.attachUrl
}
}).catch(error => {
console.error(error)
})
},
handleClick(tab, event) {
},
handleClickSection(section) {
},
handleClickPoint(section, point) {
},
handleSubmitEvaluate() {
if (this.hasEvaluate) {
messageWarn(this, this.$t('exam.course.doNotResubmit'))
return
}
if (this.evaluate.evaluateContent === '') {
this.evaluate.evaluateContent = this.$t('exam.course.defaultEvaluate')
}
addObj({
courseId: this.courseId,
...this.evaluate
}).then(res => {
if (res.data.code === 0) {
this.evaluate.evaluateContent = ''
this.hasEvaluate = true
messageSuccess(this, this.$t('exam.course.submitSuccess'))
this.getEvaluateList()
} else {
messageWarn(this, this.$t('exam.course.submitFailed'))
}
}).catch(error => {
console.error(error)
})
},
handleStartLearn() {
joinCourse(this.courseId).then(res => {
if (res.data.result) {
const chapters = this.detail.chapters
if (chapters && chapters.length > 0) {
const chapter = chapters[0]
const courseId = chapter.chapter.courseId
const sections = chapter.sections
if (sections && sections.length > 0) {
const sectionId = sections[0].section.id
this.$router.push({
name: 'course-section',
query: {sectionId: sectionId, courseId}
})
}
}
}
}).catch(error => {
console.error(error)
})
},
handleFavorite() {
const userId = this.userInfo.id
let type = this.detail && this.detail.favorite ? 0 : 1;
const tips = type === 1 ? this.$t('fav.favorite') : this.$t('fav.cancelFavorite')
this.favoriteBtnLoading = true
favoriteCourse(this.courseId, userId, type).then(res => {
if (res.data.result) {
this.detail.favorite = !this.detail.favorite
} else {
messageWarn(this, tips + this.$t('failed'))
}
}).catch(error => {
console.error(error)
messageWarn(this, tips + this.$t('failed'))
}).finally(() => {
this.updateFavoriteBtnText()
this.favoriteBtnLoading = false
})
},
updateFavoriteBtnText() {
if (this.detail && this.detail.favorite) {
this.favoriteBtnText = this.$t('fav.cancelFavorite')
} else {
this.favoriteBtnText = this.$t('fav.favorite')
}
},
handleEvaluateSizeChange(val) {
this.evaluateQuery.pageSize = val
this.getEvaluateList()
},
handleEvaluateCurrentChange(val) {
this.evaluateQuery.page = val
this.getEvaluateList()
}
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.chapter-title {
color: #1c1f21;
font-size: 16px;
font-weight: 700;
line-height: 24px;
}
.section-title {
font-size: 15px;
}
.user-evaluate-item-reply {
margin-top: 4px;
}
</style>
前端api配置:
export function getCourseExchange (id) {
return request({
url: baseUrl + 'exchange/' + id,
method: 'get'
})
}
export function sumbitExchange (data) {
return request({
url: baseUrl + 'sumbitExchange',
method: 'post',
data: data
})
}
export function sumbitReply (data) {
return request({
url: baseUrl + 'sumbitReply',
method: 'post',
data: data
})
}
后端代码controller:
@GetMapping("/exchange/{id}")
@Operation(summary = "获取课程评价")
public R<List<CourseExchangeDto>> exchange(@PathVariable Long id) {
return R.success(examCourseExchangeService.getCourseExchange(id));
}
@PostMapping("/sumbitExchange")
@Operation(summary = "添加课程评价")
public R<Boolean> sumbitExchange(CourseExchangeSumbitDto courseExchangeSumbitDto) {
return R.success(examCourseExchangeService.sumbitExchange(courseExchangeSumbitDto));
}
@PostMapping("/sumbitReply")
@Operation(summary = "添加课程评价回复")
public R<Boolean> sumbitReply(CourseExchangeReplySumbitDto courseExchangeReplySumbitDto) {
return R.success(examCourseExchangeService.sumbitReply(courseExchangeReplySumbitDto));
}
@Data
public class CourseExchangeSumbitDto {
private Long courseId;
private String courseTitle;
private Long userId;
private String userName;
private String content;
private String time;
}
@Data
public class CourseExchangeReplySumbitDto {
private Long userId;
private String userName;
private Long exchangeId;
private String reply;
private String time;
}
主体实现:
@Override
public List<CourseExchangeDto> getCourseExchange(Long id) {
List<ExamCourseExchange> courseExchange = examCourseExchangeMapper.getCourseExchange(id);
List<CourseExchangeDto> collect = courseExchange.stream().map(s -> {
CourseExchangeDto courseExchangeDto = new CourseExchangeDto();
BeanUtils.copyProperties(s, courseExchangeDto);
List<ExamCourseReply> courseReply = replyMapper.getCourseReply(s.getId());
courseExchangeDto.setReplyList(courseReply);
return courseExchangeDto;
}).collect(Collectors.toList());
return collect;
}
@Override
public Boolean sumbitExchange(CourseExchangeSumbitDto courseExchangeSumbitDto) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyy-MM-dd HH:mm");
String format = simpleDateFormat.format(new Date());
courseExchangeSumbitDto.setTime(format);
return examCourseExchangeMapper.sumbitExchange(courseExchangeSumbitDto);
}
@Override
public Boolean sumbitReply(CourseExchangeReplySumbitDto courseExchangeReplySumbitDto) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyy-MM-dd HH:mm");
String format = simpleDateFormat.format(new Date());
courseExchangeReplySumbitDto.setTime(format);
return replyMapper.sumbitReply(courseExchangeReplySumbitDto);
}