Ver Fonte

讲义埋点,课程快进埋点,课程足迹,章节讨论功能

canghailong há 6 meses atrás
pai
commit
6635e0f6f5

+ 4 - 0
src/api/forum/forumApi.js

@@ -14,6 +14,10 @@ export default {
 	submitForm(data, edit = false) {
 		return request(`forum/postinfo/${edit ? 'edit' : 'add'}`, data)
 	},
+	// 发帖接口 // 章节讨论
+	submitFormGetChapter(data) {
+		return request('forum/postinfo/getChapter', data, 'get')
+	},
 	// 帖子详情接口
 	forumTypeDetail(data) {
 		return request('forum/postinfo/detail', data, 'get')

+ 8 - 0
src/api/student/classCentre.js

@@ -73,5 +73,13 @@ export default {
 	//课程公告-列表
 	classNotice(data) {
 		return request('notice/page', data, 'get')
+	},
+	//学习足迹埋点
+	footprintClassAdd(data) {
+		return request('courselog/add', data)
+	},
+	//学习足迹
+	footprintClassList(data) {
+		return request('courselog/page', data, 'get')
 	}
 }

+ 1 - 1
src/router/index.js

@@ -89,7 +89,7 @@ router.beforeEach(async (to, from, next) => {
 	}
 	if (!token) {
 		next({
-			path: '/login'
+			path: '/slogin'
 		})
 		return false
 	}

+ 1 - 1
src/utils/request.js

@@ -27,7 +27,7 @@ const service = axios.create({
 	baseURL: '/api', // api base_url
 	timeout: sysConfig.TIMEOUT // 请求超时时间
 })
-const whiteList = ['footprint/add', 'coursestudentburialpoint/add', 'coursecentry/addViewCount']
+const whiteList = ['footprint/add', 'coursestudentburialpoint/add', 'coursecentry/addViewCount','courselog/add']
 function urlStr(str) {
 	const last = str.lastIndexOf('/')
 	const secondLast = str.lastIndexOf('/', last - 1)

+ 3 - 5
src/views/forum/addForum.vue

@@ -8,7 +8,7 @@
 							<a-input v-model:value="formData.postTitle" placeholder="请输入标题" allow-clear />
 						</a-form-item>
 					</a-col>
-					<a-col :span="8">
+					<a-col :span="8" v-if="formData.postType==0">
 						<a-form-item label="分类:" name="typeId">
 							<a-select
 								v-model:value="formData.typeId"
@@ -103,7 +103,7 @@
 		if (route.query.postType == 2) {
 			errorCorrection()
 		}
-		formData.value.typeId = typeOptions.value.filter((r) => r.typeDesc == route.query.postType)[0]?.typeId
+		formData.value.postType = parseFloat(route.query.postType)
 	}
 	onMounted(() => {
 		getData()
@@ -137,14 +137,12 @@
 					params = {
 						...formData.value,
 						...browserObj.value,
-						postType: route.query.postType
 					}
 				}
 				if (route.query.postType == 2) {
 					params = {
 						...formData.value,
 						...errorVal.value,
-						postType: route.query.postType
 					}
 				}
 				forumApi.submitForm(params).then(() => {
@@ -176,7 +174,7 @@
 	function errorCorrection() {
 		errorVal.value = {
 			contentCorrectionParam: {
-				resourceType: route.query.postType,
+				resourceType: route.query.resourceType,
 				resourceId: route.query.id
 			}
 		}

+ 40 - 7
src/views/forum/detail.vue

@@ -2,7 +2,7 @@
 	<div style="display: flex; justify-content: center" class="main-content-wrapper">
 		<div style="width: 1200px">
 			<a-card>
-				<div style="display: flex; justify-content: space-between">
+				<div style="display: flex; justify-content: space-between" v-if="detailObj.userNickName">
 					<div style="display: flex">
 						<a-avatar
 							style="width: 60px; height: 60px"
@@ -20,7 +20,7 @@
 				<div class="forum-list-title">{{ detailObj.postTitle }}</div>
 				<div class="htmlContent" v-html="detailObj.postContent"></div>
 				<div>
-					<span>
+					<span v-if="detailObj.userNickName">
 						<a-tooltip title="点赞">
 							<template v-if="detailObj.isLike == 1">
 								<HeartOutlined :style="{ color: '#fa6c8d' }" @click="like(detailObj, 0)" />
@@ -41,7 +41,7 @@
 						<span>回复</span>
 						<span style="padding-left: 8px">{{ detailObj.replyCount }}</span>
 					</span>
-					<a-tooltip title="举报">
+					<a-tooltip title="举报" v-if="detailObj.userNickName">
 						<WarningOutlined class="ml-2" @click="reportFormRef.onOpen(detailObj, detailObj.postId)" />
 					</a-tooltip>
 				</div>
@@ -117,17 +117,50 @@
 	}
 	function morePaging() {
 		pagination.value.current += 1
-		getDetail(true)
+		if (route.query.postId) {
+			getDetail(true)
+		} else {
+			submitFormGetChapter(true)
+		}
 	}
 	function resetGetList() {
 		pagination.value.size = 10
 		pagination.value.current = 1
 		moreType.value = true
-		getDetail()
+		if (route.query.postId) {
+			getDetail()
+		} else {
+			submitFormGetChapter()
+		}
+	}
+	const submitFormGetChapter = (p) => {
+		forumApi
+			.submitFormGetChapter({
+				courseId: route.query.courseId,
+				chapterId: route.query.chapterId,
+				courseName: route.query.courseName,
+				chapterName: route.query.chapterName,
+				...pagination.value
+			})
+			.then((data) => {
+				if (p) {
+					if (data.replyList.records.length > 0) {
+						detailObj.value.replyList.records = detailObj.value.replyList.records.concat(data.replyList.records)
+					} else {
+						pagination.value.current -= 1
+						moreType.value = false
+					}
+				} else {
+					detailObj.value = data
+				}
+			})
 	}
-	
 	onMounted(() => {
-		getDetail()
+		if (route.query.postId) {
+			getDetail()
+		} else {
+			submitFormGetChapter()
+		}
 	})
 </script>
 

Diff do ficheiro suprimidas por serem muito extensas
+ 3 - 2
src/views/forum/index.vue


+ 1 - 1
src/views/forum/replyForm.vue

@@ -56,7 +56,7 @@
 
 	// 打开抽屉
 	const onOpen = (record, module, childId, formType) => {
-		moduleId.value = module
+		moduleId.value = module ?? record.postId
 		replyid.value = childId ?? '-1'
 		visible.value = true
 		formData.value.formType = formType

+ 5 - 3
src/views/portal/components/Header.vue

@@ -28,7 +28,9 @@
 				<template #overlay>
 					<a-menu>
 						<a-menu-item key="1" @click="jump('/userInfo')">个人中心</a-menu-item>
-						<a-menu-item key="2" @click="jump('/forum')">论坛</a-menu-item>
+						<a-menu-item key="2">
+							<router-link :to="{ path: '/forum' }" target="_blank">论坛</router-link>
+						</a-menu-item>
 						<a-menu-item key="3" @click="jump('/inSsiteMessage')">站内信</a-menu-item>
 						<a-menu-item key="4" @click="jump('/classNotice')">课程公告</a-menu-item>
 						<a-menu-item key="5" @click="jump('/learningFootprint')">学习足迹</a-menu-item>
@@ -61,10 +63,10 @@
 	const current = ref([route.path.slice(1)]) // 默认选中“资源中心”
 	const emit = defineEmits(['onChangeCurrent'])
 	const userInfo = tool.data.get('USER_INFO')
-	
+
 	watch(route, (newValue) => {
 		current.value = []
-		nextTick(()=>{
+		nextTick(() => {
 			current.value = [newValue.path.slice(1)]
 		})
 	})

+ 5 - 69
src/views/student/classCentre/form.vue

@@ -7,19 +7,11 @@
 		@close="onClose"
 		:mask="false"
 	>
-		<div v-if="itemObj.key == 2" style="height: 100%">
-			<div v-if="!showPdf">渲染pdf失败</div>
-			<vue-office-pdf :src="itemObj.url" style="width: 100%; height: 100%" @error="errorHandler" />
+		<div v-if="itemObj.key == 2" style="height: 100%;">
+			<handouts :itemObj="itemObj"></handouts>
 		</div>
 		<div v-if="itemObj.key == 3" style="height: 100%">
-			<div v-if="subtitle">{{ subtitle }}</div>
-			<a-input v-model:value="subtitleVal" allowClear placeholder="请输入" />
-			<div class="mt-2 subtitleBox pb-4">
-				<div v-for="(item, idx) in subtitleList" :key="idx">
-					<div>{{ item.startTime }}~{{ item.endTime }}</div>
-					<div style="cursor: pointer; padding: 10px 0" @click="videoSpeed(item)">{{ item.text }}</div>
-				</div>
-			</div>
+			<subtitleBox :url="itemObj.url" @videoSpeed="videoSpeed"></subtitleBox>
 		</div>
 		<div v-if="itemObj.type == 2 || itemObj.type == 4">
 			<a-card :bordered="false">
@@ -54,8 +46,8 @@
 	import { required } from '@/utils/formRules'
 	import note from './note.vue'
 	import askDiv from './ask.vue'
-	import VueOfficePdf from '@vue-office/pdf'
-	import axios from 'axios'
+	import handouts from './handouts.vue'
+	import subtitleBox from './subtitle.vue'
 	const props = defineProps({
 		rightItem: {
 			type: [Array, Object],
@@ -67,9 +59,6 @@
 		}
 	})
 	const itemObj = computed(() => {
-		if (props.rightItem?.url) {
-			GetSrtInfo(props.rightItem.url)
-		}
 		return props.rightItem
 	})
 	const noteRef = ref()
@@ -142,63 +131,10 @@
 	const videoSpeed = (e) => {
 		emit('videoSpeed', e)
 	}
-	const subtitle = ref()
-	//获取字幕内容,发送一个get请求
-	function GetSrtInfo(srtUrl) {
-		subtitle.value = ''
-		axios
-			.get(`${srtUrl}`)
-			.then(function (response) {
-				try {
-					let textList = response.data
-						.split(/\n\s\n/)
-						.filter((item) => item != '')
-						.map((item, index) => {
-							let textItem = item.split(/\n/)
-							return {
-								index: index,
-								sort: textItem[0],
-								text: textItem[2],
-								startTime: ToSeconds(textItem[1].split(' --> ')[0]),
-								endTime: ToSeconds(textItem[1].split(' --> ')[1]),
-								timeLine: textItem[1]
-							}
-						})
-					itemObj.value.srtInfoList = textList
-				} catch (error) {
-					subtitle.value = '字幕解析失败'
-				}
-			})
-			.catch(function (error) {
-				console.log(error)
-			})
-	}
-	//将时间转化为秒
-	function ToSeconds(t) {
-		var s = 0.0
-		if (t) {
-			var p = t.split(':')
-			for (let i = 0; i < p.length; i++) {
-				s = s * 60 + parseFloat(p[i].replace(',', '.'))
-			}
-		}
-		return s
-	}
 	const showPdf = ref(true)
 	function errorHandler() {
 		showPdf.value = false
 	}
-	// 模糊查询函数
-	const subtitleVal = ref()
-	const subtitleList = computed(() => {
-		return itemObj.value.srtInfoList?.filter((item) => {
-			if (subtitleVal.value) {
-				return item.text.includes(subtitleVal.value)
-			} else {
-				return true
-			}
-		})
-	})
 	// 调用这个函数将子组件的一些数据和方法暴露出去
 	defineExpose({
 		onOpen

+ 97 - 0
src/views/student/classCentre/handouts.vue

@@ -0,0 +1,97 @@
+<template>
+	<div style="height: 900px">
+		<div class="mb-4">
+			<a-button type="primary" @click="downPdf">下载讲义</a-button>
+		</div>
+		<div v-if="!showPdf">渲染pdf失败</div>
+		<vue-office-pdf
+			:src="props.itemObj.url"
+			style="width: 100%; height: 100%"
+			@error="errorHandler"
+			ref="scrollDiv"
+			@scroll="handleScroll"
+		/>
+	</div>
+</template>
+
+<script setup>
+	import classCentre from '@/api/student/classCentre'
+	import { message } from 'ant-design-vue'
+	import VueOfficePdf from '@vue-office/pdf'
+	const scrollDiv = ref(null)
+	const scrollPercent = ref(0)
+	const showPdf = ref(true)
+	const props = defineProps({
+		itemObj: {
+			type: Object,
+			default: () => {}
+		},
+		hourId: {
+			type: [String, Number],
+			default: 0
+		}
+	})
+	function errorHandler() {
+		showPdf.value = false
+	}
+	const maxStr = ref(0)
+	function handleScroll() {
+		const el = scrollDiv.value.$el
+		if (!el) return
+		const scrollTop = el.scrollTop
+		const scrollHeight = el.scrollHeight
+		const clientHeight = el.clientHeight
+		// 计算百分比
+		scrollPercent.value = Math.round((scrollTop / (scrollHeight - clientHeight)) * 100)
+		if (scrollPercent.value > maxStr.value) {
+			maxStr.value = scrollPercent.value
+		}
+	}
+	const downPdf = () => {
+		const link = document.createElement('a')
+		link.href = props.itemObj.url
+		link.download = props.itemObj.name
+		document.body.appendChild(link)
+		link.click()
+		document.body.removeChild(link)
+		message.success('开始下载文件')
+		addClassPlan()
+	}
+	const nowTimesStr = Date.now()
+	const outNowTimesStr = ref()
+	const addClassPlan = () => {
+		classCentre
+			.classPlanAdd({
+				hourId: props.hourId,
+				type: 2,
+				funcType:2
+			})
+			.then((data) => {})
+	}
+	const addScrollPlan = () => {
+		classCentre
+			.classPlanAdd({
+				progress: maxStr.value,
+				hourId: props.hourId,
+				stayTime: outNowTimesStr.value - nowTimesStr,
+				type: 1,
+				funcType:2
+			})
+			.then((data) => {})
+	}
+	onBeforeUnmount(() => {
+		outNowTimesStr.value = Date.now()
+		addScrollPlan()
+	})
+</script>
+<style scoped lang="less">
+	.flc {
+		display: flex;
+		align-items: center;
+	}
+	.fcbc {
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+	}
+</style>

+ 82 - 105
src/views/student/classCentre/index.vue

@@ -67,20 +67,10 @@
 					<a-card :bordered="false" class="mt-2" style="width: 1200px">
 						<a-tabs v-model:activeKey="tabsActiveKey">
 							<a-tab-pane key="1" tab="讲义">
-								<div style="height: 900px">
-									<div v-if="!showPdf">渲染pdf失败</div>
-									<vue-office-pdf :src="itemObj.url" style="width: 100%; height: 100%" @error="errorHandler" />
-								</div>
+								<handouts :itemObj="itemObj" :hourId="selectedKeys[0]"></handouts>
 							</a-tab-pane>
 							<a-tab-pane key="2" tab="字幕">
-								<a-input v-model:value="subtitleVal" allowClear placeholder="请输入" />
-								<div style="height: 900px; overflow-y: auto" class="mt-2">
-									<div v-if="subtitle">{{ subtitle }}</div>
-									<div v-for="(item, idx) in subtitleList" :key="idx">
-										<div>{{ item.startTime }}~{{ item.endTime }}</div>
-										<div style="cursor: pointer; padding: 10px 0" @click="videoSpeed(item)">{{ item.text }}</div>
-									</div>
-								</div>
+								<subtitleBox :url="danmuObj.url" @videoSpeed="videoSpeed"></subtitleBox>
 							</a-tab-pane>
 							<a-tab-pane key="3" tab="笔记">
 								<note :idsObj="idsObj" ref="noteRef" @edit="noteEdit"></note>
@@ -91,7 +81,7 @@
 						</a-tabs>
 					</a-card>
 				</div>
-				<forumBtn :forumData="forumData"></forumBtn>
+				<forumBtn :forumData="forumData" resourceType="0"></forumBtn>
 			</a-layout-content>
 		</a-layout>
 	</a-layout>
@@ -104,8 +94,9 @@
 	import sysConfig from '@/config/index'
 	import note from './note.vue'
 	import askDiv from './ask.vue'
-	import VueOfficePdf from '@vue-office/pdf'
-	import axios from 'axios'
+	import handouts from './handouts.vue'
+	import subtitleBox from './subtitle.vue'
+
 	const route = useRoute()
 	const router = useRouter()
 	const classDetail = ref({})
@@ -115,13 +106,13 @@
 	const selectedKeys = ref([])
 	const collapsed = ref(false)
 	const videoRef = ref()
-	const currentTimenew = ref()
+	const currentTimenew = ref(0)
 	const idsObj = computed(() => {
 		let item = findNodeByKey(classTimeList.value, selectedKeys.value[0])
 		return {
 			courseId: route.query.id,
-			chapterId: item?.chapterId,
-			hourId: selectedKeys.value[0]
+			chapterId: selectedKeys.value[0],
+			hourId: classHourData.value?.chapterId
 		}
 	})
 	function findNodeByKey(list, id) {
@@ -160,6 +151,8 @@
 				}
 			})
 			videoRef.value.src = classTimeData.value.filter((r) => r.funcType == 1)[0]?.url
+			footprintClassAdd()
+			getVideoTime()
 		})
 	}
 
@@ -172,9 +165,6 @@
 	})
 	const danmuObj = computed(() => {
 		let item = classTimeData.value.length > 0 ? classTimeData.value.filter((r) => r.funcType == 3)[0] : { url: '' }
-		if (item?.url) {
-			GetSrtInfo(item.url)
-		}
 		return item
 	})
 	const videoUrl = ref('')
@@ -203,30 +193,42 @@
 			timeStamp1.value = parseInt(e.target.currentTime) //播放进度 (秒)
 			biNum.value = Math.floor((timeStamp1.value / allTime.value) * 10000) / 100 //暂时没用到
 			currentTime.value = e.target.currentTime
-			if (videoJumpTime.value) {
-				videoJumpTime.value = false
-				newsschedule.value = currentTime.value
-			} else {
-				if (newsschedule.value <= currentTime.value) {
-					//请求接口获取的参数就是判断当前观看的时长是否小于当前时间 小于当前时间就禁止拖动进度条
 
-					if (e.srcElement.currentTime - currTime.value > 3) {
-						//这里拖动进度条时间 - 当前观看的时长 > 3秒 这边判定它为拖动进度条
-						e.srcElement.currentTime = currTime.value > maxTime.value ? currTime.value : maxTime.value
-						let newsVal = initialtime.value
-						if (newsVal) {
-							videoContext.value.currentTime = newsVal
-						}
-						//当前用户记录观看时间 大于 实施播放时间
-						if (currTime.value > newsVal) {
-							videoContext.value.currentTime = currTime.value
-						}
-						console.log('快进了')
-					}
-					currTime.value = e.srcElement.currentTime
-					maxTime.value = currTime.value > maxTime.value ? currTime.value : maxTime.value
-				}
+			console.log('🚀 ~ timeUpdate ~ e.srcElement.currentTime:', e.srcElement.currentTime)
+			console.log('🚀 ~ timeUpdate ~ currTime.value:', currTime.value)
+			if (e.srcElement.currentTime - currTime.value > 3) {
+				addClassPlan(3)
+				console.log('快进了')
 			}
+			if (currTime.value - e.srcElement.currentTime > 3) {
+				addClassPlan(3)
+				console.log('快退了')
+			}
+			currTime.value = e.srcElement.currentTime
+			// if (videoJumpTime.value) {
+			// 	videoJumpTime.value = false
+			// 	newsschedule.value = currentTime.value
+			// } else {
+			// 	if (newsschedule.value <= currentTime.value) {
+			// 		//请求接口获取的参数就是判断当前观看的时长是否小于当前时间 小于当前时间就禁止拖动进度条
+
+			// 		if (e.srcElement.currentTime - currTime.value > 3) {
+			// 			//这里拖动进度条时间 - 当前观看的时长 > 3秒 这边判定它为拖动进度条
+			// 			e.srcElement.currentTime = currTime.value > maxTime.value ? currTime.value : maxTime.value
+			// 			let newsVal = initialtime.value
+			// 			if (newsVal) {
+			// 				videoContext.value.currentTime = newsVal
+			// 			}
+			// 			//当前用户记录观看时间 大于 实施播放时间
+			// 			if (currTime.value > newsVal) {
+			// 				videoContext.value.currentTime = currTime.value
+			// 			}
+			// 			console.log('快进了')
+			// 		}
+			// 		currTime.value = e.srcElement.currentTime
+			// 		maxTime.value = currTime.value > maxTime.value ? currTime.value : maxTime.value
+			// 	}
+			// }
 		}
 	}
 
@@ -242,47 +244,7 @@
 		videoJumpTime.value = true
 		currentTimenew.value = e.startTime
 	}
-	//获取字幕内容,发送一个get请求
-	function GetSrtInfo(srtUrl) {
-		subtitle.value = ''
-		axios
-			.get(`${srtUrl}`)
-			.then(function (response) {
-				try {
-					let textList = response.data
-						.split(/\n\s\n/)
-						.filter((item) => item != '')
-						.map((item, index) => {
-							let textItem = item.split(/\n/)
-							return {
-								index: index,
-								sort: textItem[0],
-								text: textItem[2],
-								startTime: ToSeconds(textItem[1].split(' --> ')[0]),
-								endTime: ToSeconds(textItem[1].split(' --> ')[1]),
-								timeLine: textItem[1]
-							}
-						})
-					danmuObj.value.srtInfoList = textList
-				} catch (error) {
-					subtitle.value = '字幕解析失败'
-				}
-			})
-			.catch(function (error) {
-				console.log(error)
-			})
-	}
-	//将时间转化为秒
-	function ToSeconds(t) {
-		var s = 0.0
-		if (t) {
-			var p = t.split(':')
-			for (let i = 0; i < p.length; i++) {
-				s = s * 60 + parseFloat(p[i].replace(',', '.'))
-			}
-		}
-		return s
-	}
+
 	const forumData = computed(() => {
 		let item = findNodeByKey(classTimeList.value, selectedKeys.value[0]) ?? {}
 		if (!item.src) {
@@ -291,19 +253,30 @@
 		return {
 			id: selectedKeys.value[0],
 			title: item?.name,
-			videoUrl: btoa(encodeURIComponent(videoRef.value?.src ? videoRef.value.src : item.src))
+			videoUrl: btoa(encodeURIComponent(videoRef.value?.src ? videoRef.value.src : item.src)),
+			courseId: route.query.id,
+			chapterId: selectedKeys.value[0],
+			courseName: classDetail.value?.courseName,
+			chapterName: classHourData.value?.name
 		}
 	})
 	const showPdf = ref(true)
 	function errorHandler() {
 		showPdf.value = false
 	}
-	const subtitle = ref()
 	const getVideoTime = () => {
-		classCentre.theLastDetail().then((data) => {
-			initialtime.value = parseFloat(data.endTime) / 1000
-			currentTimenew.value = parseFloat(data.endTime) / 1000
-		})
+		classCentre
+			.theLastDetail({
+				funcType: 1,
+				type: 1,
+				hourld: classHourData.value?.chapterId
+			})
+			.then((data) => {
+				if (data.endTime) {
+					initialtime.value = parseFloat(data.endTime) / 1000
+					currentTimenew.value = parseFloat(data.endTime) / 1000
+				}
+			})
 	}
 
 	// 毫秒转换时分秒
@@ -331,12 +304,12 @@
 		return ms
 	}
 	onBeforeUnmount(() => {
-		outNowTimesStr.value = Date.now()
-		addClassPlan()
+		addClassPlan(1)
 	})
 	const nowTimesStr = Date.now()
 	const outNowTimesStr = ref()
-	const addClassPlan = () => {
+	const addClassPlan = (type) => {
+		outNowTimesStr.value = Date.now()
 		let progress = parseFloat((videoRef.value?.currentTime / videoRef.value?.duration) * 100)
 		if (
 			(progress || progress == 0) &&
@@ -348,9 +321,10 @@
 					startTime: parseFloat(initialtime.value),
 					endTime: Math.round(videoRef.value.currentTime * 1000),
 					progress: Math.round(progress),
-					hourId: selectedKeys.value[0],
+					hourId: classHourData.value?.chapterId,
 					stayTime: outNowTimesStr.value - nowTimesStr,
-					type: 1
+					type: type,
+					funcType: type
 				})
 				.then((data) => {})
 		}
@@ -369,17 +343,20 @@
 				})
 			})
 	}
-	// 模糊查询函数
-	const subtitleVal = ref()
-	const subtitleList = computed(() => {
-		return danmuObj.value.srtInfoList?.filter((item) => {
-			if (subtitleVal.value) {
-				return item.text.includes(subtitleVal.value)
-			} else {
-				return true
-			}
+	//足迹
+	const footprintClassAdd = () => {
+		classCentre.footprintClassAdd({
+			hourName: classHourData.value.name,
+			chapterName: classHourData.value.name,
+			courseName: classDetail.value.courseName,
+			hourId: classHourData.value.chapterId,
+			chapterId: selectedKeys.value[0],
+			courseId: classDetail.value.courseId,
+			fileId: classHourData.value.courseRelates.filter((r) => r.funcType == 1)[0].relateId,
+			fileName: classHourData.value.courseRelates.filter((r) => r.funcType == 1)[0].name,
+			filePath: classHourData.value.courseRelates.filter((r) => r.funcType == 1)[0].url
 		})
-	})
+	}
 	onMounted(() => {
 		getClassData()
 		getVideoTime()

+ 94 - 0
src/views/student/classCentre/subtitle.vue

@@ -0,0 +1,94 @@
+<template>
+	<a-input v-model:value="subtitleVal" allowClear placeholder="请输入" />
+	<div style="height: 900px; overflow-y: auto" class="mt-2">
+		<div v-if="subtitle">{{ subtitle }}</div>
+		<div v-for="(item, idx) in subtitleList" :key="idx">
+			<div>{{ item.startTime }}~{{ item.endTime }}</div>
+			<div style="cursor: pointer; padding: 10px 0" @click="videoSpeed(item)">{{ item.text }}</div>
+		</div>
+	</div>
+</template>
+
+<script setup>
+	import axios from 'axios'
+	const props = defineProps({
+		url: {
+			type: String,
+			default: ''
+		}
+	})
+	watch(
+		() => props.url,
+		(newVal, oldVal) => {
+			if (newVal) {
+				nextTick(() => {
+					GetSrtInfo(newVal)
+				})
+			}
+		},
+		{ immediate: true }
+	)
+	const emit = defineEmits({ videoSpeed: null })
+	const videoSpeed = (e) => {
+		emit('videoSpeed', e)
+	}
+	// 模糊查询函数
+	const subtitleVal = ref()
+	const srtInfoList = ref([])
+	const subtitleList = computed(() => {
+		return srtInfoList.value.filter((item) => {
+			if (subtitleVal.value) {
+				return item.text.includes(subtitleVal.value)
+			} else {
+				return true
+			}
+		})
+	})
+	//获取字幕内容,发送一个get请求
+	const subtitle = ref()
+	const GetSrtInfo = (srtUrl) => {
+		subtitle.value = ''
+		axios
+			.get(`${srtUrl}`)
+			.then(function (response) {
+				try {
+					let textList = response.data
+						.split(/\n\s\n/)
+						.filter((item) => item != '')
+						.map((item, index) => {
+							let textItem = item.split(/\n/)
+							return {
+								index: index,
+								sort: textItem[0],
+								text: textItem[2],
+								startTime: ToSeconds(textItem[1].split(' --> ')[0]),
+								endTime: ToSeconds(textItem[1].split(' --> ')[1]),
+								timeLine: textItem[1]
+							}
+						})
+					subtitle.value = false
+					srtInfoList.value = textList
+				} catch (error) {
+					subtitle.value = '字幕解析失败'
+				}
+			})
+			.catch(function (error) {
+				console.log(error)
+			})
+	}
+	//将时间转化为秒
+	function ToSeconds(t) {
+		var s = 0.0
+		if (t) {
+			var p = t.split(':')
+			for (let i = 0; i < p.length; i++) {
+				s = s * 60 + parseFloat(p[i].replace(',', '.'))
+			}
+		}
+		return s
+	}
+	defineExpose({
+		GetSrtInfo
+	})
+</script>
+<style scoped lang="less"></style>

+ 17 - 3
src/views/student/exam/paper/do.vue

@@ -68,7 +68,7 @@
 				</a-form>
 			</a-layout-content>
 		</a-layout>
-		<forumBtn :forumData="forumData"></forumBtn>
+		<forumBtn :forumData="forumData" :resourceType="resourceType"></forumBtn>
 	</div>
 </template>
 
@@ -96,7 +96,20 @@
 	const timer = ref(null)
 	const remainTime = ref(0)
 	const formRef = ref()
-
+	const resourceType = computed(() => {
+		//考试
+		if (form.examType == 1 || form.paperType == 6) {
+			return 2
+		}
+		//调查问卷
+		if (form.examType == 3 || form.paperType == 5) {
+			return 2
+		}
+		//作业
+		if (form.paperType == 2) {
+			return 1
+		}
+	})
 	function questionCompleted(completed) {
 		// doCompletedTag: [{ key: false, value: 'info' }, { key: true, value: 'success' }]
 		return examStore.enumFormat(examStore.exam.question.answer.doCompletedTag, completed)
@@ -165,6 +178,7 @@
 			formLoading.value = true
 			examPaperApi.select(id).then((re) => {
 				Object.assign(form, re)
+				console.log('🚀 ~ form:', form)
 				remainTime.value = re.suggestTime * 60
 				initAnswer()
 				timeReduce()
@@ -180,7 +194,7 @@
 	const forumData = computed(() => {
 		return {
 			id: route.query.id,
-			title: form.name ,
+			title: form.name
 		}
 	})
 </script>

+ 61 - 16
src/views/student/forumBtn/index.vue

@@ -1,16 +1,50 @@
 <template>
 	<div class="redressBox">
-		<div @click="jumpRedressUrl(2)" v-if="props.isShow.includes(2)" class="btnBox">
-			<CopyOutlined style="font-size: 30px" />
-			<div>纠错</div>
+		<div class="addBox joinBox">
+			<div v-if="props.isShow.includes(2)" class="btnBox">
+				<router-link
+					:to="{
+						path: '/forum/addForum',
+						query: { postType: 2, resourceType: props.resourceType, ...props.forumData }
+					}"
+					target="_blank"
+				>
+					<CopyOutlined style="font-size: 30px" class="textColor" />
+					<div class="textColor">纠错</div>
+				</router-link>
+			</div>
+			<div v-if="props.isShow.includes(1)" class="btnBox mt-2">
+				<router-link
+					:to="{
+						path: '/forum/addForum',
+						query: { postType: 1, resourceType: props.resourceType, ...props.forumData }
+					}"
+					target="_blank"
+				>
+					<SnippetsOutlined class="textColor" style="font-size: 30px" />
+					<div class="textColor">技术</div>
+				</router-link>
+			</div>
+			<div v-if="props.isShow.includes(0)" class="btnBox mt-2">
+				<router-link
+					:to="{
+						path: '/forum/detail',
+						query: { postType: 4, resourceType: props.resourceType, ...props.forumData }
+					}"
+					target="_blank"
+				>
+					<FileTextOutlined class="textColor" style="font-size: 30px" />
+					<div class="textColor">章节</div>
+				</router-link>
+			</div>
 		</div>
-		<div @click="jumpRedressUrl(1)" v-if="props.isShow.includes(1)" class="btnBox mt-2">
-			<SnippetsOutlined style="font-size: 30px" />
-			<div>技术</div>
-		</div>
-		<div @click="jumpRedressUrl(3)" v-if="props.isShow.includes(3)" class="btnBox mt-2">
-			<CommentOutlined style="font-size: 30px" />
-			<div>讨论</div>
+		<div class="mt-4 joinBox">
+			<div v-if="props.isShow.includes(3)" class="btnBox mt-2">
+				<router-link :to="{ path: '/forum' }" target="_blank">
+					<CommentOutlined class="textColor" style="font-size: 30px" />
+					<div class="textColor">讨论</div>
+				</router-link>
+			</div>
 		</div>
 	</div>
 </template>
@@ -21,11 +55,15 @@
 	const props = defineProps({
 		isShow: {
 			type: Array,
-			default: [1, 2, 3]
+			default: [0, 1, 2, 3]
 		},
 		forumData: {
 			type: Object,
 			default: () => {}
+		},
+		resourceType: {
+			type: [String, Number],
+			default: ''
 		}
 	})
 	const jumpRedressUrl = (t) => {
@@ -33,7 +71,8 @@
 			path: '/forum/addForum',
 			query: {
 				...props.forumData,
-				postType: t
+				postType: t,
+				resourceType: props.resourceType
 			}
 		})
 	}
@@ -42,16 +81,22 @@
 <style lang="less" scoped>
 	.redressBox {
 		position: fixed;
+		right: 20px;
+		bottom: 150px;
+	}
+	.addBox {
 		display: flex;
 		justify-content: center;
 		align-items: center;
 		flex-direction: column;
-		right: 20px;
-		bottom: 200px;
-		color: #969696;
+	}
+	.joinBox {
 		background-color: #fff;
 		border-radius: 5px;
-		padding:10px;
+		padding: 10px;
+	}
+	.textColor {
+		color: #969696;
 	}
 	.btnBox {
 		display: flex;

+ 12 - 6
src/views/student/learningFootprint/index.vue

@@ -2,11 +2,11 @@
 	<a-card>
 		<a-list item-layout="vertical" size="large" :pagination="pagination" :data-source="listData">
 			<template #renderItem="{ item }">
-				<a-list-item key="item.title">
-					<a-list-item-meta :description="item.extendName">
-						<template #title>{{ item.fileName }}</template>
+				<a-list-item>
+					<a-list-item-meta :description="item.chapterName">
+						<template #title>{{ item.courseName }}</template>
 					</a-list-item-meta>
-					{{ item.filePath }}
+					{{ item.fileName }}
 				</a-list-item>
 			</template>
 		</a-list>
@@ -23,8 +23,14 @@
 		pageSize: 10
 	})
 	const getList = (current) => {
-		classCentre
-			.footprintList({
+		// classCentre.footprintList({
+		// 		current: current ? current : pagination.value.current,
+		// 		size: pagination.value.pageSize
+		// 	})
+		// 	.then((data) => {
+		// 		listData.value = data.records
+		// 	})
+		classCentre.footprintClassList({
 				current: current ? current : pagination.value.current,
 				size: pagination.value.pageSize
 			})

Diff do ficheiro suprimidas por serem muito extensas
+ 146 - 0
stats.html


Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff